UNPKG

34.7 kBJavaScriptView Raw
1const {fnNumberSort} = require('./utils')
2const FuzzyCSGFactory = require('./FuzzyFactory3d')
3const Tree = require('./trees')
4const {EPS} = require('./constants')
5const {reTesselateCoplanarPolygons} = require('./math/polygonUtils')
6const Polygon = require('./math/Polygon3')
7const Plane = require('./math/Plane')
8const Vertex = require('./math/Vertex3')
9const Vector2D = require('./math/Vector2')
10const Vector3D = require('./math/Vector3')
11const Matrix4x4 = require('./math/Matrix4')
12const OrthoNormalBasis = require('./math/OrthoNormalBasis')
13
14const CAG = require('./CAG') // FIXME: circular dependency !
15
16const Properties = require('./Properties')
17const {Connector} = require('./connectors')
18const fixTJunctions = require('./utils/fixTJunctions')
19// let {fromPolygons} = require('./CSGMakers') // FIXME: circular dependency !
20
21/** Class CSG
22 * Holds a binary space partition tree representing a 3D solid. Two solids can
23 * be combined using the `union()`, `subtract()`, and `intersect()` methods.
24 * @constructor
25 */
26let CSG = function () {
27 this.polygons = []
28 this.properties = new Properties()
29 this.isCanonicalized = true
30 this.isRetesselated = true
31}
32
33CSG.prototype = {
34 /** @return {Polygon[]} The list of polygons. */
35 toPolygons: function () {
36 return this.polygons
37 },
38
39 /**
40 * Return a new CSG solid representing the space in either this solid or
41 * in the given solids. Neither this solid nor the given solids are modified.
42 * @param {CSG[]} csg - list of CSG objects
43 * @returns {CSG} new CSG object
44 * @example
45 * let C = A.union(B)
46 * @example
47 * +-------+ +-------+
48 * | | | |
49 * | A | | |
50 * | +--+----+ = | +----+
51 * +----+--+ | +----+ |
52 * | B | | |
53 * | | | |
54 * +-------+ +-------+
55 */
56 union: function (csg) {
57 let csgs
58 if (csg instanceof Array) {
59 csgs = csg.slice(0)
60 csgs.push(this)
61 } else {
62 csgs = [this, csg]
63 }
64
65 let i
66 // combine csg pairs in a way that forms a balanced binary tree pattern
67 for (i = 1; i < csgs.length; i += 2) {
68 csgs.push(csgs[i - 1].unionSub(csgs[i]))
69 }
70 return csgs[i - 1].reTesselated().canonicalized()
71 },
72
73 unionSub: function (csg, retesselate, canonicalize) {
74 if (!this.mayOverlap(csg)) {
75 return this.unionForNonIntersecting(csg)
76 } else {
77 let a = new Tree(this.polygons)
78 let b = new Tree(csg.polygons)
79 a.clipTo(b, false)
80
81 // b.clipTo(a, true); // ERROR: this doesn't work
82 b.clipTo(a)
83 b.invert()
84 b.clipTo(a)
85 b.invert()
86
87 let newpolygons = a.allPolygons().concat(b.allPolygons())
88 let result = CSG.fromPolygons(newpolygons)
89 result.properties = this.properties._merge(csg.properties)
90 if (retesselate) result = result.reTesselated()
91 if (canonicalize) result = result.canonicalized()
92 return result
93 }
94 },
95
96 // Like union, but when we know that the two solids are not intersecting
97 // Do not use if you are not completely sure that the solids do not intersect!
98 unionForNonIntersecting: function (csg) {
99 let newpolygons = this.polygons.concat(csg.polygons)
100 let result = CSG.fromPolygons(newpolygons)
101 result.properties = this.properties._merge(csg.properties)
102 result.isCanonicalized = this.isCanonicalized && csg.isCanonicalized
103 result.isRetesselated = this.isRetesselated && csg.isRetesselated
104 return result
105 },
106
107 /**
108 * Return a new CSG solid representing space in this solid but
109 * not in the given solids. Neither this solid nor the given solids are modified.
110 * @param {CSG[]} csg - list of CSG objects
111 * @returns {CSG} new CSG object
112 * @example
113 * let C = A.subtract(B)
114 * @example
115 * +-------+ +-------+
116 * | | | |
117 * | A | | |
118 * | +--+----+ = | +--+
119 * +----+--+ | +----+
120 * | B |
121 * | |
122 * +-------+
123 */
124 subtract: function (csg) {
125 let csgs
126 if (csg instanceof Array) {
127 csgs = csg
128 } else {
129 csgs = [csg]
130 }
131 let result = this
132 for (let i = 0; i < csgs.length; i++) {
133 let islast = (i === (csgs.length - 1))
134 result = result.subtractSub(csgs[i], islast, islast)
135 }
136 return result
137 },
138
139 subtractSub: function (csg, retesselate, canonicalize) {
140 let a = new Tree(this.polygons)
141 let b = new Tree(csg.polygons)
142 a.invert()
143 a.clipTo(b)
144 b.clipTo(a, true)
145 a.addPolygons(b.allPolygons())
146 a.invert()
147 let result = CSG.fromPolygons(a.allPolygons())
148 result.properties = this.properties._merge(csg.properties)
149 if (retesselate) result = result.reTesselated()
150 if (canonicalize) result = result.canonicalized()
151 return result
152 },
153
154 /**
155 * Return a new CSG solid representing space in both this solid and
156 * in the given solids. Neither this solid nor the given solids are modified.
157 * @param {CSG[]} csg - list of CSG objects
158 * @returns {CSG} new CSG object
159 * @example
160 * let C = A.intersect(B)
161 * @example
162 * +-------+
163 * | |
164 * | A |
165 * | +--+----+ = +--+
166 * +----+--+ | +--+
167 * | B |
168 * | |
169 * +-------+
170 */
171 intersect: function (csg) {
172 let csgs
173 if (csg instanceof Array) {
174 csgs = csg
175 } else {
176 csgs = [csg]
177 }
178 let result = this
179 for (let i = 0; i < csgs.length; i++) {
180 let islast = (i === (csgs.length - 1))
181 result = result.intersectSub(csgs[i], islast, islast)
182 }
183 return result
184 },
185
186 intersectSub: function (csg, retesselate, canonicalize) {
187 let a = new Tree(this.polygons)
188 let b = new Tree(csg.polygons)
189 a.invert()
190 b.clipTo(a)
191 b.invert()
192 a.clipTo(b)
193 b.clipTo(a)
194 a.addPolygons(b.allPolygons())
195 a.invert()
196 let result = CSG.fromPolygons(a.allPolygons())
197 result.properties = this.properties._merge(csg.properties)
198 if (retesselate) result = result.reTesselated()
199 if (canonicalize) result = result.canonicalized()
200 return result
201 },
202
203 /**
204 * Return a new CSG solid with solid and empty space switched.
205 * This solid is not modified.
206 * @returns {CSG} new CSG object
207 * @example
208 * let B = A.invert()
209 */
210 invert: function () {
211 let flippedpolygons = this.polygons.map(function (p) {
212 return p.flipped()
213 })
214 return CSG.fromPolygons(flippedpolygons)
215 // TODO: flip properties?
216 },
217
218 // Affine transformation of CSG object. Returns a new CSG object
219 transform1: function (matrix4x4) {
220 let newpolygons = this.polygons.map(function (p) {
221 return p.transform(matrix4x4)
222 })
223 let result = CSG.fromPolygons(newpolygons)
224 result.properties = this.properties._transform(matrix4x4)
225 result.isRetesselated = this.isRetesselated
226 return result
227 },
228
229 /**
230 * Return a new CSG solid that is transformed using the given Matrix.
231 * Several matrix transformations can be combined before transforming this solid.
232 * @param {CSG.Matrix4x4} matrix4x4 - matrix to be applied
233 * @returns {CSG} new CSG object
234 * @example
235 * var m = new CSG.Matrix4x4()
236 * m = m.multiply(CSG.Matrix4x4.rotationX(40))
237 * m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0]))
238 * let B = A.transform(m)
239 */
240 transform: function (matrix4x4) {
241 let ismirror = matrix4x4.isMirroring()
242 let transformedvertices = {}
243 let transformedplanes = {}
244 let newpolygons = this.polygons.map(function (p) {
245 let newplane
246 let plane = p.plane
247 let planetag = plane.getTag()
248 if (planetag in transformedplanes) {
249 newplane = transformedplanes[planetag]
250 } else {
251 newplane = plane.transform(matrix4x4)
252 transformedplanes[planetag] = newplane
253 }
254 let newvertices = p.vertices.map(function (v) {
255 let newvertex
256 let vertextag = v.getTag()
257 if (vertextag in transformedvertices) {
258 newvertex = transformedvertices[vertextag]
259 } else {
260 newvertex = v.transform(matrix4x4)
261 transformedvertices[vertextag] = newvertex
262 }
263 return newvertex
264 })
265 if (ismirror) newvertices.reverse()
266 return new Polygon(newvertices, p.shared, newplane)
267 })
268 let result = CSG.fromPolygons(newpolygons)
269 result.properties = this.properties._transform(matrix4x4)
270 result.isRetesselated = this.isRetesselated
271 result.isCanonicalized = this.isCanonicalized
272 return result
273 },
274
275 toString: function () {
276 let result = 'CSG solid:\n'
277 this.polygons.map(function (p) {
278 result += p.toString()
279 })
280 return result
281 },
282
283 // Expand the solid
284 // resolution: number of points per 360 degree for the rounded corners
285 expand: function (radius, resolution) {
286 let result = this.expandedShell(radius, resolution, true)
287 result = result.reTesselated()
288 result.properties = this.properties // keep original properties
289 return result
290 },
291
292 // Contract the solid
293 // resolution: number of points per 360 degree for the rounded corners
294 contract: function (radius, resolution) {
295 let expandedshell = this.expandedShell(radius, resolution, false)
296 let result = this.subtract(expandedshell)
297 result = result.reTesselated()
298 result.properties = this.properties // keep original properties
299 return result
300 },
301
302 // cut the solid at a plane, and stretch the cross-section found along plane normal
303 stretchAtPlane: function (normal, point, length) {
304 let plane = Plane.fromNormalAndPoint(normal, point)
305 let onb = new OrthoNormalBasis(plane)
306 let crosssect = this.sectionCut(onb)
307 let midpiece = crosssect.extrudeInOrthonormalBasis(onb, length)
308 let piece1 = this.cutByPlane(plane)
309 let piece2 = this.cutByPlane(plane.flipped())
310 let result = piece1.union([midpiece, piece2.translate(plane.normal.times(length))])
311 return result
312 },
313
314 // Create the expanded shell of the solid:
315 // All faces are extruded to get a thickness of 2*radius
316 // Cylinders are constructed around every side
317 // Spheres are placed on every vertex
318 // unionWithThis: if true, the resulting solid will be united with 'this' solid;
319 // the result is a true expansion of the solid
320 // If false, returns only the shell
321 expandedShell: function (radius, resolution, unionWithThis) {
322 // const {sphere} = require('./primitives3d') // FIXME: circular dependency !
323 let csg = this.reTesselated()
324 let result
325 if (unionWithThis) {
326 result = csg
327 } else {
328 result = new CSG()
329 }
330
331 // first extrude all polygons:
332 csg.polygons.map(function (polygon) {
333 let extrudevector = polygon.plane.normal.unit().times(2 * radius)
334 let translatedpolygon = polygon.translate(extrudevector.times(-0.5))
335 let extrudedface = translatedpolygon.extrude(extrudevector)
336 result = result.unionSub(extrudedface, false, false)
337 })
338
339 // Make a list of all unique vertex pairs (i.e. all sides of the solid)
340 // For each vertex pair we collect the following:
341 // v1: first coordinate
342 // v2: second coordinate
343 // planenormals: array of normal vectors of all planes touching this side
344 let vertexpairs = {} // map of 'vertex pair tag' to {v1, v2, planenormals}
345 csg.polygons.map(function (polygon) {
346 let numvertices = polygon.vertices.length
347 let prevvertex = polygon.vertices[numvertices - 1]
348 let prevvertextag = prevvertex.getTag()
349 for (let i = 0; i < numvertices; i++) {
350 let vertex = polygon.vertices[i]
351 let vertextag = vertex.getTag()
352 let vertextagpair
353 if (vertextag < prevvertextag) {
354 vertextagpair = vertextag + '-' + prevvertextag
355 } else {
356 vertextagpair = prevvertextag + '-' + vertextag
357 }
358 let obj
359 if (vertextagpair in vertexpairs) {
360 obj = vertexpairs[vertextagpair]
361 } else {
362 obj = {
363 v1: prevvertex,
364 v2: vertex,
365 planenormals: []
366 }
367 vertexpairs[vertextagpair] = obj
368 }
369 obj.planenormals.push(polygon.plane.normal)
370
371 prevvertextag = vertextag
372 prevvertex = vertex
373 }
374 })
375
376 // now construct a cylinder on every side
377 // The cylinder is always an approximation of a true cylinder: it will have <resolution> polygons
378 // around the sides. We will make sure though that the cylinder will have an edge at every
379 // face that touches this side. This ensures that we will get a smooth fill even
380 // if two edges are at, say, 10 degrees and the resolution is low.
381 // Note: the result is not retesselated yet but it really should be!
382 for (let vertextagpair in vertexpairs) {
383 let vertexpair = vertexpairs[vertextagpair]
384 let startpoint = vertexpair.v1.pos
385 let endpoint = vertexpair.v2.pos
386 // our x,y and z vectors:
387 let zbase = endpoint.minus(startpoint).unit()
388 let xbase = vertexpair.planenormals[0].unit()
389 let ybase = xbase.cross(zbase)
390
391 // make a list of angles that the cylinder should traverse:
392 let angles = []
393
394 // first of all equally spaced around the cylinder:
395 for (let i = 0; i < resolution; i++) {
396 angles.push(i * Math.PI * 2 / resolution)
397 }
398
399 // and also at every normal of all touching planes:
400 for (let i = 0, iMax = vertexpair.planenormals.length; i < iMax; i++) {
401 let planenormal = vertexpair.planenormals[i]
402 let si = ybase.dot(planenormal)
403 let co = xbase.dot(planenormal)
404 let angle = Math.atan2(si, co)
405
406 if (angle < 0) angle += Math.PI * 2
407 angles.push(angle)
408 angle = Math.atan2(-si, -co)
409 if (angle < 0) angle += Math.PI * 2
410 angles.push(angle)
411 }
412
413 // this will result in some duplicate angles but we will get rid of those later.
414 // Sort:
415 angles = angles.sort(fnNumberSort)
416
417 // Now construct the cylinder by traversing all angles:
418 let numangles = angles.length
419 let prevp1
420 let prevp2
421 let startfacevertices = []
422 let endfacevertices = []
423 let polygons = []
424 for (let i = -1; i < numangles; i++) {
425 let angle = angles[(i < 0) ? (i + numangles) : i]
426 let si = Math.sin(angle)
427 let co = Math.cos(angle)
428 let p = xbase.times(co * radius).plus(ybase.times(si * radius))
429 let p1 = startpoint.plus(p)
430 let p2 = endpoint.plus(p)
431 let skip = false
432 if (i >= 0) {
433 if (p1.distanceTo(prevp1) < EPS) {
434 skip = true
435 }
436 }
437 if (!skip) {
438 if (i >= 0) {
439 startfacevertices.push(new Vertex(p1))
440 endfacevertices.push(new Vertex(p2))
441 let polygonvertices = [
442 new Vertex(prevp2),
443 new Vertex(p2),
444 new Vertex(p1),
445 new Vertex(prevp1)
446 ]
447 let polygon = new Polygon(polygonvertices)
448 polygons.push(polygon)
449 }
450 prevp1 = p1
451 prevp2 = p2
452 }
453 }
454 endfacevertices.reverse()
455 polygons.push(new Polygon(startfacevertices))
456 polygons.push(new Polygon(endfacevertices))
457 let cylinder = CSG.fromPolygons(polygons)
458 result = result.unionSub(cylinder, false, false)
459 }
460
461 // make a list of all unique vertices
462 // For each vertex we also collect the list of normals of the planes touching the vertices
463 let vertexmap = {}
464 csg.polygons.map(function (polygon) {
465 polygon.vertices.map(function (vertex) {
466 let vertextag = vertex.getTag()
467 let obj
468 if (vertextag in vertexmap) {
469 obj = vertexmap[vertextag]
470 } else {
471 obj = {
472 pos: vertex.pos,
473 normals: []
474 }
475 vertexmap[vertextag] = obj
476 }
477 obj.normals.push(polygon.plane.normal)
478 })
479 })
480
481 // and build spheres at each vertex
482 // We will try to set the x and z axis to the normals of 2 planes
483 // This will ensure that our sphere tesselation somewhat matches 2 planes
484 for (let vertextag in vertexmap) {
485 let vertexobj = vertexmap[vertextag]
486 // use the first normal to be the x axis of our sphere:
487 let xaxis = vertexobj.normals[0].unit()
488 // and find a suitable z axis. We will use the normal which is most perpendicular to the x axis:
489 let bestzaxis = null
490 let bestzaxisorthogonality = 0
491 for (let i = 1; i < vertexobj.normals.length; i++) {
492 let normal = vertexobj.normals[i].unit()
493 let cross = xaxis.cross(normal)
494 let crosslength = cross.length()
495 if (crosslength > 0.05) {
496 if (crosslength > bestzaxisorthogonality) {
497 bestzaxisorthogonality = crosslength
498 bestzaxis = normal
499 }
500 }
501 }
502 if (!bestzaxis) {
503 bestzaxis = xaxis.randomNonParallelVector()
504 }
505 let yaxis = xaxis.cross(bestzaxis).unit()
506 let zaxis = yaxis.cross(xaxis)
507 let _sphere = CSG.sphere({
508 center: vertexobj.pos,
509 radius: radius,
510 resolution: resolution,
511 axes: [xaxis, yaxis, zaxis]
512 })
513 result = result.unionSub(_sphere, false, false)
514 }
515
516 return result
517 },
518
519 canonicalized: function () {
520 if (this.isCanonicalized) {
521 return this
522 } else {
523 let factory = new FuzzyCSGFactory()
524 let result = CSGFromCSGFuzzyFactory(factory, this)
525 result.isCanonicalized = true
526 result.isRetesselated = this.isRetesselated
527 result.properties = this.properties // keep original properties
528 return result
529 }
530 },
531
532 reTesselated: function () {
533 if (this.isRetesselated) {
534 return this
535 } else {
536 let csg = this
537 let polygonsPerPlane = {}
538 let isCanonicalized = csg.isCanonicalized
539 let fuzzyfactory = new FuzzyCSGFactory()
540 csg.polygons.map(function (polygon) {
541 let plane = polygon.plane
542 let shared = polygon.shared
543 if (!isCanonicalized) {
544 // in order to identify to polygons having the same plane, we need to canonicalize the planes
545 // We don't have to do a full canonizalization (including vertices), to save time only do the planes and the shared data:
546 plane = fuzzyfactory.getPlane(plane)
547 shared = fuzzyfactory.getPolygonShared(shared)
548 }
549 let tag = plane.getTag() + '/' + shared.getTag()
550 if (!(tag in polygonsPerPlane)) {
551 polygonsPerPlane[tag] = [polygon]
552 } else {
553 polygonsPerPlane[tag].push(polygon)
554 }
555 })
556 let destpolygons = []
557 for (let planetag in polygonsPerPlane) {
558 let sourcepolygons = polygonsPerPlane[planetag]
559 if (sourcepolygons.length < 2) {
560 destpolygons = destpolygons.concat(sourcepolygons)
561 } else {
562 let retesselayedpolygons = []
563 reTesselateCoplanarPolygons(sourcepolygons, retesselayedpolygons)
564 destpolygons = destpolygons.concat(retesselayedpolygons)
565 }
566 }
567 let result = CSG.fromPolygons(destpolygons)
568 result.isRetesselated = true
569 // result = result.canonicalized();
570 result.properties = this.properties // keep original properties
571 return result
572 }
573 },
574
575 /**
576 * Returns an array of Vector3D, providing minimum coordinates and maximum coordinates
577 * of this solid.
578 * @returns {Vector3D[]}
579 * @example
580 * let bounds = A.getBounds()
581 * let minX = bounds[0].x
582 */
583 getBounds: function () {
584 if (!this.cachedBoundingBox) {
585 let minpoint = new Vector3D(0, 0, 0)
586 let maxpoint = new Vector3D(0, 0, 0)
587 let polygons = this.polygons
588 let numpolygons = polygons.length
589 for (let i = 0; i < numpolygons; i++) {
590 let polygon = polygons[i]
591 let bounds = polygon.boundingBox()
592 if (i === 0) {
593 minpoint = bounds[0]
594 maxpoint = bounds[1]
595 } else {
596 minpoint = minpoint.min(bounds[0])
597 maxpoint = maxpoint.max(bounds[1])
598 }
599 }
600 this.cachedBoundingBox = [minpoint, maxpoint]
601 }
602 return this.cachedBoundingBox
603 },
604
605 // returns true if there is a possibility that the two solids overlap
606 // returns false if we can be sure that they do not overlap
607 mayOverlap: function (csg) {
608 if ((this.polygons.length === 0) || (csg.polygons.length === 0)) {
609 return false
610 } else {
611 let mybounds = this.getBounds()
612 let otherbounds = csg.getBounds()
613 if (mybounds[1].x < otherbounds[0].x) return false
614 if (mybounds[0].x > otherbounds[1].x) return false
615 if (mybounds[1].y < otherbounds[0].y) return false
616 if (mybounds[0].y > otherbounds[1].y) return false
617 if (mybounds[1].z < otherbounds[0].z) return false
618 if (mybounds[0].z > otherbounds[1].z) return false
619 return true
620 }
621 },
622
623 // Cut the solid by a plane. Returns the solid on the back side of the plane
624 cutByPlane: function (plane) {
625 if (this.polygons.length === 0) {
626 return new CSG()
627 }
628 // Ideally we would like to do an intersection with a polygon of inifinite size
629 // but this is not supported by our implementation. As a workaround, we will create
630 // a cube, with one face on the plane, and a size larger enough so that the entire
631 // solid fits in the cube.
632 // find the max distance of any vertex to the center of the plane:
633 let planecenter = plane.normal.times(plane.w)
634 let maxdistance = 0
635 this.polygons.map(function (polygon) {
636 polygon.vertices.map(function (vertex) {
637 let distance = vertex.pos.distanceToSquared(planecenter)
638 if (distance > maxdistance) maxdistance = distance
639 })
640 })
641 maxdistance = Math.sqrt(maxdistance)
642 maxdistance *= 1.01 // make sure it's really larger
643 // Now build a polygon on the plane, at any point farther than maxdistance from the plane center:
644 let vertices = []
645 let orthobasis = new OrthoNormalBasis(plane)
646 vertices.push(new Vertex(orthobasis.to3D(new Vector2D(maxdistance, -maxdistance))))
647 vertices.push(new Vertex(orthobasis.to3D(new Vector2D(-maxdistance, -maxdistance))))
648 vertices.push(new Vertex(orthobasis.to3D(new Vector2D(-maxdistance, maxdistance))))
649 vertices.push(new Vertex(orthobasis.to3D(new Vector2D(maxdistance, maxdistance))))
650 let polygon = new Polygon(vertices, null, plane.flipped())
651
652 // and extrude the polygon into a cube, backwards of the plane:
653 let cube = polygon.extrude(plane.normal.times(-maxdistance))
654
655 // Now we can do the intersection:
656 let result = this.intersect(cube)
657 result.properties = this.properties // keep original properties
658 return result
659 },
660
661 // Connect a solid to another solid, such that two Connectors become connected
662 // myConnector: a Connector of this solid
663 // otherConnector: a Connector to which myConnector should be connected
664 // mirror: false: the 'axis' vectors of the connectors should point in the same direction
665 // true: the 'axis' vectors of the connectors should point in opposite direction
666 // normalrotation: degrees of rotation between the 'normal' vectors of the two
667 // connectors
668 connectTo: function (myConnector, otherConnector, mirror, normalrotation) {
669 let matrix = myConnector.getTransformationTo(otherConnector, mirror, normalrotation)
670 return this.transform(matrix)
671 },
672
673 // set the .shared property of all polygons
674 // Returns a new CSG solid, the original is unmodified!
675 setShared: function (shared) {
676 let polygons = this.polygons.map(function (p) {
677 return new Polygon(p.vertices, shared, p.plane)
678 })
679 let result = CSG.fromPolygons(polygons)
680 result.properties = this.properties // keep original properties
681 result.isRetesselated = this.isRetesselated
682 result.isCanonicalized = this.isCanonicalized
683 return result
684 },
685
686 setColor: function (args) {
687 let newshared = Polygon.Shared.fromColor.apply(this, arguments)
688 return this.setShared(newshared)
689 },
690
691 toCompactBinary: function () {
692 let csg = this.canonicalized(),
693 numpolygons = csg.polygons.length,
694 numpolygonvertices = 0,
695 numvertices = 0,
696 vertexmap = {},
697 vertices = [],
698 numplanes = 0,
699 planemap = {},
700 polygonindex = 0,
701 planes = [],
702 shareds = [],
703 sharedmap = {},
704 numshared = 0
705 // for (let i = 0, iMax = csg.polygons.length; i < iMax; i++) {
706 // let p = csg.polygons[i];
707 // for (let j = 0, jMax = p.length; j < jMax; j++) {
708 // ++numpolygonvertices;
709 // let vertextag = p[j].getTag();
710 // if(!(vertextag in vertexmap)) {
711 // vertexmap[vertextag] = numvertices++;
712 // vertices.push(p[j]);
713 // }
714 // }
715 csg.polygons.map(function (p) {
716 p.vertices.map(function (v) {
717 ++numpolygonvertices
718 let vertextag = v.getTag()
719 if (!(vertextag in vertexmap)) {
720 vertexmap[vertextag] = numvertices++
721 vertices.push(v)
722 }
723 })
724
725 let planetag = p.plane.getTag()
726 if (!(planetag in planemap)) {
727 planemap[planetag] = numplanes++
728 planes.push(p.plane)
729 }
730 let sharedtag = p.shared.getTag()
731 if (!(sharedtag in sharedmap)) {
732 sharedmap[sharedtag] = numshared++
733 shareds.push(p.shared)
734 }
735 })
736 let numVerticesPerPolygon = new Uint32Array(numpolygons)
737 let polygonSharedIndexes = new Uint32Array(numpolygons)
738 let polygonVertices = new Uint32Array(numpolygonvertices)
739 let polygonPlaneIndexes = new Uint32Array(numpolygons)
740 let vertexData = new Float64Array(numvertices * 3)
741 let planeData = new Float64Array(numplanes * 4)
742 let polygonVerticesIndex = 0
743 for (let polygonindex = 0; polygonindex < numpolygons; ++polygonindex) {
744 let p = csg.polygons[polygonindex]
745 numVerticesPerPolygon[polygonindex] = p.vertices.length
746 p.vertices.map(function (v) {
747 let vertextag = v.getTag()
748 let vertexindex = vertexmap[vertextag]
749 polygonVertices[polygonVerticesIndex++] = vertexindex
750 })
751 let planetag = p.plane.getTag()
752 let planeindex = planemap[planetag]
753 polygonPlaneIndexes[polygonindex] = planeindex
754 let sharedtag = p.shared.getTag()
755 let sharedindex = sharedmap[sharedtag]
756 polygonSharedIndexes[polygonindex] = sharedindex
757 }
758 let verticesArrayIndex = 0
759 vertices.map(function (v) {
760 let pos = v.pos
761 vertexData[verticesArrayIndex++] = pos._x
762 vertexData[verticesArrayIndex++] = pos._y
763 vertexData[verticesArrayIndex++] = pos._z
764 })
765 let planesArrayIndex = 0
766 planes.map(function (p) {
767 let normal = p.normal
768 planeData[planesArrayIndex++] = normal._x
769 planeData[planesArrayIndex++] = normal._y
770 planeData[planesArrayIndex++] = normal._z
771 planeData[planesArrayIndex++] = p.w
772 })
773 let result = {
774 'class': 'CSG',
775 numPolygons: numpolygons,
776 numVerticesPerPolygon: numVerticesPerPolygon,
777 polygonPlaneIndexes: polygonPlaneIndexes,
778 polygonSharedIndexes: polygonSharedIndexes,
779 polygonVertices: polygonVertices,
780 vertexData: vertexData,
781 planeData: planeData,
782 shared: shareds
783 }
784 return result
785 },
786
787 // Get the transformation that transforms this CSG such that it is lying on the z=0 plane,
788 // as flat as possible (i.e. the least z-height).
789 // So that it is in an orientation suitable for CNC milling
790 getTransformationAndInverseTransformationToFlatLying: function () {
791 if (this.polygons.length === 0) {
792 let m = new Matrix4x4() // unity
793 return [m, m]
794 } else {
795 // get a list of unique planes in the CSG:
796 let csg = this.canonicalized()
797 let planemap = {}
798 csg.polygons.map(function (polygon) {
799 planemap[polygon.plane.getTag()] = polygon.plane
800 })
801 // try each plane in the CSG and find the plane that, when we align it flat onto z=0,
802 // gives the least height in z-direction.
803 // If two planes give the same height, pick the plane that originally had a normal closest
804 // to [0,0,-1].
805 let xvector = new Vector3D(1, 0, 0)
806 let yvector = new Vector3D(0, 1, 0)
807 let zvector = new Vector3D(0, 0, 1)
808 let z0connectorx = new Connector([0, 0, 0], [0, 0, -1], xvector)
809 let z0connectory = new Connector([0, 0, 0], [0, 0, -1], yvector)
810 let isfirst = true
811 let minheight = 0
812 let maxdotz = 0
813 let besttransformation, bestinversetransformation
814 for (let planetag in planemap) {
815 let plane = planemap[planetag]
816 let pointonplane = plane.normal.times(plane.w)
817 let transformation, inversetransformation
818 // We need a normal vecrtor for the transformation
819 // determine which is more perpendicular to the plane normal: x or y?
820 // we will align this as much as possible to the x or y axis vector
821 let xorthogonality = plane.normal.cross(xvector).length()
822 let yorthogonality = plane.normal.cross(yvector).length()
823 if (xorthogonality > yorthogonality) {
824 // x is better:
825 let planeconnector = new Connector(pointonplane, plane.normal, xvector)
826 transformation = planeconnector.getTransformationTo(z0connectorx, false, 0)
827 inversetransformation = z0connectorx.getTransformationTo(planeconnector, false, 0)
828 } else {
829 // y is better:
830 let planeconnector = new Connector(pointonplane, plane.normal, yvector)
831 transformation = planeconnector.getTransformationTo(z0connectory, false, 0)
832 inversetransformation = z0connectory.getTransformationTo(planeconnector, false, 0)
833 }
834 let transformedcsg = csg.transform(transformation)
835 let dotz = -plane.normal.dot(zvector)
836 let bounds = transformedcsg.getBounds()
837 let zheight = bounds[1].z - bounds[0].z
838 let isbetter = isfirst
839 if (!isbetter) {
840 if (zheight < minheight) {
841 isbetter = true
842 } else if (zheight === minheight) {
843 if (dotz > maxdotz) isbetter = true
844 }
845 }
846 if (isbetter) {
847 // translate the transformation around the z-axis and onto the z plane:
848 let translation = new Vector3D([-0.5 * (bounds[1].x + bounds[0].x), -0.5 * (bounds[1].y + bounds[0].y), -bounds[0].z])
849 transformation = transformation.multiply(Matrix4x4.translation(translation))
850 inversetransformation = Matrix4x4.translation(translation.negated()).multiply(inversetransformation)
851 minheight = zheight
852 maxdotz = dotz
853 besttransformation = transformation
854 bestinversetransformation = inversetransformation
855 }
856 isfirst = false
857 }
858 return [besttransformation, bestinversetransformation]
859 }
860 },
861
862 getTransformationToFlatLying: function () {
863 let result = this.getTransformationAndInverseTransformationToFlatLying()
864 return result[0]
865 },
866
867 lieFlat: function () {
868 let transformation = this.getTransformationToFlatLying()
869 return this.transform(transformation)
870 },
871
872 // project the 3D CSG onto a plane
873 // This returns a 2D CAG with the 'shadow' shape of the 3D solid when projected onto the
874 // plane represented by the orthonormal basis
875 projectToOrthoNormalBasis: function (orthobasis) {
876 let cags = []
877 this.polygons.filter(function (p) {
878 // only return polys in plane, others may disturb result
879 return p.plane.normal.minus(orthobasis.plane.normal).lengthSquared() < (EPS * EPS)
880 })
881 .map(function (polygon) {
882 let cag = polygon.projectToOrthoNormalBasis(orthobasis)
883 if (cag.sides.length > 0) {
884 cags.push(cag)
885 }
886 })
887 let result = new CAG().union(cags)
888 return result
889 },
890
891 sectionCut: function (orthobasis) {
892 let plane1 = orthobasis.plane
893 let plane2 = orthobasis.plane.flipped()
894 plane1 = new Plane(plane1.normal, plane1.w)
895 plane2 = new Plane(plane2.normal, plane2.w + (5 * EPS))
896 let cut3d = this.cutByPlane(plane1)
897 cut3d = cut3d.cutByPlane(plane2)
898 return cut3d.projectToOrthoNormalBasis(orthobasis)
899 },
900
901 fixTJunctions: function () {
902 return fixTJunctions(CSG.fromPolygons, this)
903 },
904
905 toTriangles: function () {
906 let polygons = []
907 this.polygons.forEach(function (poly) {
908 let firstVertex = poly.vertices[0]
909 for (let i = poly.vertices.length - 3; i >= 0; i--) {
910 polygons.push(new Polygon([
911 firstVertex, poly.vertices[i + 1], poly.vertices[i + 2]
912 ],
913 poly.shared, poly.plane))
914 }
915 })
916 return polygons
917 },
918
919 /**
920 * Returns an array of values for the requested features of this solid.
921 * Supported Features: 'volume', 'area'
922 * @param {String[]} features - list of features to calculate
923 * @returns {Float[]} values
924 * @example
925 * let volume = A.getFeatures('volume')
926 * let values = A.getFeatures('area','volume')
927 */
928 getFeatures: function (features) {
929 if (!(features instanceof Array)) {
930 features = [features]
931 }
932 let result = this.toTriangles().map(function (triPoly) {
933 return triPoly.getTetraFeatures(features)
934 })
935 .reduce(function (pv, v) {
936 return v.map(function (feat, i) {
937 return feat + (pv === 0 ? 0 : pv[i])
938 })
939 }, 0)
940 return (result.length === 1) ? result[0] : result
941 }
942}
943
944/** Construct a CSG solid from a list of `Polygon` instances.
945 * @param {Polygon[]} polygons - list of polygons
946 * @returns {CSG} new CSG object
947 */
948CSG.fromPolygons = function fromPolygons (polygons) {
949 let csg = new CSG()
950 csg.polygons = polygons
951 csg.isCanonicalized = false
952 csg.isRetesselated = false
953 return csg
954}
955
956const CSGFromCSGFuzzyFactory = function (factory, sourcecsg) {
957 let _this = factory
958 let newpolygons = []
959 sourcecsg.polygons.forEach(function (polygon) {
960 let newpolygon = _this.getPolygon(polygon)
961 // see getPolygon above: we may get a polygon with no vertices, discard it:
962 if (newpolygon.vertices.length >= 3) {
963 newpolygons.push(newpolygon)
964 }
965 })
966 return CSG.fromPolygons(newpolygons)
967}
968
969module.exports = CSG