diff --git a/examples/slicing/SectionCaps.html b/examples/slicing/SectionCaps.html
new file mode 100644
index 0000000000..050be3744c
--- /dev/null
+++ b/examples/slicing/SectionCaps.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+ xeokit Example
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html b/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html
new file mode 100644
index 0000000000..cb7bb76be5
--- /dev/null
+++ b/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+ xeokit Example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/viewer/scene/libs/csg.js b/src/viewer/scene/libs/csg.js
new file mode 100644
index 0000000000..7a36990071
--- /dev/null
+++ b/src/viewer/scene/libs/csg.js
@@ -0,0 +1,600 @@
+// Constructive Solid Geometry (CSG) is a modeling technique that uses Boolean
+// operations like union and intersection to combine 3D solids. This library
+// implements CSG operations on meshes elegantly and concisely using BSP trees,
+// and is meant to serve as an easily understandable implementation of the
+// algorithm. All edge cases involving overlapping coplanar polygons in both
+// solids are correctly handled.
+//
+// Example usage:
+//
+// var cube = CSG.cube();
+// var sphere = CSG.sphere({ radius: 1.3 });
+// var polygons = cube.subtract(sphere).toPolygons();
+//
+// ## Implementation Details
+//
+// All CSG operations are implemented in terms of two functions, `clipTo()` and
+// `invert()`, which remove parts of a BSP tree inside another BSP tree and swap
+// solid and empty space, respectively. To find the union of `a` and `b`, we
+// want to remove everything in `a` inside `b` and everything in `b` inside `a`,
+// then combine polygons from `a` and `b` into one solid:
+//
+// a.clipTo(b);
+// b.clipTo(a);
+// a.build(b.allPolygons());
+//
+// The only tricky part is handling overlapping coplanar polygons in both trees.
+// The code above keeps both copies, but we need to keep them in one tree and
+// remove them in the other tree. To remove them from `b` we can clip the
+// inverse of `b` against `a`. The code for union now looks like this:
+//
+// a.clipTo(b);
+// b.clipTo(a);
+// b.invert();
+// b.clipTo(a);
+// b.invert();
+// a.build(b.allPolygons());
+//
+// Subtraction and intersection naturally follow from set operations. If
+// union is `A | B`, subtraction is `A - B = ~(~A | B)` and intersection is
+// `A & B = ~(~A | ~B)` where `~` is the complement operator.
+//
+// ## License
+//
+// Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license.
+
+// # class CSG
+
+// Holds a binary space partition tree representing a 3D solid. Two solids can
+// be combined using the `union()`, `subtract()`, and `intersect()` methods.
+
+let CSG = {}
+
+CSG = function () {
+ this.polygons = [];
+};
+
+// Construct a CSG solid from a list of `CSG.Polygon` instances.
+CSG.fromPolygons = function (polygons) {
+ var csg = new CSG();
+ csg.polygons = polygons;
+ return csg;
+};
+
+CSG.prototype = {
+ clone: function () {
+ var csg = new CSG();
+ csg.polygons = this.polygons.map(function (p) { return p.clone(); });
+ return csg;
+ },
+
+ toPolygons: function () {
+ return this.polygons;
+ },
+
+ // Return a new CSG solid representing space in either this solid or in the
+ // solid `csg`. Neither this solid nor the solid `csg` are modified.
+ //
+ // A.union(B)
+ //
+ // +-------+ +-------+
+ // | | | |
+ // | A | | |
+ // | +--+----+ = | +----+
+ // +----+--+ | +----+ |
+ // | B | | |
+ // | | | |
+ // +-------+ +-------+
+ //
+ union: function (csg) {
+ var a = new CSG.Node(this.clone().polygons);
+ var b = new CSG.Node(csg.clone().polygons);
+ a.clipTo(b);
+ b.clipTo(a);
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+ a.build(b.allPolygons());
+ return CSG.fromPolygons(a.allPolygons());
+ },
+
+ // Return a new CSG solid representing space in this solid but not in the
+ // solid `csg`. Neither this solid nor the solid `csg` are modified.
+ //
+ // A.subtract(B)
+ //
+ // +-------+ +-------+
+ // | | | |
+ // | A | | |
+ // | +--+----+ = | +--+
+ // +----+--+ | +----+
+ // | B |
+ // | |
+ // +-------+
+ //
+ subtract: function (csg) {
+ var a = new CSG.Node(this.clone().polygons);
+ var b = new CSG.Node(csg.clone().polygons);
+ a.invert();
+ a.clipTo(b);
+ b.clipTo(a);
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+ a.build(b.allPolygons());
+ a.invert();
+ return CSG.fromPolygons(a.allPolygons());
+ },
+
+ // Return a new CSG solid representing space both this solid and in the
+ // solid `csg`. Neither this solid nor the solid `csg` are modified.
+ //
+ // A.intersect(B)
+ //
+ // +-------+
+ // | |
+ // | A |
+ // | +--+----+ = +--+
+ // +----+--+ | +--+
+ // | B |
+ // | |
+ // +-------+
+ //
+ intersect: function (csg) {
+ var a = new CSG.Node(this.clone().polygons);
+ var b = new CSG.Node(csg.clone().polygons);
+ a.invert();
+ b.clipTo(a);
+ b.invert();
+ a.clipTo(b);
+ b.clipTo(a);
+ a.build(b.allPolygons());
+ a.invert();
+ return CSG.fromPolygons(a.allPolygons());
+ },
+
+ // Return a new CSG solid with solid and empty space switched. This solid is
+ // not modified.
+ inverse: function () {
+ var csg = this.clone();
+ csg.polygons.map(function (p) { p.flip(); });
+ return csg;
+ }
+};
+
+// Construct an axis-aligned solid cuboid. Optional parameters are `center` and
+// `radius`, which default to `[0, 0, 0]` and `[1, 1, 1]`. The radius can be
+// specified using a single number or a list of three numbers, one for each axis.
+//
+// Example code:
+//
+// var cube = CSG.cube({
+// center: [0, 0, 0],
+// radius: 1
+// });
+CSG.cube = function (options) {
+ options = options || {};
+ var c = new CSG.Vector(options.center || [0, 0, 0]);
+ var r = !options.radius ? [1, 1, 1] : options.radius.length ?
+ options.radius : [options.radius, options.radius, options.radius];
+ return CSG.fromPolygons([
+ [[0, 4, 6, 2], [-1, 0, 0]],
+ [[1, 3, 7, 5], [+1, 0, 0]],
+ [[0, 1, 5, 4], [0, -1, 0]],
+ [[2, 6, 7, 3], [0, +1, 0]],
+ [[0, 2, 3, 1], [0, 0, -1]],
+ [[4, 5, 7, 6], [0, 0, +1]]
+ ].map(function (info) {
+ return new CSG.Polygon(info[0].map(function (i) {
+ var pos = new CSG.Vector(
+ c.x + r[0] * (2 * !!(i & 1) - 1),
+ c.y + r[1] * (2 * !!(i & 2) - 1),
+ c.z + r[2] * (2 * !!(i & 4) - 1)
+ );
+ return new CSG.Vertex(pos, new CSG.Vector(info[1]));
+ }));
+ }));
+};
+
+// Construct a solid sphere. Optional parameters are `center`, `radius`,
+// `slices`, and `stacks`, which default to `[0, 0, 0]`, `1`, `16`, and `8`.
+// The `slices` and `stacks` parameters control the tessellation along the
+// longitude and latitude directions.
+//
+// Example usage:
+//
+// var sphere = CSG.sphere({
+// center: [0, 0, 0],
+// radius: 1,
+// slices: 16,
+// stacks: 8
+// });
+CSG.sphere = function (options) {
+ options = options || {};
+ var c = new CSG.Vector(options.center || [0, 0, 0]);
+ var r = options.radius || 1;
+ var slices = options.slices || 16;
+ var stacks = options.stacks || 8;
+ var polygons = [], vertices;
+ function vertex(theta, phi) {
+ theta *= Math.PI * 2;
+ phi *= Math.PI;
+ var dir = new CSG.Vector(
+ Math.cos(theta) * Math.sin(phi),
+ Math.cos(phi),
+ Math.sin(theta) * Math.sin(phi)
+ );
+ vertices.push(new CSG.Vertex(c.plus(dir.times(r)), dir));
+ }
+ for (var i = 0; i < slices; i++) {
+ for (var j = 0; j < stacks; j++) {
+ vertices = [];
+ vertex(i / slices, j / stacks);
+ if (j > 0) vertex((i + 1) / slices, j / stacks);
+ if (j < stacks - 1) vertex((i + 1) / slices, (j + 1) / stacks);
+ vertex(i / slices, (j + 1) / stacks);
+ polygons.push(new CSG.Polygon(vertices));
+ }
+ }
+ return CSG.fromPolygons(polygons);
+};
+
+// Construct a solid cylinder. Optional parameters are `start`, `end`,
+// `radius`, and `slices`, which default to `[0, -1, 0]`, `[0, 1, 0]`, `1`, and
+// `16`. The `slices` parameter controls the tessellation.
+//
+// Example usage:
+//
+// var cylinder = CSG.cylinder({
+// start: [0, -1, 0],
+// end: [0, 1, 0],
+// radius: 1,
+// slices: 16
+// });
+CSG.cylinder = function (options) {
+ options = options || {};
+ var s = new CSG.Vector(options.start || [0, -1, 0]);
+ var e = new CSG.Vector(options.end || [0, 1, 0]);
+ var ray = e.minus(s);
+ var r = options.radius || 1;
+ var slices = options.slices || 16;
+ var axisZ = ray.unit(), isY = (Math.abs(axisZ.y) > 0.5);
+ var axisX = new CSG.Vector(isY, !isY, 0).cross(axisZ).unit();
+ var axisY = axisX.cross(axisZ).unit();
+ var start = new CSG.Vertex(s, axisZ.negated());
+ var end = new CSG.Vertex(e, axisZ.unit());
+ var polygons = [];
+ function point(stack, slice, normalBlend) {
+ var angle = slice * Math.PI * 2;
+ var out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle)));
+ var pos = s.plus(ray.times(stack)).plus(out.times(r));
+ var normal = out.times(1 - Math.abs(normalBlend)).plus(axisZ.times(normalBlend));
+ return new CSG.Vertex(pos, normal);
+ }
+ for (var i = 0; i < slices; i++) {
+ var t0 = i / slices, t1 = (i + 1) / slices;
+ polygons.push(new CSG.Polygon([start, point(0, t0, -1), point(0, t1, -1)]));
+ polygons.push(new CSG.Polygon([point(0, t1, 0), point(0, t0, 0), point(1, t0, 0), point(1, t1, 0)]));
+ polygons.push(new CSG.Polygon([end, point(1, t1, 1), point(1, t0, 1)]));
+ }
+ return CSG.fromPolygons(polygons);
+};
+
+// # class Vector
+
+// Represents a 3D vector.
+//
+// Example usage:
+//
+// new CSG.Vector(1, 2, 3);
+// new CSG.Vector([1, 2, 3]);
+// new CSG.Vector({ x: 1, y: 2, z: 3 });
+
+CSG.Vector = function (x, y, z) {
+ if (arguments.length == 3) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ } else if ('x' in x) {
+ this.x = x.x;
+ this.y = x.y;
+ this.z = x.z;
+ } else {
+ this.x = x[0];
+ this.y = x[1];
+ this.z = x[2];
+ }
+};
+
+CSG.Vector.prototype = {
+ clone: function () {
+ return new CSG.Vector(this.x, this.y, this.z);
+ },
+
+ negated: function () {
+ return new CSG.Vector(-this.x, -this.y, -this.z);
+ },
+
+ plus: function (a) {
+ return new CSG.Vector(this.x + a.x, this.y + a.y, this.z + a.z);
+ },
+
+ minus: function (a) {
+ return new CSG.Vector(this.x - a.x, this.y - a.y, this.z - a.z);
+ },
+
+ times: function (a) {
+ return new CSG.Vector(this.x * a, this.y * a, this.z * a);
+ },
+
+ dividedBy: function (a) {
+ return new CSG.Vector(this.x / a, this.y / a, this.z / a);
+ },
+
+ dot: function (a) {
+ return this.x * a.x + this.y * a.y + this.z * a.z;
+ },
+
+ lerp: function (a, t) {
+ return this.plus(a.minus(this).times(t));
+ },
+
+ length: function () {
+ return Math.sqrt(this.dot(this));
+ },
+
+ unit: function () {
+ return this.dividedBy(this.length());
+ },
+
+ cross: function (a) {
+ return new CSG.Vector(
+ this.y * a.z - this.z * a.y,
+ this.z * a.x - this.x * a.z,
+ this.x * a.y - this.y * a.x
+ );
+ }
+};
+
+// # class Vertex
+
+// Represents a vertex of a polygon. Use your own vertex class instead of this
+// one to provide additional features like texture coordinates and vertex
+// colors. Custom vertex classes need to provide a `pos` property and `clone()`,
+// `flip()`, and `interpolate()` methods that behave analogous to the ones
+// defined by `CSG.Vertex`. This class provides `normal` so convenience
+// functions like `CSG.sphere()` can return a smooth vertex normal, but `normal`
+// is not used anywhere else.
+
+CSG.Vertex = function (pos, normal) {
+ this.pos = new CSG.Vector(pos);
+ this.normal = normal ? new CSG.Vector(normal) : new CSG.Vector(pos);
+ // this.normal = new CSG.Vector(normal);
+};
+
+CSG.Vertex.prototype = {
+ clone: function () {
+ return new CSG.Vertex(this.pos.clone(), this.normal.clone());
+ },
+
+ // Invert all orientation-specific data (e.g. vertex normal). Called when the
+ // orientation of a polygon is flipped.
+ flip: function () {
+ this.normal = this.normal.negated();
+ },
+
+ // Create a new vertex between this vertex and `other` by linearly
+ // interpolating all properties using a parameter of `t`. Subclasses should
+ // override this to interpolate additional properties.
+ interpolate: function (other, t) {
+ return new CSG.Vertex(
+ this.pos.lerp(other.pos, t),
+ this.normal.lerp(other.normal, t)
+ );
+ }
+};
+
+// # class Plane
+
+// Represents a plane in 3D space.
+
+CSG.Plane = function (normal, w) {
+ this.normal = normal;
+ this.w = w;
+};
+
+// `CSG.Plane.EPSILON` is the tolerance used by `splitPolygon()` to decide if a
+// point is on the plane.
+CSG.Plane.EPSILON = 1e-5;
+
+CSG.Plane.fromPoints = function (a, b, c) {
+ var n = b.minus(a).cross(c.minus(a)).unit();
+ return new CSG.Plane(n, n.dot(a));
+};
+
+CSG.Plane.prototype = {
+ clone: function () {
+ return new CSG.Plane(this.normal.clone(), this.w);
+ },
+
+ flip: function () {
+ this.normal = this.normal.negated();
+ this.w = -this.w;
+ },
+
+ // Split `polygon` by this plane if needed, then put the polygon or polygon
+ // fragments in the appropriate lists. Coplanar polygons go into either
+ // `coplanarFront` or `coplanarBack` depending on their orientation with
+ // respect to this plane. Polygons in front or in back of this plane go into
+ // either `front` or `back`.
+ splitPolygon: function (polygon, coplanarFront, coplanarBack, front, back) {
+ var COPLANAR = 0;
+ var FRONT = 1;
+ var BACK = 2;
+ var SPANNING = 3;
+
+ // Classify each point as well as the entire polygon into one of the above
+ // four classes.
+ var polygonType = 0;
+ var types = [];
+ for (var i = 0; i < polygon.vertices.length; i++) {
+ var t = this.normal.dot(polygon.vertices[i].pos) - this.w;
+ var type = (t < -CSG.Plane.EPSILON) ? BACK : (t > CSG.Plane.EPSILON) ? FRONT : COPLANAR;
+ polygonType |= type;
+ types.push(type);
+ }
+
+ // Put the polygon in the correct list, splitting it when necessary.
+ switch (polygonType) {
+ case COPLANAR:
+ (this.normal.dot(polygon.plane.normal) > 0 ? coplanarFront : coplanarBack).push(polygon);
+ break;
+ case FRONT:
+ front.push(polygon);
+ break;
+ case BACK:
+ back.push(polygon);
+ break;
+ case SPANNING:
+ var f = [], b = [];
+ for (var i = 0; i < polygon.vertices.length; i++) {
+ var j = (i + 1) % polygon.vertices.length;
+ var ti = types[i], tj = types[j];
+ var vi = polygon.vertices[i], vj = polygon.vertices[j];
+ if (ti != BACK) f.push(vi);
+ if (ti != FRONT) b.push(ti != BACK ? vi.clone() : vi);
+ if ((ti | tj) == SPANNING) {
+ var t = (this.w - this.normal.dot(vi.pos)) / this.normal.dot(vj.pos.minus(vi.pos));
+ var v = vi.interpolate(vj, t);
+ f.push(v);
+ b.push(v.clone());
+ }
+ }
+ if (f.length >= 3) front.push(new CSG.Polygon(f, polygon.shared));
+ if (b.length >= 3) back.push(new CSG.Polygon(b, polygon.shared));
+ break;
+ }
+ }
+};
+
+// # class Polygon
+
+// Represents a convex polygon. The vertices used to initialize a polygon must
+// be coplanar and form a convex loop. They do not have to be `CSG.Vertex`
+// instances but they must behave similarly (duck typing can be used for
+// customization).
+//
+// Each convex polygon has a `shared` property, which is shared between all
+// polygons that are clones of each other or were split from the same polygon.
+// This can be used to define per-polygon properties (such as surface color).
+
+CSG.Polygon = function (vertices, shared) {
+ this.vertices = vertices;
+ this.shared = shared;
+ this.plane = CSG.Plane.fromPoints(vertices[0].pos, vertices[1].pos, vertices[2].pos);
+};
+
+CSG.Polygon.prototype = {
+ clone: function () {
+ var vertices = this.vertices.map(function (v) { return v.clone(); });
+ return new CSG.Polygon(vertices, this.shared);
+ },
+
+ flip: function () {
+ this.vertices.reverse().map(function (v) { v.flip(); });
+ this.plane.flip();
+ }
+};
+
+// # class Node
+
+// Holds a node in a BSP tree. A BSP tree is built from a collection of polygons
+// by picking a polygon to split along. That polygon (and all other coplanar
+// polygons) are added directly to that node and the other polygons are added to
+// the front and/or back subtrees. This is not a leafy BSP tree since there is
+// no distinction between internal and leaf nodes.
+
+CSG.Node = function (polygons) {
+ this.plane = null;
+ this.front = null;
+ this.back = null;
+ this.polygons = [];
+ if (polygons) this.build(polygons);
+};
+
+CSG.Node.prototype = {
+ clone: function () {
+ var node = new CSG.Node();
+ node.plane = this.plane && this.plane.clone();
+ node.front = this.front && this.front.clone();
+ node.back = this.back && this.back.clone();
+ node.polygons = this.polygons.map(function (p) { return p.clone(); });
+ return node;
+ },
+
+ // Convert solid space to empty space and empty space to solid space.
+ invert: function () {
+ for (var i = 0; i < this.polygons.length; i++) {
+ this.polygons[i].flip();
+ }
+ this.plane.flip();
+ if (this.front) this.front.invert();
+ if (this.back) this.back.invert();
+ var temp = this.front;
+ this.front = this.back;
+ this.back = temp;
+ },
+
+ // Recursively remove all polygons in `polygons` that are inside this BSP
+ // tree.
+ clipPolygons: function (polygons) {
+ if (!this.plane) return polygons.slice();
+ var front = [], back = [];
+ for (var i = 0; i < polygons.length; i++) {
+ this.plane.splitPolygon(polygons[i], front, back, front, back);
+ }
+ if (this.front) front = this.front.clipPolygons(front);
+ if (this.back) back = this.back.clipPolygons(back);
+ else back = [];
+ return front.concat(back);
+ },
+
+ // Remove all polygons in this BSP tree that are inside the other BSP tree
+ // `bsp`.
+ clipTo: function (bsp) {
+ this.polygons = bsp.clipPolygons(this.polygons);
+ if (this.front) this.front.clipTo(bsp);
+ if (this.back) this.back.clipTo(bsp);
+ },
+
+ // Return a list of all polygons in this BSP tree.
+ allPolygons: function () {
+ var polygons = this.polygons.slice();
+ if (this.front) polygons = polygons.concat(this.front.allPolygons());
+ if (this.back) polygons = polygons.concat(this.back.allPolygons());
+ return polygons;
+ },
+
+ // Build a BSP tree out of `polygons`. When called on an existing tree, the
+ // new polygons are filtered down to the bottom of the tree and become new
+ // nodes there. Each set of polygons is partitioned using the first polygon
+ // (no heuristic is used to pick a good split).
+ build: function (polygons) {
+ if (!polygons.length) return;
+ if (!this.plane) this.plane = polygons[0].plane.clone();
+ var front = [], back = [];
+ for (var i = 0; i < polygons.length; i++) {
+ this.plane.splitPolygon(polygons[i], this.polygons, this.polygons, front, back);
+ }
+ if (front.length) {
+ if (!this.front) this.front = new CSG.Node();
+ this.front.build(front);
+ }
+ if (back.length) {
+ if (!this.back) this.back = new CSG.Node();
+ this.back.build(back);
+ }
+ }
+};
+
+export default CSG
diff --git a/src/viewer/scene/model/SceneModelEntity.js b/src/viewer/scene/model/SceneModelEntity.js
index b2a4052a1c..ff5839effa 100644
--- a/src/viewer/scene/model/SceneModelEntity.js
+++ b/src/viewer/scene/model/SceneModelEntity.js
@@ -1,5 +1,6 @@
import {ENTITY_FLAGS} from './ENTITY_FLAGS.js';
import {math} from "../math/math.js";
+import { Material } from '../materials/Material.js';
const tempFloatRGB = new Float32Array([0, 0, 0]);
const tempIntRGB = new Uint16Array([0, 0, 0]);
@@ -43,6 +44,7 @@ export class SceneModelEntity {
this.meshes = meshes;
this._numPrimitives = 0;
+ this._capMaterial = null;
for (let i = 0, len = this.meshes.length; i < len; i++) { // TODO: tidier way? Refactor?
const mesh = this.meshes[i];
@@ -644,6 +646,29 @@ export class SceneModelEntity {
return this.model.saoEnabled;
}
+ /**
+ * Sets the SceneModelEntity's capMaterial that will be used on the caps generated when this entity is sliced
+ *
+ * Default value is ````null````.
+ *
+ * @type {Material}
+ */
+ set capMaterial(value) {
+ this._capMaterial = value instanceof Material ? value : null;
+ this.scene._capMaterialUpdated();
+ }
+
+ /**
+ * Gets the SceneModelEntity's capMaterial.
+ *
+ * Default value is ````null````.
+ *
+ * @type {Material}
+ */
+ get capMaterial() {
+ return this._capMaterial;
+ }
+
getEachVertex(callback) {
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i].getEachVertex(callback)
diff --git a/src/viewer/scene/scene/Scene.js b/src/viewer/scene/scene/Scene.js
index d2745e71c7..0c3d748254 100644
--- a/src/viewer/scene/scene/Scene.js
+++ b/src/viewer/scene/scene/Scene.js
@@ -19,6 +19,7 @@ import {SAO} from "../postfx/SAO.js";
import {CrossSections} from "../postfx/CrossSections.js";
import {PointsMaterial} from "../materials/PointsMaterial.js";
import {LinesMaterial} from "../materials/LinesMaterial.js";
+import {SectionCaps} from '../sectionCaps/SectionCaps.js';
// Enables runtime check for redundant calls to object state update methods, eg. Scene#_objectVisibilityUpdated
const ASSERT_OBJECT_STATE_UPDATE = false;
@@ -887,6 +888,8 @@ class Scene extends Component {
dontClear: true // Never destroy this component with Scene#clear();
});
+ this._sectionCaps = new SectionCaps(this);
+
// Default lights
new AmbientLight(this, {
@@ -988,6 +991,10 @@ class Scene extends Component {
// Scene. Violates Hollywood Principle, where we could just filter on type in _addComponent,
// but this is faster than checking the type of each component in such a filter.
+ _capMaterialUpdated() {
+ this._sectionCaps._onCapMaterialUpdated();
+ }
+
_sectionPlaneCreated(sectionPlane) {
this.sectionPlanes[sectionPlane.id] = sectionPlane;
this.scene._sectionPlanesState.addSectionPlane(sectionPlane._state);
@@ -2796,6 +2803,9 @@ class Scene extends Component {
}
}
+ //destroy section caps separately because it's not a component
+ this._sectionCaps.destroy();
+
this.canvas.gl = null;
// Memory leak prevention
@@ -2818,6 +2828,7 @@ class Scene extends Component {
this._highlightedObjectIds = null;
this._selectedObjectIds = null;
this._colorizedObjectIds = null;
+ this._sectionCaps = null;
this.types = null;
this.components = null;
this.canvas = null;
diff --git a/src/viewer/scene/sectionCaps/SectionCaps.js b/src/viewer/scene/sectionCaps/SectionCaps.js
new file mode 100644
index 0000000000..26d7ed0bb5
--- /dev/null
+++ b/src/viewer/scene/sectionCaps/SectionCaps.js
@@ -0,0 +1,510 @@
+import { math } from "../math/math.js";
+import { Mesh } from "../mesh/Mesh.js";
+import { ReadableGeometry } from "../geometry/ReadableGeometry.js";
+import CSG from "../libs/csg.js";
+
+class SectionCaps {
+ /**
+ * @constructor
+ */
+ constructor(scene) {
+ this.scene = scene;
+ if (!this.scene.readableGeometryEnabled) {
+ console.log('SectionCapsPlugin only works when readable geometry is enable on the viewer.');
+ return;
+ }
+ this._sectionPlanes = [];
+ this._sceneModel = [];
+ this._prevIntersectionModels = {};
+ this._sectionPlaneTimeout = null;
+ this._capsDirty = false;
+ this._setupSectionPlanes();
+ this._setupTicks();
+ }
+
+
+ //this function will be used to setup event listeners for creation of section planes
+ _setupSectionPlanes() {
+ this._onSectionPlaneCreated = this.scene.on('sectionPlaneCreated', (sectionPlane) => {
+ this._sectionPlaneCreated(sectionPlane);
+ })
+ }
+
+ _setupTicks() {
+ this._onTick = this.scene.on("tick", () => {
+ if (this._capsDirty) {
+ this._capsDirty = false;
+ this._sceneModel = Object.keys(this.scene.models).map((key) => this.scene.models[key]);
+ this._addHatches(this._sceneModel, this._sectionPlanes);
+ }
+ })
+ }
+
+ //this hook will be called when a section plane is created
+ _sectionPlaneCreated(sectionPlane) {
+ this._sectionPlanes.push(sectionPlane);
+ this._update();
+ sectionPlane.on('pos', this._onSectionPlaneUpdated.bind(this));
+ sectionPlane.on('dir', this._onSectionPlaneUpdated.bind(this));
+ sectionPlane.once('destroyed', this._onSectionPlaneDestroyed.bind(this));
+ }
+
+ //this hook will be called when a section plane is destroyed
+ _onSectionPlaneDestroyed(sectionPlane) {
+ const sectionPlaneId = sectionPlane.id;
+ if (sectionPlaneId) {
+ this._sectionPlanes = this._sectionPlanes.filter((sectionPlane) => sectionPlane.id !== sectionPlaneId);
+ this._update();
+ }
+ }
+
+ //this function will be called when the position of a section plane is updated
+ _onSectionPlaneUpdated() {
+ this._deletePreviousModels();
+ if (this._sectionPlaneTimeout)
+ clearTimeout(this._sectionPlaneTimeout);
+ this._sectionPlaneTimeout = setTimeout(() => {
+ this._update();
+ clearTimeout(this._sectionPlaneTimeout);
+ this._sectionPlaneTimeout = null;
+ }, 1000);
+ }
+
+ _onCapMaterialUpdated() {
+ this._update();
+ }
+
+ _update() {
+ this._capsDirty = true;
+ }
+
+ _addHatches(sceneModel, plane) {
+
+ if (plane.length <= 0) return;
+
+ this._deletePreviousModels();
+
+ const { csgGeometries, materials } = this._convertWebglGeometriesToCsgGeometries(sceneModel);
+
+ if (Object.keys(materials).length <= 0) return;
+
+ const csgPlane = this._createCSGPlane(plane);
+ let caps = this._getCapGeometries(csgGeometries, csgPlane);
+ this._addIntersectedGeometries(caps, materials);
+ }
+
+ //#region main callers
+
+ _deletePreviousModels() {
+ const keys = Object.keys(this._prevIntersectionModels).length;
+ if (keys) {
+ for (const key in this._prevIntersectionModels) {
+ this._prevIntersectionModels[key].destroy();
+ }
+ }
+ }
+
+ _convertWebglGeometriesToCsgGeometries(sceneModels) {
+ let csgGeometries = {};
+ let materials = {};
+ sceneModels.forEach((sceneModel) => {
+ const objects = {};
+ for (const key in sceneModel.objects) {
+
+ const object = sceneModel.objects[key];
+ const isSolid = object.meshes[0].layer.solid !== false;
+ if (isSolid && object.capMaterial) {
+ objects[key] = sceneModel.objects[key];
+ materials[key] = sceneModel.objects[key].capMaterial;
+ }
+ }
+
+ let cloneModel = {
+ ...sceneModel,
+ objects: objects
+ }
+
+ const { vertices: webglVertices, indices: webglIndices } = this._getVerticesAndIndices(cloneModel);
+ const csgGeometry = this._createCSGGeometries(webglVertices, webglIndices);
+ csgGeometries = {
+ ...csgGeometries,
+ ...csgGeometry
+ }
+ })
+
+ return { csgGeometries, materials };
+ }
+
+ _getVerticesAndIndices(sceneModel) {
+ const vertices = {};
+ const indices = {};
+ const objects = sceneModel.objects;
+ for (const key in objects) {
+ const value = objects[key];
+ vertices[key] = [];
+ indices[key] = [];
+
+ if (value.meshes.length > 1) {
+ let index = 0;
+ value.meshes.forEach((mesh) => {
+ if (mesh.layer.solid) {
+ vertices[key].push([]);
+ indices[key].push([]);
+ mesh.getEachVertex((v) => {
+ vertices[key][index].push(v[0], v[1], v[2]);
+ })
+ mesh.getEachIndex((_indices) => {
+ indices[key][index].push(_indices);
+ })
+ index++;
+ }
+
+ })
+ }
+ else {
+ value.getEachVertex((_vertices) => {
+ vertices[key].push(_vertices[0], _vertices[1], _vertices[2]);
+ })
+ value.getEachIndex((_indices) => {
+ indices[key].push(_indices);
+ })
+ }
+ }
+ return { vertices, indices };
+ }
+
+ _createCSGGeometries(vertices, indices) {
+ const geometries = {};
+ for (const key in vertices) {
+ const vertex = vertices[key];
+ const index = indices[key];
+ if (!Array.isArray(vertex[0]))
+ geometries[key] = this._createGeometry(vertex, index);
+ else {
+ let geometry = null;
+ for (let i = 0; i < vertex.length; i++) {
+ if (vertex[i].length > 0) {
+ const g = this._createGeometry(vertex[i], index[i])
+ geometry = geometry ? geometry.union(g) : g;
+ }
+ }
+ geometries[key] = geometry;
+ }
+ }
+ return geometries;
+ }
+
+ _createCSGPlane(sectionPlanes) {
+ const vertices = [
+ 20, 20, 0.01,
+ -20, 20, 0.01,
+ -20, -20, 0.01,
+ 20, -20, 0.01,
+ 20, 20, 0.01,
+ 20, -20, 0.01,
+ 20, -20, -0.01,
+ 20, 20, -0.01,
+ 20, 20, 0.01,
+ 20, 20, -0.01,
+ -20, 20, -0.01,
+ -20, 20, 0.01,
+ -20, 20, 0.01,
+ -20, 20, -0.01,
+ -20, -20, -0.01,
+ -20, -20, 0.01,
+ -20, -20, -0.01,
+ 20, -20, -0.01,
+ 20, -20, 0.01,
+ -20, -20, 0.01,
+ 20, -20, -0.01,
+ -20, -20, -0.01,
+ -20, 20, -0.01,
+ 20, 20, -0.01
+ ];
+
+ const indices = [
+ 0, 1, 2, 0, 2, 3, // front
+ 4, 5, 6, 4, 6, 7, // right
+ 8, 9, 10, 8, 10, 11, // top
+ 12, 13, 14, 12, 14, 15, // left
+ 16, 17, 18, 16, 18, 19, // bottom
+ 20, 21, 22, 20, 22, 23
+ ]
+
+ let csgPlaneMerged = null;
+
+ sectionPlanes.forEach((sectionPlane) => {
+ const planeNormal = this._getObjectNormal(vertices, indices);
+ const normalizedDir = math.normalizeVec3(sectionPlane.dir);
+ const rotationQuaternion = this._computeQuaternionFromVectors(planeNormal, normalizedDir);
+
+ const planeTransformMatrix = this._getTransformationMatrix(sectionPlane.pos, rotationQuaternion, true);
+ const planeTransformedVertices = this._transformVertices(vertices, planeTransformMatrix);
+
+ const csgPlane = this._createGeometry(planeTransformedVertices, indices);
+
+ if (csgPlaneMerged)
+ csgPlaneMerged = csgPlaneMerged.union(csgPlane);
+ else
+ csgPlaneMerged = csgPlane;
+ })
+ return csgPlaneMerged;
+ }
+
+ _getCapGeometries(csgGeometries, csgPlane) {
+ const cappedGeometries = {};
+ for (const key in csgGeometries) {
+ const geometry = csgGeometries[key];
+ if (geometry.polygons.length) {
+ const intersected = geometry.intersect(csgPlane);
+ cappedGeometries[key] = intersected;
+ }
+
+ }
+ return cappedGeometries;
+ }
+
+ _addIntersectedGeometries(csgGometries, materials) {
+ for (const key in csgGometries) {
+ const webglGeometry = this._csgToWebGLGeometry(csgGometries[key]);
+ const model = this._addGeometryToScene(webglGeometry.vertices, webglGeometry.indices, key, materials[key]);
+ if (model)
+ this._prevIntersectionModels[key] = model;
+ }
+ }
+
+ //#endregion
+
+ //#region utility functions
+
+ _createGeometry(vertices, indices) {
+ let polygons = [];
+ for (let i = 0; i < indices.length; i += 3) {
+ let points = [];
+ for (let j = 0; j < 3; j++) {
+ let vertexIndex = indices[i + j];
+ points.push(new CSG.Vertex(
+ {
+ x: vertices[vertexIndex * 3],
+ y: vertices[vertexIndex * 3 + 1],
+ z: vertices[vertexIndex * 3 + 2]
+ }
+ ));
+ }
+ polygons.push(new CSG.Polygon(points));
+ }
+ return CSG.fromPolygons(polygons);
+ }
+
+ _getObjectNormal(vertices, indices) {
+ const v0 = this._getVertex(vertices, indices[0]);
+ const v1 = this._getVertex(vertices, indices[1]);
+ const v2 = this._getVertex(vertices, indices[2]);
+
+ const edge1 = math.subVec3(v1, v0);
+ const edge2 = math.subVec3(v2, v0);
+
+ const normal = math.cross3Vec3(edge1, edge2);
+
+ return math.normalizeVec3(normal)
+ }
+
+ _getVertex(positionArray, index) {
+ return [
+ positionArray[3 * index],
+ positionArray[3 * index + 1],
+ positionArray[3 * index + 2]
+ ];
+ }
+
+ _computeQuaternionFromVectors(v1, v2) {
+ const dot = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
+ const cross = {
+ x: v1[1] * v2[2] - v1[2] * v2[1],
+ y: v1[2] * v2[0] - v1[0] * v2[2],
+ z: v1[0] * v2[1] - v1[1] * v2[2]
+ };
+ const s = Math.sqrt((1 + dot) * 2);
+ const invs = 1 / s;
+
+ return [
+ cross.x * invs,
+ cross.y * invs,
+ cross.z * invs,
+ s * 0.5
+ ]
+ }
+
+ _getTransformationMatrix(position, rotation, _isQuat = false) {
+ const quat = _isQuat ? rotation : math.eulerToQuaternion(rotation, 'XYZ');
+ const mat4 = math.rotationTranslationMat4(quat, position);
+ return mat4;
+ }
+
+ _transformVertices(vertices, transformationMatrix) {
+ const transformedVertices = [];
+ for (let i = 0; i < vertices.length; i += 3) {
+ const vertex = [vertices[i], vertices[i + 1], vertices[i + 2], 1];
+ const transformedVertex = this._multiplyMatrixAndPoint(transformationMatrix, vertex);
+ transformedVertices.push(transformedVertex[0], transformedVertex[1], transformedVertex[2]);
+ }
+ return new Float32Array(transformedVertices);
+ }
+
+ _multiplyMatrixAndPoint(matrix, point) {
+ const result = [];
+ for (let i = 0; i < 4; i++) {
+ result[i] = matrix[i] * point[0] + matrix[i + 4] * point[1] + matrix[i + 8] * point[2] + matrix[i + 12] * point[3];
+ }
+ return result;
+ }
+
+ // Convert CSG to WebGL-compatible geometry
+ _csgToWebGLGeometry(csgGeometry) {
+ const vertices = [];
+ const indices = [];
+ csgGeometry.polygons.forEach(polygon => {
+ const vertexStartIndex = Math.floor(vertices.length / 3);
+ polygon.vertices.forEach(vertex => {
+ vertices.push(vertex.pos.x, vertex.pos.y, vertex.pos.z);
+ });
+ for (let i = 2; i < polygon.vertices.length; i++) {
+ indices.push(vertexStartIndex, vertexStartIndex + i - 1, vertexStartIndex + i);
+ }
+ });
+ return { vertices: new Float32Array(vertices), indices: new Uint16Array(indices) };
+ }
+
+ _addGeometryToScene(vertices, indices, id, material) {
+ if (vertices.length <= 0 && indices.length <= 0) return;
+ const meshNormals = math.buildNormals(vertices, indices);
+ const uvs = this._createUVsFromNormals(vertices, meshNormals);
+ const intersectedModel = new Mesh(this.scene, {
+ id,
+ geometry: new ReadableGeometry(this.scene, {
+ primitive: 'triangles',
+ positions: vertices,
+ indices: indices,
+ normals: meshNormals,
+ uv: uvs
+ }),
+ position: [0, 0, 0],
+ rotation: [0, 0, 0],
+ material,
+ })
+
+ return intersectedModel;
+ }
+
+ _createUVsFromNormals(vertices, normals) {
+ const avgMeshNormal = this._calculateAverageNormal(normals);
+ const projectionPlane = this._selectProjectionPlaneFromNormal(avgMeshNormal);
+ return this._generateUVsFromVertices(vertices, projectionPlane);
+ }
+
+ _calculateAverageNormal(normals) {
+ // Initialize the sum of normal components
+ let sumX = 0, sumY = 0, sumZ = 0;
+ const normalCount = normals.length / 3;
+
+ // Loop through the normals array
+ for (let i = 0; i < normals.length; i += 3) {
+ sumX += normals[i]; // x component of the normal
+ sumY += normals[i + 1]; // y component of the normal
+ sumZ += normals[i + 2]; // z component of the normal
+ }
+
+ // Calculate the average normal
+ const avgNormal = [
+ sumX / normalCount,
+ sumY / normalCount,
+ sumZ / normalCount
+ ];
+
+ // Normalize the average normal to unit length
+ const length = Math.sqrt(avgNormal[0] * avgNormal[0] + avgNormal[1] * avgNormal[1] + avgNormal[2] * avgNormal[2]);
+
+ // Return the normalized average normal
+ return [
+ avgNormal[0] / length,
+ avgNormal[1] / length,
+ avgNormal[2] / length
+ ];
+
+ }
+
+ _selectProjectionPlaneFromNormal(normal) {
+ const absNormal = [
+ Math.abs(normal[0]),
+ Math.abs(normal[1]),
+ Math.abs(normal[2])
+ ]
+
+ if (absNormal[0] >= absNormal[1] && absNormal[0] >= absNormal[2])
+ return 'yz';
+ else if (absNormal[1] >= absNormal[0] && absNormal[1] >= absNormal[2])
+ return 'xz';
+ else
+ return 'xy';
+ }
+
+ _generateUVsFromVertices(vertices, projectionPlane = 'xy') {
+ const uvs = [];
+ let min = { x: Infinity, y: Infinity };
+ let max = { x: -Infinity, y: -Infinity };
+
+ // Project and find min/max for UV normalization
+ for (let i = 0; i < vertices.length; i += 3) {
+ const x = vertices[i];
+ const y = vertices[i + 1];
+ const z = vertices[i + 2];
+
+ let u, v;
+
+ // Select projection plane (XY, XZ, or YZ)
+ if (projectionPlane === 'xy') {
+ u = x;
+ v = y;
+ } else if (projectionPlane === 'xz') {
+ u = x;
+ v = z;
+ } else if (projectionPlane === 'yz') {
+ u = y;
+ v = z;
+ }
+
+ // Update the min/max for normalization
+ min.x = Math.min(min.x, u);
+ min.y = Math.min(min.y, v);
+ max.x = Math.max(max.x, u);
+ max.y = Math.max(max.y, v);
+
+ // Store raw UVs before normalization
+ uvs.push(u, v);
+ }
+
+ // Calculate UV ranges
+ const range = {
+ x: max.x - min.x,
+ y: max.y - min.y
+ };
+
+ // Normalize UVs to fit within [0, 1]
+ for (let i = 0; i < uvs.length; i += 2) {
+ uvs[i] = (uvs[i] - min.x) / range.x;
+ uvs[i + 1] = (uvs[i + 1] - min.y) / range.y;
+ }
+
+ return uvs;
+ }
+
+ //#endregion
+
+ destroy() {
+ this._deletePreviousModels();
+ this.scene.off(this._onSectionPlaneCreated);
+ this.scene.off(this._onTick);
+ }
+}
+
+export { SectionCaps };
\ No newline at end of file
diff --git a/src/viewer/scene/sectionCaps/index.js b/src/viewer/scene/sectionCaps/index.js
new file mode 100644
index 0000000000..df30b200ff
--- /dev/null
+++ b/src/viewer/scene/sectionCaps/index.js
@@ -0,0 +1 @@
+export * from "./SectionCaps.js";
\ No newline at end of file