const {fnNumberSort} = require('./utils') const FuzzyCSGFactory = require('./FuzzyFactory3d') const Tree = require('./trees') const {EPS} = require('./constants') const {reTesselateCoplanarPolygons} = require('./math/polygonUtils') const Polygon = require('./math/Polygon3') const Plane = require('./math/Plane') const Vertex = require('./math/Vertex3') const Vector2D = require('./math/Vector2') const Vector3D = require('./math/Vector3') const Matrix4x4 = require('./math/Matrix4') const OrthoNormalBasis = require('./math/OrthoNormalBasis') const CAG = require('./CAG') // FIXME: circular dependency ! const Properties = require('./Properties') const {Connector} = require('./connectors') const fixTJunctions = require('./utils/fixTJunctions') // let {fromPolygons} = require('./CSGMakers') // FIXME: circular dependency ! /** Class CSG * Holds a binary space partition tree representing a 3D solid. Two solids can * be combined using the `union()`, `subtract()`, and `intersect()` methods. * @constructor */ let CSG = function () { this.polygons = [] this.properties = new Properties() this.isCanonicalized = true this.isRetesselated = true } CSG.prototype = { /** @return {Polygon[]} The list of polygons. */ toPolygons: function () { return this.polygons }, /** * Return a new CSG solid representing the space in either this solid or * in the given solids. Neither this solid nor the given solids are modified. * @param {CSG[]} csg - list of CSG objects * @returns {CSG} new CSG object * @example * let C = A.union(B) * @example * +-------+ +-------+ * | | | | * | A | | | * | +--+----+ = | +----+ * +----+--+ | +----+ | * | B | | | * | | | | * +-------+ +-------+ */ union: function (csg) { let csgs if (csg instanceof Array) { csgs = csg.slice(0) csgs.push(this) } else { csgs = [this, csg] } let i // combine csg pairs in a way that forms a balanced binary tree pattern for (i = 1; i < csgs.length; i += 2) { csgs.push(csgs[i - 1].unionSub(csgs[i])) } return csgs[i - 1].reTesselated().canonicalized() }, unionSub: function (csg, retesselate, canonicalize) { if (!this.mayOverlap(csg)) { return this.unionForNonIntersecting(csg) } else { let a = new Tree(this.polygons) let b = new Tree(csg.polygons) a.clipTo(b, false) // b.clipTo(a, true); // ERROR: this doesn't work b.clipTo(a) b.invert() b.clipTo(a) b.invert() let newpolygons = a.allPolygons().concat(b.allPolygons()) let result = CSG.fromPolygons(newpolygons) result.properties = this.properties._merge(csg.properties) if (retesselate) result = result.reTesselated() if (canonicalize) result = result.canonicalized() return result } }, // Like union, but when we know that the two solids are not intersecting // Do not use if you are not completely sure that the solids do not intersect! unionForNonIntersecting: function (csg) { let newpolygons = this.polygons.concat(csg.polygons) let result = CSG.fromPolygons(newpolygons) result.properties = this.properties._merge(csg.properties) result.isCanonicalized = this.isCanonicalized && csg.isCanonicalized result.isRetesselated = this.isRetesselated && csg.isRetesselated return result }, /** * Return a new CSG solid representing space in this solid but * not in the given solids. Neither this solid nor the given solids are modified. * @param {CSG[]} csg - list of CSG objects * @returns {CSG} new CSG object * @example * let C = A.subtract(B) * @example * +-------+ +-------+ * | | | | * | A | | | * | +--+----+ = | +--+ * +----+--+ | +----+ * | B | * | | * +-------+ */ subtract: function (csg) { let csgs if (csg instanceof Array) { csgs = csg } else { csgs = [csg] } let result = this for (let i = 0; i < csgs.length; i++) { let islast = (i === (csgs.length - 1)) result = result.subtractSub(csgs[i], islast, islast) } return result }, subtractSub: function (csg, retesselate, canonicalize) { let a = new Tree(this.polygons) let b = new Tree(csg.polygons) a.invert() a.clipTo(b) b.clipTo(a, true) a.addPolygons(b.allPolygons()) a.invert() let result = CSG.fromPolygons(a.allPolygons()) result.properties = this.properties._merge(csg.properties) if (retesselate) result = result.reTesselated() if (canonicalize) result = result.canonicalized() return result }, /** * Return a new CSG solid representing space in both this solid and * in the given solids. Neither this solid nor the given solids are modified. * @param {CSG[]} csg - list of CSG objects * @returns {CSG} new CSG object * @example * let C = A.intersect(B) * @example * +-------+ * | | * | A | * | +--+----+ = +--+ * +----+--+ | +--+ * | B | * | | * +-------+ */ intersect: function (csg) { let csgs if (csg instanceof Array) { csgs = csg } else { csgs = [csg] } let result = this for (let i = 0; i < csgs.length; i++) { let islast = (i === (csgs.length - 1)) result = result.intersectSub(csgs[i], islast, islast) } return result }, intersectSub: function (csg, retesselate, canonicalize) { let a = new Tree(this.polygons) let b = new Tree(csg.polygons) a.invert() b.clipTo(a) b.invert() a.clipTo(b) b.clipTo(a) a.addPolygons(b.allPolygons()) a.invert() let result = CSG.fromPolygons(a.allPolygons()) result.properties = this.properties._merge(csg.properties) if (retesselate) result = result.reTesselated() if (canonicalize) result = result.canonicalized() return result }, /** * Return a new CSG solid with solid and empty space switched. * This solid is not modified. * @returns {CSG} new CSG object * @example * let B = A.invert() */ invert: function () { let flippedpolygons = this.polygons.map(function (p) { return p.flipped() }) return CSG.fromPolygons(flippedpolygons) // TODO: flip properties? }, // Affine transformation of CSG object. Returns a new CSG object transform1: function (matrix4x4) { let newpolygons = this.polygons.map(function (p) { return p.transform(matrix4x4) }) let result = CSG.fromPolygons(newpolygons) result.properties = this.properties._transform(matrix4x4) result.isRetesselated = this.isRetesselated return result }, /** * Return a new CSG solid that is transformed using the given Matrix. * Several matrix transformations can be combined before transforming this solid. * @param {CSG.Matrix4x4} matrix4x4 - matrix to be applied * @returns {CSG} new CSG object * @example * var m = new CSG.Matrix4x4() * m = m.multiply(CSG.Matrix4x4.rotationX(40)) * m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0])) * let B = A.transform(m) */ transform: function (matrix4x4) { let ismirror = matrix4x4.isMirroring() let transformedvertices = {} let transformedplanes = {} let newpolygons = this.polygons.map(function (p) { let newplane let plane = p.plane let planetag = plane.getTag() if (planetag in transformedplanes) { newplane = transformedplanes[planetag] } else { newplane = plane.transform(matrix4x4) transformedplanes[planetag] = newplane } let newvertices = p.vertices.map(function (v) { let newvertex let vertextag = v.getTag() if (vertextag in transformedvertices) { newvertex = transformedvertices[vertextag] } else { newvertex = v.transform(matrix4x4) transformedvertices[vertextag] = newvertex } return newvertex }) if (ismirror) newvertices.reverse() return new Polygon(newvertices, p.shared, newplane) }) let result = CSG.fromPolygons(newpolygons) result.properties = this.properties._transform(matrix4x4) result.isRetesselated = this.isRetesselated result.isCanonicalized = this.isCanonicalized return result }, toString: function () { let result = 'CSG solid:\n' this.polygons.map(function (p) { result += p.toString() }) return result }, // Expand the solid // resolution: number of points per 360 degree for the rounded corners expand: function (radius, resolution) { let result = this.expandedShell(radius, resolution, true) result = result.reTesselated() result.properties = this.properties // keep original properties return result }, // Contract the solid // resolution: number of points per 360 degree for the rounded corners contract: function (radius, resolution) { let expandedshell = this.expandedShell(radius, resolution, false) let result = this.subtract(expandedshell) result = result.reTesselated() result.properties = this.properties // keep original properties return result }, // cut the solid at a plane, and stretch the cross-section found along plane normal stretchAtPlane: function (normal, point, length) { let plane = Plane.fromNormalAndPoint(normal, point) let onb = new OrthoNormalBasis(plane) let crosssect = this.sectionCut(onb) let midpiece = crosssect.extrudeInOrthonormalBasis(onb, length) let piece1 = this.cutByPlane(plane) let piece2 = this.cutByPlane(plane.flipped()) let result = piece1.union([midpiece, piece2.translate(plane.normal.times(length))]) return result }, // Create the expanded shell of the solid: // All faces are extruded to get a thickness of 2*radius // Cylinders are constructed around every side // Spheres are placed on every vertex // unionWithThis: if true, the resulting solid will be united with 'this' solid; // the result is a true expansion of the solid // If false, returns only the shell expandedShell: function (radius, resolution, unionWithThis) { // const {sphere} = require('./primitives3d') // FIXME: circular dependency ! let csg = this.reTesselated() let result if (unionWithThis) { result = csg } else { result = new CSG() } // first extrude all polygons: csg.polygons.map(function (polygon) { let extrudevector = polygon.plane.normal.unit().times(2 * radius) let translatedpolygon = polygon.translate(extrudevector.times(-0.5)) let extrudedface = translatedpolygon.extrude(extrudevector) result = result.unionSub(extrudedface, false, false) }) // Make a list of all unique vertex pairs (i.e. all sides of the solid) // For each vertex pair we collect the following: // v1: first coordinate // v2: second coordinate // planenormals: array of normal vectors of all planes touching this side let vertexpairs = {} // map of 'vertex pair tag' to {v1, v2, planenormals} csg.polygons.map(function (polygon) { let numvertices = polygon.vertices.length let prevvertex = polygon.vertices[numvertices - 1] let prevvertextag = prevvertex.getTag() for (let i = 0; i < numvertices; i++) { let vertex = polygon.vertices[i] let vertextag = vertex.getTag() let vertextagpair if (vertextag < prevvertextag) { vertextagpair = vertextag + '-' + prevvertextag } else { vertextagpair = prevvertextag + '-' + vertextag } let obj if (vertextagpair in vertexpairs) { obj = vertexpairs[vertextagpair] } else { obj = { v1: prevvertex, v2: vertex, planenormals: [] } vertexpairs[vertextagpair] = obj } obj.planenormals.push(polygon.plane.normal) prevvertextag = vertextag prevvertex = vertex } }) // now construct a cylinder on every side // The cylinder is always an approximation of a true cylinder: it will have polygons // around the sides. We will make sure though that the cylinder will have an edge at every // face that touches this side. This ensures that we will get a smooth fill even // if two edges are at, say, 10 degrees and the resolution is low. // Note: the result is not retesselated yet but it really should be! for (let vertextagpair in vertexpairs) { let vertexpair = vertexpairs[vertextagpair] let startpoint = vertexpair.v1.pos let endpoint = vertexpair.v2.pos // our x,y and z vectors: let zbase = endpoint.minus(startpoint).unit() let xbase = vertexpair.planenormals[0].unit() let ybase = xbase.cross(zbase) // make a list of angles that the cylinder should traverse: let angles = [] // first of all equally spaced around the cylinder: for (let i = 0; i < resolution; i++) { angles.push(i * Math.PI * 2 / resolution) } // and also at every normal of all touching planes: for (let i = 0, iMax = vertexpair.planenormals.length; i < iMax; i++) { let planenormal = vertexpair.planenormals[i] let si = ybase.dot(planenormal) let co = xbase.dot(planenormal) let angle = Math.atan2(si, co) if (angle < 0) angle += Math.PI * 2 angles.push(angle) angle = Math.atan2(-si, -co) if (angle < 0) angle += Math.PI * 2 angles.push(angle) } // this will result in some duplicate angles but we will get rid of those later. // Sort: angles = angles.sort(fnNumberSort) // Now construct the cylinder by traversing all angles: let numangles = angles.length let prevp1 let prevp2 let startfacevertices = [] let endfacevertices = [] let polygons = [] for (let i = -1; i < numangles; i++) { let angle = angles[(i < 0) ? (i + numangles) : i] let si = Math.sin(angle) let co = Math.cos(angle) let p = xbase.times(co * radius).plus(ybase.times(si * radius)) let p1 = startpoint.plus(p) let p2 = endpoint.plus(p) let skip = false if (i >= 0) { if (p1.distanceTo(prevp1) < EPS) { skip = true } } if (!skip) { if (i >= 0) { startfacevertices.push(new Vertex(p1)) endfacevertices.push(new Vertex(p2)) let polygonvertices = [ new Vertex(prevp2), new Vertex(p2), new Vertex(p1), new Vertex(prevp1) ] let polygon = new Polygon(polygonvertices) polygons.push(polygon) } prevp1 = p1 prevp2 = p2 } } endfacevertices.reverse() polygons.push(new Polygon(startfacevertices)) polygons.push(new Polygon(endfacevertices)) let cylinder = CSG.fromPolygons(polygons) result = result.unionSub(cylinder, false, false) } // make a list of all unique vertices // For each vertex we also collect the list of normals of the planes touching the vertices let vertexmap = {} csg.polygons.map(function (polygon) { polygon.vertices.map(function (vertex) { let vertextag = vertex.getTag() let obj if (vertextag in vertexmap) { obj = vertexmap[vertextag] } else { obj = { pos: vertex.pos, normals: [] } vertexmap[vertextag] = obj } obj.normals.push(polygon.plane.normal) }) }) // and build spheres at each vertex // We will try to set the x and z axis to the normals of 2 planes // This will ensure that our sphere tesselation somewhat matches 2 planes for (let vertextag in vertexmap) { let vertexobj = vertexmap[vertextag] // use the first normal to be the x axis of our sphere: let xaxis = vertexobj.normals[0].unit() // and find a suitable z axis. We will use the normal which is most perpendicular to the x axis: let bestzaxis = null let bestzaxisorthogonality = 0 for (let i = 1; i < vertexobj.normals.length; i++) { let normal = vertexobj.normals[i].unit() let cross = xaxis.cross(normal) let crosslength = cross.length() if (crosslength > 0.05) { if (crosslength > bestzaxisorthogonality) { bestzaxisorthogonality = crosslength bestzaxis = normal } } } if (!bestzaxis) { bestzaxis = xaxis.randomNonParallelVector() } let yaxis = xaxis.cross(bestzaxis).unit() let zaxis = yaxis.cross(xaxis) let _sphere = CSG.sphere({ center: vertexobj.pos, radius: radius, resolution: resolution, axes: [xaxis, yaxis, zaxis] }) result = result.unionSub(_sphere, false, false) } return result }, canonicalized: function () { if (this.isCanonicalized) { return this } else { let factory = new FuzzyCSGFactory() let result = CSGFromCSGFuzzyFactory(factory, this) result.isCanonicalized = true result.isRetesselated = this.isRetesselated result.properties = this.properties // keep original properties return result } }, reTesselated: function () { if (this.isRetesselated) { return this } else { let csg = this let polygonsPerPlane = {} let isCanonicalized = csg.isCanonicalized let fuzzyfactory = new FuzzyCSGFactory() csg.polygons.map(function (polygon) { let plane = polygon.plane let shared = polygon.shared if (!isCanonicalized) { // in order to identify to polygons having the same plane, we need to canonicalize the planes // We don't have to do a full canonizalization (including vertices), to save time only do the planes and the shared data: plane = fuzzyfactory.getPlane(plane) shared = fuzzyfactory.getPolygonShared(shared) } let tag = plane.getTag() + '/' + shared.getTag() if (!(tag in polygonsPerPlane)) { polygonsPerPlane[tag] = [polygon] } else { polygonsPerPlane[tag].push(polygon) } }) let destpolygons = [] for (let planetag in polygonsPerPlane) { let sourcepolygons = polygonsPerPlane[planetag] if (sourcepolygons.length < 2) { destpolygons = destpolygons.concat(sourcepolygons) } else { let retesselayedpolygons = [] reTesselateCoplanarPolygons(sourcepolygons, retesselayedpolygons) destpolygons = destpolygons.concat(retesselayedpolygons) } } let result = CSG.fromPolygons(destpolygons) result.isRetesselated = true // result = result.canonicalized(); result.properties = this.properties // keep original properties return result } }, /** * Returns an array of Vector3D, providing minimum coordinates and maximum coordinates * of this solid. * @returns {Vector3D[]} * @example * let bounds = A.getBounds() * let minX = bounds[0].x */ getBounds: function () { if (!this.cachedBoundingBox) { let minpoint = new Vector3D(0, 0, 0) let maxpoint = new Vector3D(0, 0, 0) let polygons = this.polygons let numpolygons = polygons.length for (let i = 0; i < numpolygons; i++) { let polygon = polygons[i] let bounds = polygon.boundingBox() if (i === 0) { minpoint = bounds[0] maxpoint = bounds[1] } else { minpoint = minpoint.min(bounds[0]) maxpoint = maxpoint.max(bounds[1]) } } this.cachedBoundingBox = [minpoint, maxpoint] } return this.cachedBoundingBox }, // returns true if there is a possibility that the two solids overlap // returns false if we can be sure that they do not overlap mayOverlap: function (csg) { if ((this.polygons.length === 0) || (csg.polygons.length === 0)) { return false } else { let mybounds = this.getBounds() let otherbounds = csg.getBounds() if (mybounds[1].x < otherbounds[0].x) return false if (mybounds[0].x > otherbounds[1].x) return false if (mybounds[1].y < otherbounds[0].y) return false if (mybounds[0].y > otherbounds[1].y) return false if (mybounds[1].z < otherbounds[0].z) return false if (mybounds[0].z > otherbounds[1].z) return false return true } }, // Cut the solid by a plane. Returns the solid on the back side of the plane cutByPlane: function (plane) { if (this.polygons.length === 0) { return new CSG() } // Ideally we would like to do an intersection with a polygon of inifinite size // but this is not supported by our implementation. As a workaround, we will create // a cube, with one face on the plane, and a size larger enough so that the entire // solid fits in the cube. // find the max distance of any vertex to the center of the plane: let planecenter = plane.normal.times(plane.w) let maxdistance = 0 this.polygons.map(function (polygon) { polygon.vertices.map(function (vertex) { let distance = vertex.pos.distanceToSquared(planecenter) if (distance > maxdistance) maxdistance = distance }) }) maxdistance = Math.sqrt(maxdistance) maxdistance *= 1.01 // make sure it's really larger // Now build a polygon on the plane, at any point farther than maxdistance from the plane center: let vertices = [] let orthobasis = new OrthoNormalBasis(plane) vertices.push(new Vertex(orthobasis.to3D(new Vector2D(maxdistance, -maxdistance)))) vertices.push(new Vertex(orthobasis.to3D(new Vector2D(-maxdistance, -maxdistance)))) vertices.push(new Vertex(orthobasis.to3D(new Vector2D(-maxdistance, maxdistance)))) vertices.push(new Vertex(orthobasis.to3D(new Vector2D(maxdistance, maxdistance)))) let polygon = new Polygon(vertices, null, plane.flipped()) // and extrude the polygon into a cube, backwards of the plane: let cube = polygon.extrude(plane.normal.times(-maxdistance)) // Now we can do the intersection: let result = this.intersect(cube) result.properties = this.properties // keep original properties return result }, // Connect a solid to another solid, such that two Connectors become connected // myConnector: a Connector of this solid // otherConnector: a Connector to which myConnector should be connected // mirror: false: the 'axis' vectors of the connectors should point in the same direction // true: the 'axis' vectors of the connectors should point in opposite direction // normalrotation: degrees of rotation between the 'normal' vectors of the two // connectors connectTo: function (myConnector, otherConnector, mirror, normalrotation) { let matrix = myConnector.getTransformationTo(otherConnector, mirror, normalrotation) return this.transform(matrix) }, // set the .shared property of all polygons // Returns a new CSG solid, the original is unmodified! setShared: function (shared) { let polygons = this.polygons.map(function (p) { return new Polygon(p.vertices, shared, p.plane) }) let result = CSG.fromPolygons(polygons) result.properties = this.properties // keep original properties result.isRetesselated = this.isRetesselated result.isCanonicalized = this.isCanonicalized return result }, setColor: function (args) { let newshared = Polygon.Shared.fromColor.apply(this, arguments) return this.setShared(newshared) }, toCompactBinary: function () { let csg = this.canonicalized(), numpolygons = csg.polygons.length, numpolygonvertices = 0, numvertices = 0, vertexmap = {}, vertices = [], numplanes = 0, planemap = {}, polygonindex = 0, planes = [], shareds = [], sharedmap = {}, numshared = 0 // for (let i = 0, iMax = csg.polygons.length; i < iMax; i++) { // let p = csg.polygons[i]; // for (let j = 0, jMax = p.length; j < jMax; j++) { // ++numpolygonvertices; // let vertextag = p[j].getTag(); // if(!(vertextag in vertexmap)) { // vertexmap[vertextag] = numvertices++; // vertices.push(p[j]); // } // } csg.polygons.map(function (p) { p.vertices.map(function (v) { ++numpolygonvertices let vertextag = v.getTag() if (!(vertextag in vertexmap)) { vertexmap[vertextag] = numvertices++ vertices.push(v) } }) let planetag = p.plane.getTag() if (!(planetag in planemap)) { planemap[planetag] = numplanes++ planes.push(p.plane) } let sharedtag = p.shared.getTag() if (!(sharedtag in sharedmap)) { sharedmap[sharedtag] = numshared++ shareds.push(p.shared) } }) let numVerticesPerPolygon = new Uint32Array(numpolygons) let polygonSharedIndexes = new Uint32Array(numpolygons) let polygonVertices = new Uint32Array(numpolygonvertices) let polygonPlaneIndexes = new Uint32Array(numpolygons) let vertexData = new Float64Array(numvertices * 3) let planeData = new Float64Array(numplanes * 4) let polygonVerticesIndex = 0 for (let polygonindex = 0; polygonindex < numpolygons; ++polygonindex) { let p = csg.polygons[polygonindex] numVerticesPerPolygon[polygonindex] = p.vertices.length p.vertices.map(function (v) { let vertextag = v.getTag() let vertexindex = vertexmap[vertextag] polygonVertices[polygonVerticesIndex++] = vertexindex }) let planetag = p.plane.getTag() let planeindex = planemap[planetag] polygonPlaneIndexes[polygonindex] = planeindex let sharedtag = p.shared.getTag() let sharedindex = sharedmap[sharedtag] polygonSharedIndexes[polygonindex] = sharedindex } let verticesArrayIndex = 0 vertices.map(function (v) { let pos = v.pos vertexData[verticesArrayIndex++] = pos._x vertexData[verticesArrayIndex++] = pos._y vertexData[verticesArrayIndex++] = pos._z }) let planesArrayIndex = 0 planes.map(function (p) { let normal = p.normal planeData[planesArrayIndex++] = normal._x planeData[planesArrayIndex++] = normal._y planeData[planesArrayIndex++] = normal._z planeData[planesArrayIndex++] = p.w }) let result = { 'class': 'CSG', numPolygons: numpolygons, numVerticesPerPolygon: numVerticesPerPolygon, polygonPlaneIndexes: polygonPlaneIndexes, polygonSharedIndexes: polygonSharedIndexes, polygonVertices: polygonVertices, vertexData: vertexData, planeData: planeData, shared: shareds } return result }, // Get the transformation that transforms this CSG such that it is lying on the z=0 plane, // as flat as possible (i.e. the least z-height). // So that it is in an orientation suitable for CNC milling getTransformationAndInverseTransformationToFlatLying: function () { if (this.polygons.length === 0) { let m = new Matrix4x4() // unity return [m, m] } else { // get a list of unique planes in the CSG: let csg = this.canonicalized() let planemap = {} csg.polygons.map(function (polygon) { planemap[polygon.plane.getTag()] = polygon.plane }) // try each plane in the CSG and find the plane that, when we align it flat onto z=0, // gives the least height in z-direction. // If two planes give the same height, pick the plane that originally had a normal closest // to [0,0,-1]. let xvector = new Vector3D(1, 0, 0) let yvector = new Vector3D(0, 1, 0) let zvector = new Vector3D(0, 0, 1) let z0connectorx = new Connector([0, 0, 0], [0, 0, -1], xvector) let z0connectory = new Connector([0, 0, 0], [0, 0, -1], yvector) let isfirst = true let minheight = 0 let maxdotz = 0 let besttransformation, bestinversetransformation for (let planetag in planemap) { let plane = planemap[planetag] let pointonplane = plane.normal.times(plane.w) let transformation, inversetransformation // We need a normal vecrtor for the transformation // determine which is more perpendicular to the plane normal: x or y? // we will align this as much as possible to the x or y axis vector let xorthogonality = plane.normal.cross(xvector).length() let yorthogonality = plane.normal.cross(yvector).length() if (xorthogonality > yorthogonality) { // x is better: let planeconnector = new Connector(pointonplane, plane.normal, xvector) transformation = planeconnector.getTransformationTo(z0connectorx, false, 0) inversetransformation = z0connectorx.getTransformationTo(planeconnector, false, 0) } else { // y is better: let planeconnector = new Connector(pointonplane, plane.normal, yvector) transformation = planeconnector.getTransformationTo(z0connectory, false, 0) inversetransformation = z0connectory.getTransformationTo(planeconnector, false, 0) } let transformedcsg = csg.transform(transformation) let dotz = -plane.normal.dot(zvector) let bounds = transformedcsg.getBounds() let zheight = bounds[1].z - bounds[0].z let isbetter = isfirst if (!isbetter) { if (zheight < minheight) { isbetter = true } else if (zheight === minheight) { if (dotz > maxdotz) isbetter = true } } if (isbetter) { // translate the transformation around the z-axis and onto the z plane: let translation = new Vector3D([-0.5 * (bounds[1].x + bounds[0].x), -0.5 * (bounds[1].y + bounds[0].y), -bounds[0].z]) transformation = transformation.multiply(Matrix4x4.translation(translation)) inversetransformation = Matrix4x4.translation(translation.negated()).multiply(inversetransformation) minheight = zheight maxdotz = dotz besttransformation = transformation bestinversetransformation = inversetransformation } isfirst = false } return [besttransformation, bestinversetransformation] } }, getTransformationToFlatLying: function () { let result = this.getTransformationAndInverseTransformationToFlatLying() return result[0] }, lieFlat: function () { let transformation = this.getTransformationToFlatLying() return this.transform(transformation) }, // project the 3D CSG onto a plane // This returns a 2D CAG with the 'shadow' shape of the 3D solid when projected onto the // plane represented by the orthonormal basis projectToOrthoNormalBasis: function (orthobasis) { let cags = [] this.polygons.filter(function (p) { // only return polys in plane, others may disturb result return p.plane.normal.minus(orthobasis.plane.normal).lengthSquared() < (EPS * EPS) }) .map(function (polygon) { let cag = polygon.projectToOrthoNormalBasis(orthobasis) if (cag.sides.length > 0) { cags.push(cag) } }) let result = new CAG().union(cags) return result }, sectionCut: function (orthobasis) { let plane1 = orthobasis.plane let plane2 = orthobasis.plane.flipped() plane1 = new Plane(plane1.normal, plane1.w) plane2 = new Plane(plane2.normal, plane2.w + (5 * EPS)) let cut3d = this.cutByPlane(plane1) cut3d = cut3d.cutByPlane(plane2) return cut3d.projectToOrthoNormalBasis(orthobasis) }, fixTJunctions: function () { return fixTJunctions(CSG.fromPolygons, this) }, toTriangles: function () { let polygons = [] this.polygons.forEach(function (poly) { let firstVertex = poly.vertices[0] for (let i = poly.vertices.length - 3; i >= 0; i--) { polygons.push(new Polygon([ firstVertex, poly.vertices[i + 1], poly.vertices[i + 2] ], poly.shared, poly.plane)) } }) return polygons }, /** * Returns an array of values for the requested features of this solid. * Supported Features: 'volume', 'area' * @param {String[]} features - list of features to calculate * @returns {Float[]} values * @example * let volume = A.getFeatures('volume') * let values = A.getFeatures('area','volume') */ getFeatures: function (features) { if (!(features instanceof Array)) { features = [features] } let result = this.toTriangles().map(function (triPoly) { return triPoly.getTetraFeatures(features) }) .reduce(function (pv, v) { return v.map(function (feat, i) { return feat + (pv === 0 ? 0 : pv[i]) }) }, 0) return (result.length === 1) ? result[0] : result } } /** Construct a CSG solid from a list of `Polygon` instances. * @param {Polygon[]} polygons - list of polygons * @returns {CSG} new CSG object */ CSG.fromPolygons = function fromPolygons (polygons) { let csg = new CSG() csg.polygons = polygons csg.isCanonicalized = false csg.isRetesselated = false return csg } const CSGFromCSGFuzzyFactory = function (factory, sourcecsg) { let _this = factory let newpolygons = [] sourcecsg.polygons.forEach(function (polygon) { let newpolygon = _this.getPolygon(polygon) // see getPolygon above: we may get a polygon with no vertices, discard it: if (newpolygon.vertices.length >= 3) { newpolygons.push(newpolygon) } }) return CSG.fromPolygons(newpolygons) } module.exports = CSG