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 + + + + + + + +
+ +

SceneModel

+

Non-realistic rendering, geometry reuse, triangle primitives

+

+ SceneModel is a WebGL2-based SceneModel implementation that stores model geometry as data textures on the GPU. +

+

Components Used

+ +
+ + + + + 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 + + + + + + + + +
+ +

SectionPlanesPlugin

+

Slices models open to reveal internal structures

+

In this example, we're loading an IFC2x3 BIM model from the file system, then slicing it with a section + plane.

+

Stats

+ +

Components used

+ +

Resources

+ +
+ + + + + \ 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