@jscad/modeling 2.12.6 → 2.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Benchmarks for boolean operations
3
+ *
4
+ * Run with: node bench/booleans.bench.js
5
+ */
6
+
7
+ const { cube, cuboid, sphere, cylinder, torus } = require('../src/primitives')
8
+ const { union, subtract, intersect } = require('../src/operations/booleans')
9
+ const { translate } = require('../src/operations/transforms')
10
+
11
+ // Simple benchmark runner
12
+ const benchmark = (name, fn, iterations = 10) => {
13
+ // Warmup
14
+ for (let i = 0; i < 2; i++) fn()
15
+
16
+ const start = process.hrtime.bigint()
17
+ for (let i = 0; i < iterations; i++) {
18
+ fn()
19
+ }
20
+ const end = process.hrtime.bigint()
21
+ const totalMs = Number(end - start) / 1e6
22
+ const avgMs = totalMs / iterations
23
+
24
+ console.log(`${name.padEnd(50)} ${avgMs.toFixed(1).padStart(10)} ms/op (${iterations} iterations)`)
25
+ return avgMs
26
+ }
27
+
28
+ console.log('='.repeat(80))
29
+ console.log('Boolean Operation Benchmarks')
30
+ console.log('='.repeat(80))
31
+ console.log()
32
+
33
+ // Prepare test geometries
34
+ const smallCube = cube({ size: 10 })
35
+ const smallCubeOffset = translate([5, 0, 0], cube({ size: 10 }))
36
+ const medCube = cube({ size: 20 })
37
+ const medCubeOffset = translate([10, 0, 0], cube({ size: 20 }))
38
+
39
+ const smallSphere = sphere({ radius: 5, segments: 16 })
40
+ const smallSphereOffset = translate([3, 0, 0], sphere({ radius: 5, segments: 16 }))
41
+ const medSphere = sphere({ radius: 5, segments: 32 })
42
+ const medSphereOffset = translate([3, 0, 0], sphere({ radius: 5, segments: 32 }))
43
+
44
+ const smallCyl = cylinder({ radius: 5, height: 10, segments: 16 })
45
+ const smallCylOffset = translate([3, 0, 0], cylinder({ radius: 5, height: 10, segments: 16 }))
46
+ const medCyl = cylinder({ radius: 5, height: 10, segments: 32 })
47
+ const medCylOffset = translate([3, 0, 0], cylinder({ radius: 5, height: 10, segments: 32 }))
48
+
49
+ const smallTorus = torus({ innerRadius: 1, outerRadius: 4, innerSegments: 16, outerSegments: 16 })
50
+ const smallTorusOffset = translate([2, 0, 0], torus({ innerRadius: 1, outerRadius: 4, innerSegments: 16, outerSegments: 16 }))
51
+ const medTorus = torus({ innerRadius: 1, outerRadius: 4, innerSegments: 32, outerSegments: 32 })
52
+ const medTorusOffset = translate([2, 0, 0], torus({ innerRadius: 1, outerRadius: 4, innerSegments: 32, outerSegments: 32 }))
53
+
54
+ console.log('--- Union Operations ---')
55
+ benchmark('union: cube + cube', () => union(smallCube, smallCubeOffset), 50)
56
+ benchmark('union: sphere(16) + sphere(16)', () => union(smallSphere, smallSphereOffset), 20)
57
+ benchmark('union: sphere(32) + sphere(32)', () => union(medSphere, medSphereOffset), 10)
58
+ benchmark('union: cylinder(16) + cylinder(16)', () => union(smallCyl, smallCylOffset), 20)
59
+ benchmark('union: cylinder(32) + cylinder(32)', () => union(medCyl, medCylOffset), 10)
60
+ benchmark('union: torus(16) + torus(16)', () => union(smallTorus, smallTorusOffset), 5)
61
+ benchmark('union: torus(32) + torus(32)', () => union(medTorus, medTorusOffset), 3)
62
+ console.log()
63
+
64
+ console.log('--- Subtract Operations ---')
65
+ benchmark('subtract: cube - cube', () => subtract(smallCube, smallCubeOffset), 50)
66
+ benchmark('subtract: sphere(16) - sphere(16)', () => subtract(smallSphere, smallSphereOffset), 20)
67
+ benchmark('subtract: sphere(32) - sphere(32)', () => subtract(medSphere, medSphereOffset), 10)
68
+ benchmark('subtract: cylinder(16) - cylinder(16)', () => subtract(smallCyl, smallCylOffset), 20)
69
+ benchmark('subtract: cylinder(32) - cylinder(32)', () => subtract(medCyl, medCylOffset), 10)
70
+ benchmark('subtract: torus(16) - torus(16)', () => subtract(smallTorus, smallTorusOffset), 5)
71
+ benchmark('subtract: torus(32) - torus(32)', () => subtract(medTorus, medTorusOffset), 3)
72
+ console.log()
73
+
74
+ console.log('--- Intersect Operations ---')
75
+ benchmark('intersect: cube & cube', () => intersect(smallCube, smallCubeOffset), 50)
76
+ benchmark('intersect: sphere(16) & sphere(16)', () => intersect(smallSphere, smallSphereOffset), 20)
77
+ benchmark('intersect: sphere(32) & sphere(32)', () => intersect(medSphere, medSphereOffset), 10)
78
+ benchmark('intersect: cylinder(16) & cylinder(16)', () => intersect(smallCyl, smallCylOffset), 20)
79
+ benchmark('intersect: cylinder(32) & cylinder(32)', () => intersect(medCyl, medCylOffset), 10)
80
+ benchmark('intersect: torus(16) & torus(16)', () => intersect(smallTorus, smallTorusOffset), 5)
81
+ benchmark('intersect: torus(32) & torus(32)', () => intersect(medTorus, medTorusOffset), 3)
82
+ console.log()
83
+
84
+ // Multiple operations (chain)
85
+ console.log('--- Chained Operations ---')
86
+ const cube1 = cube({ size: 10 })
87
+ const cube2 = translate([5, 0, 0], cube({ size: 10 }))
88
+ const cube3 = translate([0, 5, 0], cube({ size: 10 }))
89
+ const cube4 = translate([0, 0, 5], cube({ size: 10 }))
90
+
91
+ benchmark('union: 4 cubes', () => union(cube1, cube2, cube3, cube4), 20)
92
+ benchmark('subtract: cube - 3 cubes', () => subtract(cube1, cube2, cube3, cube4), 20)
93
+ console.log()
94
+
95
+ // Non-overlapping (fast path)
96
+ console.log('--- Non-overlapping (fast path) ---')
97
+ const farCube1 = cube({ size: 5 })
98
+ const farCube2 = translate([20, 0, 0], cube({ size: 5 }))
99
+ benchmark('union: non-overlapping cubes', () => union(farCube1, farCube2), 100)
100
+ console.log()
101
+
102
+ console.log('='.repeat(80))
103
+ console.log('Benchmark complete')
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Benchmarks for primitive shapes
3
+ *
4
+ * Run with: node bench/primitives.bench.js
5
+ */
6
+
7
+ const {
8
+ cube, cuboid, roundedCuboid,
9
+ sphere, geodesicSphere, ellipsoid,
10
+ cylinder, roundedCylinder, cylinderElliptic,
11
+ torus,
12
+ polyhedron
13
+ } = require('../src/primitives')
14
+
15
+ // Simple benchmark runner
16
+ const benchmark = (name, fn, iterations = 100) => {
17
+ // Warmup
18
+ for (let i = 0; i < 10; i++) fn()
19
+
20
+ const start = process.hrtime.bigint()
21
+ for (let i = 0; i < iterations; i++) {
22
+ fn()
23
+ }
24
+ const end = process.hrtime.bigint()
25
+ const totalMs = Number(end - start) / 1e6
26
+ const avgMs = totalMs / iterations
27
+
28
+ console.log(`${name.padEnd(40)} ${avgMs.toFixed(3).padStart(10)} ms/op (${iterations} iterations, ${totalMs.toFixed(1)} ms total)`)
29
+ return avgMs
30
+ }
31
+
32
+ console.log('='.repeat(80))
33
+ console.log('Primitive Shape Benchmarks')
34
+ console.log('='.repeat(80))
35
+ console.log()
36
+
37
+ // Box primitives
38
+ console.log('--- Box Primitives ---')
39
+ benchmark('cube (default)', () => cube())
40
+ benchmark('cube (size: 10)', () => cube({ size: 10 }))
41
+ benchmark('cuboid (default)', () => cuboid())
42
+ benchmark('cuboid (size: [10, 20, 30])', () => cuboid({ size: [10, 20, 30] }))
43
+ benchmark('roundedCuboid (default)', () => roundedCuboid())
44
+ benchmark('roundedCuboid (roundRadius: 2)', () => roundedCuboid({ size: [10, 10, 10], roundRadius: 2 }))
45
+ benchmark('roundedCuboid (segments: 32)', () => roundedCuboid({ size: [10, 10, 10], roundRadius: 2, segments: 32 }))
46
+ console.log()
47
+
48
+ // Sphere primitives
49
+ console.log('--- Sphere Primitives ---')
50
+ benchmark('sphere (default, 32 seg)', () => sphere())
51
+ benchmark('sphere (segments: 16)', () => sphere({ segments: 16 }))
52
+ benchmark('sphere (segments: 64)', () => sphere({ segments: 64 }), 50)
53
+ benchmark('sphere (segments: 128)', () => sphere({ segments: 128 }), 20)
54
+ benchmark('geodesicSphere (default)', () => geodesicSphere())
55
+ benchmark('geodesicSphere (frequency: 6)', () => geodesicSphere({ frequency: 6 }))
56
+ benchmark('geodesicSphere (frequency: 12)', () => geodesicSphere({ frequency: 12 }), 20)
57
+ benchmark('ellipsoid (default)', () => ellipsoid())
58
+ benchmark('ellipsoid (segments: 64)', () => ellipsoid({ segments: 64 }), 50)
59
+ console.log()
60
+
61
+ // Cylinder primitives
62
+ console.log('--- Cylinder Primitives ---')
63
+ benchmark('cylinder (default)', () => cylinder())
64
+ benchmark('cylinder (segments: 16)', () => cylinder({ segments: 16 }))
65
+ benchmark('cylinder (segments: 64)', () => cylinder({ segments: 64 }))
66
+ benchmark('cylinder (segments: 128)', () => cylinder({ segments: 128 }), 50)
67
+ benchmark('roundedCylinder (default)', () => roundedCylinder())
68
+ benchmark('roundedCylinder (segments: 64)', () => roundedCylinder({ segments: 64 }), 50)
69
+ benchmark('cylinderElliptic (default)', () => cylinderElliptic())
70
+ benchmark('cylinderElliptic (segments: 64)', () => cylinderElliptic({ segments: 64 }), 50)
71
+ console.log()
72
+
73
+ // Torus primitives (uses extrudeRotate internally)
74
+ console.log('--- Torus Primitives ---')
75
+ benchmark('torus (default 32x32)', () => torus())
76
+ benchmark('torus (16x16)', () => torus({ innerSegments: 16, outerSegments: 16 }))
77
+ benchmark('torus (32x32)', () => torus({ innerSegments: 32, outerSegments: 32 }))
78
+ benchmark('torus (48x48)', () => torus({ innerSegments: 48, outerSegments: 48 }), 50)
79
+ benchmark('torus (64x64)', () => torus({ innerSegments: 64, outerSegments: 64 }), 20)
80
+ benchmark('torus (partial, 180deg)', () => torus({ outerRotation: Math.PI }))
81
+ benchmark('torus (partial, 90deg)', () => torus({ outerRotation: Math.PI / 2 }))
82
+ console.log()
83
+
84
+ // Polyhedron
85
+ console.log('--- Polyhedron Primitives ---')
86
+ // Tetrahedron
87
+ const tetraPoints = [[1, 1, 1], [-1, -1, 1], [-1, 1, -1], [1, -1, -1]]
88
+ const tetraFaces = [[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]]
89
+ benchmark('polyhedron (tetrahedron)', () => polyhedron({ points: tetraPoints, faces: tetraFaces }))
90
+
91
+ // Larger polyhedron (icosahedron-like)
92
+ const phi = (1 + Math.sqrt(5)) / 2
93
+ const icoPoints = [
94
+ [-1, phi, 0], [1, phi, 0], [-1, -phi, 0], [1, -phi, 0],
95
+ [0, -1, phi], [0, 1, phi], [0, -1, -phi], [0, 1, -phi],
96
+ [phi, 0, -1], [phi, 0, 1], [-phi, 0, -1], [-phi, 0, 1]
97
+ ]
98
+ const icoFaces = [
99
+ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
100
+ [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
101
+ [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
102
+ [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]
103
+ ]
104
+ benchmark('polyhedron (icosahedron)', () => polyhedron({ points: icoPoints, faces: icoFaces }))
105
+ console.log()
106
+
107
+ console.log('='.repeat(80))
108
+ console.log('Benchmark complete')
@@ -836,7 +836,7 @@ const vec3=require("../../maths/vec3"),geom2=require("../../geometries/geom2"),g
836
836
  const plane=require("../../../maths/plane"),poly3=require("../../../geometries/poly3");class Node{constructor(e){this.plane=null,this.front=null,this.back=null,this.polygontreenodes=[],this.parent=e}invert(){const e=[this];let o;for(let n=0;n<e.length;n++){(o=e[n]).plane&&(o.plane=plane.flip(plane.create(),o.plane)),o.front&&e.push(o.front),o.back&&e.push(o.back);const t=o.front;o.front=o.back,o.back=t}}clipPolygons(e,o){let n,t={node:this,polygontreenodes:e};const l=[];do{if(n=t.node,e=t.polygontreenodes,n.plane){const t=n.plane,s=[],p=[],r=o?s:p,d=e.length;for(let o=0;o<d;o++){const n=e[o];n.isRemoved()||n.splitByPlane(t,r,s,p,s)}n.front&&p.length>0&&l.push({node:n.front,polygontreenodes:p});const h=s.length;if(n.back&&h>0)l.push({node:n.back,polygontreenodes:s});else for(let e=0;e<h;e++)s[e].remove()}t=l.pop()}while(void 0!==t)}clipTo(e,o){let n=this;const t=[];do{n.polygontreenodes.length>0&&e.rootnode.clipPolygons(n.polygontreenodes,o),n.front&&t.push(n.front),n.back&&t.push(n.back),n=t.pop()}while(void 0!==n)}addPolygonTreeNodes(e){let o={node:this,polygontreenodes:e};const n=[];do{const e=o.node,t=o.polygontreenodes;if(0===t.length){o=n.pop();continue}if(!e.plane){let o=0;const n=t[o=Math.floor(t.length/2)].getPolygon();e.plane=poly3.plane(n)}const l=[],s=[],p=t.length;for(let o=0;o<p;++o)t[o].splitByPlane(e.plane,e.polygontreenodes,s,l,s);if(l.length>0){e.front||(e.front=new Node(e)),p===l.length&&0===s.length?e.front.polygontreenodes=l:n.push({node:e.front,polygontreenodes:l})}if(s.length>0){e.back||(e.back=new Node(e)),p===s.length&&0===l.length?e.back.polygontreenodes=s:n.push({node:e.back,polygontreenodes:s})}o=n.pop()}while(void 0!==o)}}module.exports=Node;
837
837
 
838
838
  },{"../../../geometries/poly3":79,"../../../maths/plane":163}],280:[function(require,module,exports){
839
- const{EPS:EPS}=require("../../../maths/constants"),vec3=require("../../../maths/vec3"),poly3=require("../../../geometries/poly3"),splitPolygonByPlane=require("./splitPolygonByPlane");class PolygonTreeNode{constructor(e,t){this.parent=e,this.children=[],this.polygon=t,this.removed=!1}addPolygons(e){if(!this.isRootNode())throw new Error("Assertion failed");const t=this;e.forEach(e=>{t.addChild(e)})}remove(){if(!this.removed){this.removed=!0,this.polygon=null;const e=this.parent.children,t=e.indexOf(this);if(t<0)throw new Error("Assertion failed");e.splice(t,1),this.parent.recursivelyInvalidatePolygon()}}isRemoved(){return this.removed}isRootNode(){return!this.parent}invert(){if(!this.isRootNode())throw new Error("Assertion failed");this.invertSub()}getPolygon(){if(!this.polygon)throw new Error("Assertion failed");return this.polygon}getPolygons(e){let t=[this];const o=[t];let n,l,s,i;for(n=0;n<o.length;++n)for(l=0,s=(t=o[n]).length;l<s;l++)(i=t[l]).polygon?e.push(i.polygon):i.children.length>0&&o.push(i.children)}splitByPlane(e,t,o,n,l){if(this.children.length){const s=[this.children];let i,r,h,c,d;for(i=0;i<s.length;i++)for(d=s[i],r=0,h=d.length;r<h;r++)(c=d[r]).children.length>0?s.push(c.children):c._splitByPlane(e,t,o,n,l)}else this._splitByPlane(e,t,o,n,l)}_splitByPlane(e,t,o,n,l){const s=this.polygon;if(s){const i=poly3.measureBoundingSphere(s),r=i[3]+EPS,h=i,c=vec3.dot(e,h)-e[3];if(c>r)n.push(this);else if(c<-r)l.push(this);else{const i=splitPolygonByPlane(e,s);switch(i.type){case 0:t.push(this);break;case 1:o.push(this);break;case 2:n.push(this);break;case 3:l.push(this);break;case 4:if(i.front){const e=this.addChild(i.front);n.push(e)}if(i.back){const e=this.addChild(i.back);l.push(e)}}}}}addChild(e){const t=new PolygonTreeNode(this,e);return this.children.push(t),t}invertSub(){let e=[this];const t=[e];let o,n,l,s;for(o=0;o<t.length;o++)for(n=0,l=(e=t[o]).length;n<l;n++)(s=e[n]).polygon&&(s.polygon=poly3.invert(s.polygon)),s.children.length>0&&t.push(s.children)}recursivelyInvalidatePolygon(){this.polygon=null,this.parent&&this.parent.recursivelyInvalidatePolygon()}clear(){let e=[this];const t=[e];for(let o=0;o<t.length;++o){const n=(e=t[o]).length;for(let o=0;o<n;o++){const n=e[o];n.polygon&&(n.polygon=null),n.parent&&(n.parent=null),n.children.length>0&&t.push(n.children),n.children=[]}}}toString(){let e="",t=[this];const o=[t];let n,l,s,i;for(n=0;n<o.length;++n){t=o[n];const r=" ".repeat(n);for(l=0,s=t.length;l<s;l++)e+=`${r}PolygonTreeNode (${(i=t[l]).isRootNode()}): ${i.children.length}`,i.polygon?e+=`\n ${r}polygon: ${i.polygon.vertices}\n`:e+="\n",i.children.length>0&&o.push(i.children)}return e}}module.exports=PolygonTreeNode;
839
+ const{EPS:EPS}=require("../../../maths/constants"),vec3=require("../../../maths/vec3"),poly3=require("../../../geometries/poly3"),splitPolygonByPlane=require("./splitPolygonByPlane");class PolygonTreeNode{constructor(e,t){this.parent=e,this.children=[],this.polygon=t,this.removed=!1}addPolygons(e){if(!this.isRootNode())throw new Error("Assertion failed");const t=this;e.forEach(e=>{t.addChild(e)})}remove(){this.removed||(this.removed=!0,this.polygon=null,this.parent.recursivelyInvalidatePolygon())}isRemoved(){return this.removed}isRootNode(){return!this.parent}invert(){if(!this.isRootNode())throw new Error("Assertion failed");this.invertSub()}getPolygon(){if(!this.polygon)throw new Error("Assertion failed");return this.polygon}getPolygons(e){if(this.isRootNode()&&this.children.length>0){const e=[];for(let t=0;t<this.children.length;t++)this.children[t].removed||e.push(this.children[t]);this.children=e}let t=[this];const o=[t];let n,l,s,i;for(n=0;n<o.length;++n)for(l=0,s=(t=o[n]).length;l<s;l++)(i=t[l]).polygon?e.push(i.polygon):i.children.length>0&&o.push(i.children)}splitByPlane(e,t,o,n,l){if(this.children.length){const s=[this.children];let i,h,r,c,d;for(i=0;i<s.length;i++)for(d=s[i],h=0,r=d.length;h<r;h++)(c=d[h]).children.length>0?s.push(c.children):c._splitByPlane(e,t,o,n,l)}else this._splitByPlane(e,t,o,n,l)}_splitByPlane(e,t,o,n,l){const s=this.polygon;if(s){const i=poly3.measureBoundingSphere(s),h=i[3]+EPS,r=i,c=vec3.dot(e,r)-e[3];if(c>h)n.push(this);else if(c<-h)l.push(this);else{const i=splitPolygonByPlane(e,s);switch(i.type){case 0:t.push(this);break;case 1:o.push(this);break;case 2:n.push(this);break;case 3:l.push(this);break;case 4:if(i.front){const e=this.addChild(i.front);n.push(e)}if(i.back){const e=this.addChild(i.back);l.push(e)}}}}}addChild(e){const t=new PolygonTreeNode(this,e);return this.children.push(t),t}invertSub(){let e=[this];const t=[e];let o,n,l,s;for(o=0;o<t.length;o++)for(n=0,l=(e=t[o]).length;n<l;n++)(s=e[n]).polygon&&(s.polygon=poly3.invert(s.polygon)),s.children.length>0&&t.push(s.children)}recursivelyInvalidatePolygon(){this.polygon=null,this.parent&&this.parent.recursivelyInvalidatePolygon()}clear(){let e=[this];const t=[e];for(let o=0;o<t.length;++o){const n=(e=t[o]).length;for(let o=0;o<n;o++){const n=e[o];n.polygon&&(n.polygon=null),n.parent&&(n.parent=null),n.children.length>0&&t.push(n.children),n.children=[]}}}toString(){let e="",t=[this];const o=[t];let n,l,s,i;for(n=0;n<o.length;++n){t=o[n];const h=" ".repeat(n);for(l=0,s=t.length;l<s;l++)e+=`${h}PolygonTreeNode (${(i=t[l]).isRootNode()}): ${i.children.length}`,i.polygon?e+=`\n ${h}polygon: ${i.polygon.vertices}\n`:e+="\n",i.children.length>0&&o.push(i.children)}return e}}module.exports=PolygonTreeNode;
840
840
 
841
841
  },{"../../../geometries/poly3":79,"../../../maths/constants":94,"../../../maths/vec3":222,"./splitPolygonByPlane":284}],281:[function(require,module,exports){
842
842
  const Node=require("./Node"),PolygonTreeNode=require("./PolygonTreeNode");class Tree{constructor(o){this.polygonTree=new PolygonTreeNode,this.rootnode=new Node(null),o&&this.addPolygons(o)}invert(){this.polygonTree.invert(),this.rootnode.invert()}clipTo(o,e=!1){this.rootnode.clipTo(o,e)}allPolygons(){const o=[];return this.polygonTree.getPolygons(o),o}addPolygons(o){const e=new Array(o.length);for(let r=0;r<o.length;r++)e[r]=this.polygonTree.addChild(o[r]);this.rootnode.addPolygonTreeNodes(e)}clear(){this.polygonTree.clear()}toString(){return"Tree: "+this.polygonTree.toString("")}}module.exports=Tree;
@@ -848,7 +848,7 @@ module.exports={Tree:require("./Tree")};
848
848
  const vec3=require("../../../maths/vec3"),splitLineSegmentByPlane=(e,t,c)=>{const n=vec3.subtract(vec3.create(),c,t);let s=(e[3]-vec3.dot(e,t))/vec3.dot(e,n);return Number.isNaN(s)&&(s=0),s>1&&(s=1),s<0&&(s=0),vec3.scale(n,n,s),vec3.add(n,t,n),n};module.exports=splitLineSegmentByPlane;
849
849
 
850
850
  },{"../../../maths/vec3":222}],284:[function(require,module,exports){
851
- const{EPS:EPS}=require("../../../maths/constants"),plane=require("../../../maths/plane"),vec3=require("../../../maths/vec3"),poly3=require("../../../geometries/poly3"),splitLineSegmentByPlane=require("./splitLineSegmentByPlane"),splitPolygonByPlane=(e,t)=>{const l={type:null,front:null,back:null},n=t.vertices,s=n.length,o=poly3.plane(t);if(plane.equals(o,e))l.type=0;else{let t=!1,p=!1;const i=[],c=-EPS;for(let l=0;l<s;l++){const s=vec3.dot(e,n[l])-e[3],o=s<c;i.push(o),s>EPS&&(t=!0),s<c&&(p=!0)}if(t||p)if(p)if(t){l.type=4;const t=[],p=[];let c=i[0];for(let l=0;l<s;l++){const o=n[l];let r=l+1;r>=s&&(r=0);const a=i[r];if(c===a)c?p.push(o):t.push(o);else{const l=n[r],s=splitLineSegmentByPlane(e,o,l);c?(p.push(o),p.push(s),t.push(s)):(t.push(o),t.push(s),p.push(s))}c=a}const r=EPS*EPS;if(p.length>=3){let e=p[p.length-1];for(let t=0;t<p.length;t++){const l=p[t];vec3.squaredDistance(l,e)<r&&(p.splice(t,1),t--),e=l}}if(t.length>=3){let e=t[t.length-1];for(let l=0;l<t.length;l++){const n=t[l];vec3.squaredDistance(n,e)<r&&(t.splice(l,1),l--),e=n}}t.length>=3&&(l.front=poly3.fromPointsAndPlane(t,o)),p.length>=3&&(l.back=poly3.fromPointsAndPlane(p,o))}else l.type=3;else l.type=2;else{const t=vec3.dot(e,o);l.type=t>=0?0:1}}return l};module.exports=splitPolygonByPlane;
851
+ const{EPS:EPS}=require("../../../maths/constants"),plane=require("../../../maths/plane"),vec3=require("../../../maths/vec3"),poly3=require("../../../geometries/poly3"),splitLineSegmentByPlane=require("./splitLineSegmentByPlane"),EPS_SQUARED=EPS*EPS,removeConsecutiveDuplicates=e=>{const t=[];let n=e[e.length-1];for(let l=0;l<e.length;l++){const s=e[l];vec3.squaredDistance(s,n)>=EPS_SQUARED&&t.push(s),n=s}return t},splitPolygonByPlane=(e,t)=>{const n={type:null,front:null,back:null},l=t.vertices,s=l.length,o=poly3.plane(t);if(plane.equals(o,e))n.type=0;else{let t=!1,p=!1;const i=[],c=-EPS;for(let n=0;n<s;n++){const s=vec3.dot(e,l[n])-e[3],o=s<c;i.push(o),s>EPS&&(t=!0),s<c&&(p=!0)}if(t||p)if(p)if(t){n.type=4;const t=[],p=[];let c=i[0];for(let n=0;n<s;n++){const o=l[n];let u=n+1;u>=s&&(u=0);const r=i[u];if(c===r)c?p.push(o):t.push(o);else{const n=l[u],s=splitLineSegmentByPlane(e,o,n);c?(p.push(o),p.push(s),t.push(s)):(t.push(o),t.push(s),p.push(s))}c=r}if(t.length>=3){const e=removeConsecutiveDuplicates(t);e.length>=3&&(n.front=poly3.fromPointsAndPlane(e,o))}if(p.length>=3){const e=removeConsecutiveDuplicates(p);e.length>=3&&(n.back=poly3.fromPointsAndPlane(e,o))}}else n.type=3;else n.type=2;else{const t=vec3.dot(e,o);n.type=t>=0?0:1}}return n};module.exports=splitPolygonByPlane;
852
852
 
853
853
  },{"../../../geometries/poly3":79,"../../../maths/constants":94,"../../../maths/plane":163,"../../../maths/vec3":222,"./splitLineSegmentByPlane":283}],285:[function(require,module,exports){
854
854
  const flatten=require("../../utils/flatten"),areAllShapesTheSameType=require("../../utils/areAllShapesTheSameType"),geom2=require("../../geometries/geom2"),geom3=require("../../geometries/geom3"),unionGeom2=require("./unionGeom2"),unionGeom3=require("./unionGeom3"),union=(...e)=>{if(0===(e=flatten(e)).length)throw new Error("wrong number of arguments");if(!areAllShapesTheSameType(e))throw new Error("only unions of the same type are supported");const o=e[0];return geom2.isA(o)?unionGeom2(e):geom3.isA(o)?unionGeom3(e):o};module.exports=union;
@@ -920,7 +920,7 @@ const geom2=require("../../../geometries/geom2"),plane=require("../../../maths/p
920
920
  const pointInTriangle=(n,a,e,r,i,o,t,x)=>(i-t)*(a-x)-(n-t)*(o-x)>=0&&(n-t)*(r-x)-(e-t)*(a-x)>=0&&(e-t)*(o-x)-(i-t)*(r-x)>=0,area=(n,a,e)=>(a.y-n.y)*(e.x-a.x)-(a.x-n.x)*(e.y-a.y);module.exports={area:area,pointInTriangle:pointInTriangle};
921
921
 
922
922
  },{}],308:[function(require,module,exports){
923
- const mat4=require("../../maths/mat4"),geom2=require("../../geometries/geom2"),geom3=require("../../geometries/geom3"),poly3=require("../../geometries/poly3"),slice=require("./slice"),repairSlice=require("./slice/repair"),extrudeWalls=require("./extrudeWalls"),defaultCallback=(e,r,l)=>{let t=null;return geom2.isA(l)&&(t=slice.fromSides(geom2.toSides(l))),poly3.isA(l)&&(t=slice.fromPoints(poly3.toPoints(l))),0===e||1===e?slice.transform(mat4.fromTranslation(mat4.create(),[0,0,e]),t):null},extrudeFromSlices=(e,r)=>{const l={numberOfSlices:2,capStart:!0,capEnd:!0,close:!1,repair:!0,callback:defaultCallback},{numberOfSlices:t,capStart:o,capEnd:c,close:s,repair:i,callback:a}=Object.assign({},l,e);if(t<2)throw new Error("numberOfSlices must be 2 or more");i&&(r=repairSlice(r));const n=t-1;let u=null,m=null,f=null,g=[];for(let e=0;e<t;e++){const l=a(e/n,e,r);if(l){if(!slice.isA(l))throw new Error("the callback function must return slice objects");if(0===slice.toEdges(l).length)throw new Error("the callback function must return slices with one or more edges");f&&(g=g.concat(extrudeWalls(f,l))),0===e&&(u=l),e===t-1&&(m=l),f=l}}if(c){const e=slice.toPolygons(m);g=g.concat(e)}if(o){const e=slice.toPolygons(u).map(poly3.invert);g=g.concat(e)}return o||c||s&&!slice.equals(m,u)&&(g=g.concat(extrudeWalls(m,u))),geom3.create(g)};module.exports=extrudeFromSlices;
923
+ const mat4=require("../../maths/mat4"),geom2=require("../../geometries/geom2"),geom3=require("../../geometries/geom3"),poly3=require("../../geometries/poly3"),slice=require("./slice"),repairSlice=require("./slice/repair"),extrudeWalls=require("./extrudeWalls"),defaultCallback=(e,r,l)=>{let t=null;return geom2.isA(l)&&(t=slice.fromSides(geom2.toSides(l))),poly3.isA(l)&&(t=slice.fromPoints(poly3.toPoints(l))),0===e||1===e?slice.transform(mat4.fromTranslation(mat4.create(),[0,0,e]),t):null},extrudeFromSlices=(e,r)=>{const l={numberOfSlices:2,capStart:!0,capEnd:!0,close:!1,repair:!0,callback:defaultCallback},{numberOfSlices:t,capStart:o,capEnd:s,close:i,repair:c,callback:n}=Object.assign({},l,e);if(t<2)throw new Error("numberOfSlices must be 2 or more");c&&(r=repairSlice(r));const a=t-1;let u=null,m=null,f=null,g=[];for(let e=0;e<t;e++){const l=n(e/a,e,r);if(l){if(!slice.isA(l))throw new Error("the callback function must return slice objects");if(0===slice.toEdges(l).length)throw new Error("the callback function must return slices with one or more edges");if(f){const e=extrudeWalls(f,l);for(let r=0;r<e.length;r++)g.push(e[r])}0===e&&(u=l),e===t-1&&(m=l),f=l}}if(s){const e=slice.toPolygons(m);for(let r=0;r<e.length;r++)g.push(e[r])}if(o){const e=slice.toPolygons(u).map(poly3.invert);for(let r=0;r<e.length;r++)g.push(e[r])}if(!o&&!s&&i&&!slice.equals(m,u)){const e=extrudeWalls(m,u);for(let r=0;r<e.length;r++)g.push(e[r])}return geom3.create(g)};module.exports=extrudeFromSlices;
924
924
 
925
925
  },{"../../geometries/geom2":25,"../../geometries/geom3":41,"../../geometries/poly3":79,"../../maths/mat4":143,"./extrudeWalls":317,"./slice":326,"./slice/repair":328}],309:[function(require,module,exports){
926
926
  const{TAU:TAU}=require("../../maths/constants"),mat4=require("../../maths/mat4"),geom2=require("../../geometries/geom2"),extrudeFromSlices=require("./extrudeFromSlices"),slice=require("./slice"),extrudeHelical=(e,t)=>{const r={angle:TAU,startAngle:0,pitch:10,height:0,endOffset:0,segmentsPerRotation:32};let{angle:a,startAngle:s,pitch:o,height:n,endOffset:i,segmentsPerRotation:m}=Object.assign({},r,e);0!=n&&(o=n/(a/TAU));if(m<3)throw new Error("The number of segments per rotation needs to be at least 3.");const l=geom2.toSides(t);if(0===l.length)throw new Error("The given geometry cannot be empty");const c=l.filter(e=>e[0][0]>=0);let g=slice.fromSides(l);0===c.length&&(g=slice.reverse(g));const h=Math.round(m/TAU*Math.abs(a)),u=h>=2?h:2,f=mat4.create(),d=mat4.create();return extrudeFromSlices({numberOfSlices:u+1,callback:(e,t,r)=>{const n=s+a/u*t,m=i/u*t,l=(n-s)/TAU*o;return mat4.multiply(f,mat4.fromTranslation(mat4.create(),[m,0,l*Math.sign(a)]),mat4.fromXRotation(mat4.create(),-TAU/4*Math.sign(a))),mat4.multiply(d,mat4.fromZRotation(mat4.create(),n),f),slice.transform(d,r)}},g)};module.exports=extrudeHelical;
@@ -944,10 +944,10 @@ const{area:area}=require("../../maths/utils"),geom2=require("../../geometries/ge
944
944
  const path2=require("../../geometries/path2"),expand=require("../expansions/expand"),extrudeLinearGeom2=require("./extrudeLinearGeom2"),extrudeRectangularPath2=(e,t)=>{const{size:r,height:n}=Object.assign({},{size:1,height:1},e);if(e.delta=r,e.offset=[0,0,n],0===path2.toPoints(t).length)throw new Error("the given geometry cannot be empty");const a=expand(e,t);return extrudeLinearGeom2(e,a)};module.exports=extrudeRectangularPath2;
945
945
 
946
946
  },{"../../geometries/path2":62,"../expansions/expand":289,"./extrudeLinearGeom2":311}],316:[function(require,module,exports){
947
- const{TAU:TAU}=require("../../maths/constants"),mat4=require("../../maths/mat4"),{mirrorX:mirrorX}=require("../transforms/mirror"),geom2=require("../../geometries/geom2"),slice=require("./slice"),extrudeFromSlices=require("./extrudeFromSlices"),extrudeRotate=(e,t)=>{const r={segments:12,startAngle:0,angle:TAU,overflow:"cap"};let{segments:a,startAngle:o,angle:s,overflow:m}=Object.assign({},r,e);if(a<3)throw new Error("segments must be greater then 3");o=Math.abs(o)>TAU?o%TAU:o,s=Math.abs(s)>TAU?s%TAU:s;let n=o+s;if((n=Math.abs(n)>TAU?n%TAU:n)<o){const e=o;o=n,n=e}let l=n-o;if(l<=0&&(l=TAU),Math.abs(l)<TAU){const e=TAU/a;a=Math.floor(Math.abs(l)/e),Math.abs(l)>a*e&&a++}let i=geom2.toSides(t);if(0===i.length)throw new Error("the given geometry cannot be empty");const c=i.filter(e=>e[0][0]<0),g=i.filter(e=>e[0][0]>=0);c.length>0&&g.length>0&&"cap"===m&&(c.length>g.length?(i=i.map(e=>{let t=e[0],r=e[1];return[t=[Math.min(t[0],0),t[1]],r=[Math.min(r[0],0),r[1]]]}),t=geom2.create(i),t=mirrorX(t)):g.length>=c.length&&(i=i.map(e=>{let t=e[0],r=e[1];return[t=[Math.max(t[0],0),t[1]],r=[Math.max(r[0],0),r[1]]]}),t=geom2.create(i)));const h=l/a,u=Math.abs(l)<TAU,A=slice.fromSides(geom2.toSides(t));slice.reverse(A,A);const f=mat4.create();return extrudeFromSlices(e={numberOfSlices:a+1,capStart:u,capEnd:u,close:!u,callback:(e,t,r)=>{let s=h*t+o;return l===TAU&&t===a&&(s=o),mat4.multiply(f,mat4.fromZRotation(f,s),mat4.fromXRotation(mat4.create(),TAU/4)),slice.transform(f,r)}},A)};module.exports=extrudeRotate;
947
+ const{TAU:TAU}=require("../../maths/constants"),mat4=require("../../maths/mat4"),{mirrorX:mirrorX}=require("../transforms/mirror"),geom2=require("../../geometries/geom2"),slice=require("./slice"),extrudeFromSlices=require("./extrudeFromSlices"),extrudeRotate=(e,t)=>{const r={segments:12,startAngle:0,angle:TAU,overflow:"cap"};let{segments:a,startAngle:o,angle:s,overflow:m}=Object.assign({},r,e);if(a<3)throw new Error("segments must be greater then 3");o=Math.abs(o)>TAU?o%TAU:o,s=Math.abs(s)>TAU?s%TAU:s;let n=o+s;if((n=Math.abs(n)>TAU?n%TAU:n)<o){const e=o;o=n,n=e}let l=n-o;if(l<=0&&(l=TAU),Math.abs(l)<TAU){const e=TAU/a;a=Math.floor(Math.abs(l)/e),Math.abs(l)>a*e&&a++}let i=geom2.toSides(t);if(0===i.length)throw new Error("the given geometry cannot be empty");const c=i.filter(e=>e[0][0]<0),g=i.filter(e=>e[0][0]>=0);c.length>0&&g.length>0&&"cap"===m&&(c.length>g.length?(i=i.map(e=>{let t=e[0],r=e[1];return[t=[Math.min(t[0],0),t[1]],r=[Math.min(r[0],0),r[1]]]}),t=geom2.create(i),t=mirrorX(t)):g.length>=c.length&&(i=i.map(e=>{let t=e[0],r=e[1];return[t=[Math.max(t[0],0),t[1]],r=[Math.max(r[0],0),r[1]]]}),t=geom2.create(i)));const h=l/a,u=Math.abs(l)<TAU,A=slice.fromSides(geom2.toSides(t));slice.reverse(A,A);const f=mat4.create(),T=mat4.fromXRotation(mat4.create(),TAU/4),U=mat4.create();return extrudeFromSlices(e={numberOfSlices:a+1,capStart:u,capEnd:u,close:!u,callback:(e,t,r)=>{let s=h*t+o;return l===TAU&&t===a&&(s=o),mat4.fromZRotation(U,s),mat4.multiply(f,U,T),slice.transform(f,r)}},A)};module.exports=extrudeRotate;
948
948
 
949
949
  },{"../../geometries/geom2":25,"../../maths/constants":94,"../../maths/mat4":143,"../transforms/mirror":363,"./extrudeFromSlices":308,"./slice":326}],317:[function(require,module,exports){
950
- const{EPS:EPS}=require("../../maths/constants"),vec3=require("../../maths/vec3"),poly3=require("../../geometries/poly3"),slice=require("./slice"),gcd=(e,t)=>e===t?e:e<t?gcd(t,e):1===t?1:0===t?e:gcd(t,e%t),lcm=(e,t)=>e*t/gcd(e,t),repartitionEdges=(e,t)=>{const r=e/t.length;if(1===r)return t;const s=vec3.fromValues(r,r,r),c=[];return t.forEach(e=>{const t=vec3.subtract(vec3.create(),e[1],e[0]);vec3.divide(t,t,s);let l=e[0];for(let e=1;e<=r;++e){const e=vec3.add(vec3.create(),l,t);c.push([l,e]),l=e}}),c},EPSAREA=EPS*EPS/2*Math.sin(Math.PI/3),extrudeWalls=(e,t)=>{let r=slice.toEdges(e),s=slice.toEdges(t);if(r.length!==s.length){const e=lcm(r.length,s.length);e!==r.length&&(r=repartitionEdges(e,r)),e!==s.length&&(s=repartitionEdges(e,s))}const c=[];return r.forEach((e,t)=>{const r=s[t],l=poly3.create([e[0],e[1],r[1]]),o=poly3.measureArea(l);Number.isFinite(o)&&o>EPSAREA&&c.push(l);const n=poly3.create([e[0],r[1],r[0]]),i=poly3.measureArea(n);Number.isFinite(i)&&i>EPSAREA&&c.push(n)}),c};module.exports=extrudeWalls;
950
+ const{EPS:EPS}=require("../../maths/constants"),vec3=require("../../maths/vec3"),poly3=require("../../geometries/poly3"),slice=require("./slice"),gcd=(e,t)=>e===t?e:e<t?gcd(t,e):1===t?1:0===t?e:gcd(t,e%t),lcm=(e,t)=>e*t/gcd(e,t),repartitionEdges=(e,t)=>{const r=e/t.length;if(1===r)return t;const s=vec3.fromValues(r,r,r),c=vec3.create(),l=[];return t.forEach(e=>{vec3.subtract(c,e[1],e[0]),vec3.divide(c,c,s);let t=e[0];for(let e=1;e<=r;++e){const e=vec3.add(vec3.create(),t,c);l.push([t,e]),t=e}}),l},EPSAREA=EPS*EPS/2*Math.sin(Math.PI/3),extrudeWalls=(e,t)=>{let r=slice.toEdges(e),s=slice.toEdges(t);if(r.length!==s.length){const e=lcm(r.length,s.length);e!==r.length&&(r=repartitionEdges(e,r)),e!==s.length&&(s=repartitionEdges(e,s))}const c=[];return r.forEach((e,t)=>{const r=s[t],l=poly3.create([e[0],e[1],r[1]]),o=poly3.measureArea(l);Number.isFinite(o)&&o>EPSAREA&&c.push(l);const i=poly3.create([e[0],r[1],r[0]]),n=poly3.measureArea(i);Number.isFinite(n)&&n>EPSAREA&&c.push(i)}),c};module.exports=extrudeWalls;
951
951
 
952
952
  },{"../../geometries/poly3":79,"../../maths/constants":94,"../../maths/vec3":222,"./slice":326}],318:[function(require,module,exports){
953
953
  module.exports={extrudeFromSlices:require("./extrudeFromSlices"),extrudeLinear:require("./extrudeLinear"),extrudeRectangular:require("./extrudeRectangular"),extrudeRotate:require("./extrudeRotate"),extrudeHelical:require("./extrudeHelical"),project:require("./project"),slice:require("./slice")};
@@ -1061,10 +1061,10 @@ const constants=require("../../maths/constants"),vec3=require("../../maths/vec3"
1061
1061
  const aboutEqualNormals=require("../../maths/utils/aboutEqualNormals"),vec3=require("../../maths/vec3"),poly3=require("../../geometries/poly3"),createEdges=e=>{const n=poly3.toPoints(e),t=[];for(let e=0;e<n.length;e++){const l=(e+1)%n.length,r={v1:n[e],v2:n[l]};t.push(r)}for(let e=0;e<t.length;e++){const l=(e+1)%n.length;t[e].next=t[l],t[l].prev=t[e]}return t},insertEdge=(e,n)=>{const t=`${n.v1}:${n.v2}`;e.set(t,n)},deleteEdge=(e,n)=>{const t=`${n.v1}:${n.v2}`;e.delete(t)},findOppositeEdge=(e,n)=>{const t=`${n.v2}:${n.v1}`;return e.get(t)},calculateAnglesBetween=(e,n,t)=>{let l=e.prev.v1,r=e.prev.v2,o=n.next.v2;const v=calculateAngle(l,r,o,t);return l=n.prev.v1,r=n.prev.v2,o=e.next.v2,[v,calculateAngle(l,r,o,t)]},v1=vec3.create(),v2=vec3.create(),calculateAngle=(e,n,t,l)=>{const r=vec3.subtract(v1,n,e),o=vec3.subtract(v2,t,n);return vec3.cross(r,r,o),vec3.dot(r,l)},createPolygonAnd=e=>{let n;const t=[];for(;e.next;){const n=e.next;t.push(e.v1),e.v1=null,e.v2=null,e.next=null,e.prev=null,e=n}return t.length>0&&(n=poly3.create(t)),n},mergeCoplanarPolygons=e=>{if(e.length<2)return e;const n=e[0].plane,t=e.slice(),l=new Map;for(;t.length>0;){const e=t.shift(),r=createEdges(e);for(let e=0;e<r.length;e++){const t=r[e],o=findOppositeEdge(l,t);if(o){const e=calculateAnglesBetween(t,o,n);if(e[0]>=0&&e[1]>=0){const n=o.next,r=t.next;t.prev.next=o.next,t.next.prev=o.prev,o.prev.next=t.next,o.next.prev=t.prev,t.v1=null,t.v2=null,t.next=null,t.prev=null,deleteEdge(l,o),o.v1=null,o.v2=null,o.next=null,o.prev=null;const v=(e,n,t)=>{const l={v1:t.v1,v2:n.v2,next:n.next,prev:t.prev};t.prev.next=l,n.next.prev=l,deleteEdge(e,n),n.v1=null,n.v2=null,n.next=null,n.prev=null,deleteEdge(e,t),t.v1=null,t.v2=null,t.next=null,t.prev=null};0===e[0]&&v(l,n,n.prev),0===e[1]&&v(l,r,r.prev)}}else t.next&&insertEdge(l,t)}}const r=[];return l.forEach(e=>{const n=createPolygonAnd(e);n&&r.push(n)}),l.clear(),r},coplanar=(e,n)=>Math.abs(e[3]-n[3])<1.5e-7&&aboutEqualNormals(e,n),mergePolygons=(e,n)=>{const t=[];n.forEach(e=>{const n=t.find(n=>coplanar(n[0],poly3.plane(e)));if(n){n[1].push(e)}else t.push([poly3.plane(e),[e]])});let l=[];return t.forEach(e=>{const n=e[1],t=mergeCoplanarPolygons(n);l=l.concat(t)}),l};module.exports=mergePolygons;
1062
1062
 
1063
1063
  },{"../../geometries/poly3":79,"../../maths/utils/aboutEqualNormals":167,"../../maths/vec3":222}],355:[function(require,module,exports){
1064
- const{EPS:EPS}=require("../../maths/constants"),line2=require("../../maths/line2"),vec2=require("../../maths/vec2"),OrthoNormalBasis=require("../../maths/OrthoNormalBasis"),interpolateBetween2DPointsForY=require("../../maths/utils/interpolateBetween2DPointsForY"),{insertSorted:insertSorted,fnNumberSort:fnNumberSort}=require("../../utils"),poly3=require("../../geometries/poly3"),reTesselateCoplanarPolygons=t=>{if(t.length<2)return t;const e=[],o=t.length,n=poly3.plane(t[0]),l=new OrthoNormalBasis(n),i=[],r=[],s=new Map,f=new Map,p=new Map,h=10/EPS;for(let e=0;e<o;e++){const o=t[e];let n=[],g=o.vertices.length,c=-1;if(g>0){let t,i;for(let r=0;r<g;r++){let s=l.to2D(o.vertices[r]);const g=Math.floor(s[1]*h);let a;p.has(g)?a=p.get(g):p.has(g+1)?a=p.get(g+1):p.has(g-1)?a=p.get(g-1):(a=s[1],p.set(g,s[1])),s=vec2.fromValues(s[0],a),n.push(s);const u=s[1];(0===r||u<t)&&(t=u,c=r),(0===r||u>i)&&(i=u);let m=f.get(u);m||(m={},f.set(u,m)),m[e]=!0}if(t>=i)n=[],g=0,c=-1;else{let o=s.get(t);o||(o=[],s.set(t,o)),o.push(e)}}n.reverse(),c=g-c-1,i.push(n),r.push(c)}const g=[];f.forEach((t,e)=>g.push(e)),g.sort(fnNumberSort);let c=[],a=[];for(let t=0;t<g.length;t++){const o=[],p=g[t],h=f.get(p);for(let t=0;t<c.length;++t){const e=c[t],o=e.polygonindex;if(h[o]){const n=i[o],l=n.length;let r=e.leftvertexindex,s=e.rightvertexindex;for(;;){let t=r+1;if(t>=l&&(t=0),n[t][1]!==p)break;r=t}let f=s-1;if(f<0&&(f=l-1),n[f][1]===p&&(s=f),r!==e.leftvertexindex&&r===s)c.splice(t,1),--t;else{e.leftvertexindex=r,e.rightvertexindex=s,e.topleft=n[r],e.topright=n[s];let t=r+1;t>=l&&(t=0),e.bottomleft=n[t];let o=s-1;o<0&&(o=l-1),e.bottomright=n[o]}}}let u;if(t>=g.length-1)c=[],u=null;else{const e=.5*(p+(u=Number(g[t+1]))),o=s.get(p);for(const t in o){const n=o[t],l=i[n],s=l.length,f=r[n];let h=f;for(;;){let t=h+1;if(t>=s&&(t=0),l[t][1]!==p)break;if(t===f)break;h=t}let g=f;for(;;){let t=g-1;if(t<0&&(t=s-1),l[t][1]!==p)break;if(t===h)break;g=t}let a=h+1;a>=s&&(a=0);let u=g-1;u<0&&(u=s-1);const m={polygonindex:n,leftvertexindex:h,rightvertexindex:g,topleft:l[h],topright:l[g],bottomleft:l[a],bottomright:l[u]};insertSorted(c,m,(t,o)=>{const n=interpolateBetween2DPointsForY(t.topleft,t.bottomleft,e),l=interpolateBetween2DPointsForY(o.topleft,o.bottomleft,e);return n>l?1:n<l?-1:0})}}for(const t in c){const e=c[t];let n=interpolateBetween2DPointsForY(e.topleft,e.bottomleft,p);const l=vec2.fromValues(n,p);n=interpolateBetween2DPointsForY(e.topright,e.bottomright,p);const i=vec2.fromValues(n,p);n=interpolateBetween2DPointsForY(e.topleft,e.bottomleft,u);const r=vec2.fromValues(n,u);n=interpolateBetween2DPointsForY(e.topright,e.bottomright,u);const s=vec2.fromValues(n,u),f={topleft:l,topright:i,bottomleft:r,bottomright:s,leftline:line2.fromPoints(line2.create(),l,r),rightline:line2.fromPoints(line2.create(),s,i)};if(o.length>0){const t=o[o.length-1],e=vec2.distance(f.topleft,t.topright),n=vec2.distance(f.bottomleft,t.bottomright);e<EPS&&n<EPS&&(f.topleft=t.topleft,f.leftline=t.leftline,f.bottomleft=t.bottomleft,o.splice(o.length-1,1))}o.push(f)}if(t>0){const t=new Set,i=new Set;for(let e=0;e<o.length;e++){const n=o[e];for(let e=0;e<a.length;e++)if(!i.has(e)){const o=a[e];if(vec2.distance(o.bottomleft,n.topleft)<EPS&&vec2.distance(o.bottomright,n.topright)<EPS){i.add(e);const l=line2.direction(n.leftline),r=line2.direction(o.leftline),s=l[0]-r[0],f=line2.direction(n.rightline),p=line2.direction(o.rightline),h=f[0]-p[0],g=Math.abs(s)<EPS,c=Math.abs(h)<EPS,a=c||h>=0;(g||s>=0)&&a&&(n.outpolygon=o.outpolygon,n.leftlinecontinues=g,n.rightlinecontinues=c,t.add(e));break}}}for(let o=0;o<a.length;o++)if(!t.has(o)){const t=a[o];t.outpolygon.rightpoints.push(t.bottomright),vec2.distance(t.bottomright,t.bottomleft)>EPS&&t.outpolygon.leftpoints.push(t.bottomleft),t.outpolygon.leftpoints.reverse();const i=t.outpolygon.rightpoints.concat(t.outpolygon.leftpoints).map(t=>l.to3D(t)),r=poly3.fromPointsAndPlane(i,n);r.vertices.length&&e.push(r)}}for(let t=0;t<o.length;t++){const e=o[t];e.outpolygon?(e.leftlinecontinues||e.outpolygon.leftpoints.push(e.topleft),e.rightlinecontinues||e.outpolygon.rightpoints.push(e.topright)):(e.outpolygon={leftpoints:[],rightpoints:[]},e.outpolygon.leftpoints.push(e.topleft),vec2.distance(e.topleft,e.topright)>EPS&&e.outpolygon.rightpoints.push(e.topright))}a=o}return e};module.exports=reTesselateCoplanarPolygons;
1064
+ const{EPS:EPS}=require("../../maths/constants"),line2=require("../../maths/line2"),vec2=require("../../maths/vec2"),OrthoNormalBasis=require("../../maths/OrthoNormalBasis"),interpolateBetween2DPointsForY=require("../../maths/utils/interpolateBetween2DPointsForY"),{insertSorted:insertSorted,fnNumberSort:fnNumberSort}=require("../../utils"),poly3=require("../../geometries/poly3"),reTesselateCoplanarPolygons=t=>{if(t.length<2)return t;const e=[],o=t.length,n=poly3.plane(t[0]),l=new OrthoNormalBasis(n),i=[],r=[],s=new Map,f=new Map,p=new Map,h=10/EPS;for(let e=0;e<o;e++){const o=t[e];let n=[],g=o.vertices.length,a=-1;if(g>0){let t,i;for(let r=0;r<g;r++){let s=l.to2D(o.vertices[r]);const g=Math.floor(s[1]*h);let c;p.has(g)?c=p.get(g):p.has(g+1)?c=p.get(g+1):p.has(g-1)?c=p.get(g-1):(c=s[1],p.set(g,s[1])),s=vec2.fromValues(s[0],c),n.push(s);const u=s[1];(0===r||u<t)&&(t=u,a=r),(0===r||u>i)&&(i=u);let m=f.get(u);m||(m={},f.set(u,m)),m[e]=!0}if(t>=i)n=[],g=0,a=-1;else{let o=s.get(t);o||(o=[],s.set(t,o)),o.push(e)}}n.reverse(),a=g-a-1,i.push(n),r.push(a)}const g=[];f.forEach((t,e)=>g.push(e)),g.sort(fnNumberSort);let a=[],c=[];for(let t=0;t<g.length;t++){const o=[],p=g[t],h=f.get(p);let u,m=0;for(let t=0;t<a.length;++t){const e=a[t],o=e.polygonindex;if(h[o]){const t=i[o],n=t.length;let l=e.leftvertexindex,r=e.rightvertexindex;for(;;){let e=l+1;if(e>=n&&(e=0),t[e][1]!==p)break;l=e}let s=r-1;if(s<0&&(s=n-1),t[s][1]===p&&(r=s),l!==e.leftvertexindex&&l===r)e._remove=!0,m++;else{e.leftvertexindex=l,e.rightvertexindex=r,e.topleft=t[l],e.topright=t[r];let o=l+1;o>=n&&(o=0),e.bottomleft=t[o];let i=r-1;i<0&&(i=n-1),e.bottomright=t[i]}}}if(m>0&&(a=a.filter(t=>!t._remove)),t>=g.length-1)a=[],u=null;else{const e=.5*(p+(u=Number(g[t+1]))),o=s.get(p);for(const t in o){const n=o[t],l=i[n],s=l.length,f=r[n];let h=f;for(;;){let t=h+1;if(t>=s&&(t=0),l[t][1]!==p)break;if(t===f)break;h=t}let g=f;for(;;){let t=g-1;if(t<0&&(t=s-1),l[t][1]!==p)break;if(t===h)break;g=t}let c=h+1;c>=s&&(c=0);let u=g-1;u<0&&(u=s-1);const m={polygonindex:n,leftvertexindex:h,rightvertexindex:g,topleft:l[h],topright:l[g],bottomleft:l[c],bottomright:l[u]};insertSorted(a,m,(t,o)=>{const n=interpolateBetween2DPointsForY(t.topleft,t.bottomleft,e),l=interpolateBetween2DPointsForY(o.topleft,o.bottomleft,e);return n>l?1:n<l?-1:0})}}for(const t in a){const e=a[t];let n=interpolateBetween2DPointsForY(e.topleft,e.bottomleft,p);const l=vec2.fromValues(n,p);n=interpolateBetween2DPointsForY(e.topright,e.bottomright,p);const i=vec2.fromValues(n,p);n=interpolateBetween2DPointsForY(e.topleft,e.bottomleft,u);const r=vec2.fromValues(n,u);n=interpolateBetween2DPointsForY(e.topright,e.bottomright,u);const s=vec2.fromValues(n,u),f={topleft:l,topright:i,bottomleft:r,bottomright:s,leftline:line2.fromPoints(line2.create(),l,r),rightline:line2.fromPoints(line2.create(),s,i)};if(o.length>0){const t=o[o.length-1],e=vec2.distance(f.topleft,t.topright),n=vec2.distance(f.bottomleft,t.bottomright);e<EPS&&n<EPS&&(f.topleft=t.topleft,f.leftline=t.leftline,f.bottomleft=t.bottomleft,o.splice(o.length-1,1))}o.push(f)}if(t>0){const t=new Set,i=new Set;for(let e=0;e<o.length;e++){const n=o[e];for(let e=0;e<c.length;e++)if(!i.has(e)){const o=c[e];if(vec2.distance(o.bottomleft,n.topleft)<EPS&&vec2.distance(o.bottomright,n.topright)<EPS){i.add(e);const l=line2.direction(n.leftline),r=line2.direction(o.leftline),s=l[0]-r[0],f=line2.direction(n.rightline),p=line2.direction(o.rightline),h=f[0]-p[0],g=Math.abs(s)<EPS,a=Math.abs(h)<EPS,c=a||h>=0;(g||s>=0)&&c&&(n.outpolygon=o.outpolygon,n.leftlinecontinues=g,n.rightlinecontinues=a,t.add(e));break}}}for(let o=0;o<c.length;o++)if(!t.has(o)){const t=c[o];t.outpolygon.rightpoints.push(t.bottomright),vec2.distance(t.bottomright,t.bottomleft)>EPS&&t.outpolygon.leftpoints.push(t.bottomleft),t.outpolygon.leftpoints.reverse();const i=t.outpolygon.rightpoints.concat(t.outpolygon.leftpoints).map(t=>l.to3D(t)),r=poly3.fromPointsAndPlane(i,n);r.vertices.length&&e.push(r)}}for(let t=0;t<o.length;t++){const e=o[t];e.outpolygon?(e.leftlinecontinues||e.outpolygon.leftpoints.push(e.topleft),e.rightlinecontinues||e.outpolygon.rightpoints.push(e.topright)):(e.outpolygon={leftpoints:[],rightpoints:[]},e.outpolygon.leftpoints.push(e.topleft),vec2.distance(e.topleft,e.topright)>EPS&&e.outpolygon.rightpoints.push(e.topright))}c=o}return e};module.exports=reTesselateCoplanarPolygons;
1065
1065
 
1066
1066
  },{"../../geometries/poly3":79,"../../maths/OrthoNormalBasis":93,"../../maths/constants":94,"../../maths/line2":105,"../../maths/utils/interpolateBetween2DPointsForY":170,"../../maths/vec2":191,"../../utils":400}],356:[function(require,module,exports){
1067
- const geom3=require("../../geometries/geom3"),poly3=require("../../geometries/poly3"),{NEPS:NEPS}=require("../../maths/constants"),reTesselateCoplanarPolygons=require("./reTesselateCoplanarPolygons"),retessellate=e=>{if(e.isRetesselated)return e;const s=geom3.toPolygons(e).map((e,s)=>({vertices:e.vertices,plane:poly3.plane(e),index:s})),o=classifyPolygons(s),l=[];o.forEach(e=>{if(Array.isArray(e)){const s=reTesselateCoplanarPolygons(e);l.push(...s)}else l.push(e)});const n=geom3.create(l);return n.isRetesselated=!0,n},classifyPolygons=e=>{let s=[e];const o=[];for(let e=3;e>=0;e--){const l=[],n=3===e?1.5e-8:NEPS;s.forEach(s=>{s.sort(byPlaneComponent(e,n));let t=0;for(let r=1;r<s.length;r++)s[r].plane[e]-s[t].plane[e]>n&&(r-t==1?o.push(s[t]):l.push(s.slice(t,r)),t=r);s.length-t==1?o.push(s[t]):l.push(s.slice(t))}),s=l}const l=[];return s.forEach(e=>{e[0]&&(l[e[0].index]=e)}),o.forEach(e=>{l[e.index]=e}),l},byPlaneComponent=(e,s)=>(o,l)=>o.plane[e]-l.plane[e]>s?1:l.plane[e]-o.plane[e]>s?-1:0;module.exports=retessellate;
1067
+ const geom3=require("../../geometries/geom3"),poly3=require("../../geometries/poly3"),{NEPS:NEPS}=require("../../maths/constants"),reTesselateCoplanarPolygons=require("./reTesselateCoplanarPolygons"),retessellate=e=>{if(e.isRetesselated)return e;const s=geom3.toPolygons(e).map((e,s)=>({vertices:e.vertices,plane:poly3.plane(e),index:s})),o=classifyPolygons(s),l=[];o.forEach(e=>{if(Array.isArray(e)){const s=reTesselateCoplanarPolygons(e);for(let e=0;e<s.length;e++)l.push(s[e])}else l.push(e)});const t=geom3.create(l);return t.isRetesselated=!0,t},classifyPolygons=e=>{let s=[e];const o=[];for(let e=3;e>=0;e--){const l=[],t=3===e?1.5e-8:NEPS;s.forEach(s=>{s.sort(byPlaneComponent(e,t));let n=0;for(let r=1;r<s.length;r++)s[r].plane[e]-s[n].plane[e]>t&&(r-n==1?o.push(s[n]):l.push(s.slice(n,r)),n=r);s.length-n==1?o.push(s[n]):l.push(s.slice(n))}),s=l}const l=[];return s.forEach(e=>{e[0]&&(l[e[0].index]=e)}),o.forEach(e=>{l[e.index]=e}),l},byPlaneComponent=(e,s)=>(o,l)=>o.plane[e]-l.plane[e]>s?1:l.plane[e]-o.plane[e]>s?-1:0;module.exports=retessellate;
1068
1068
 
1069
1069
  },{"../../geometries/geom3":41,"../../geometries/poly3":79,"../../maths/constants":94,"./reTesselateCoplanarPolygons":355}],357:[function(require,module,exports){
1070
1070
  const flatten=require("../../utils/flatten"),vec2=require("../../maths/vec2"),geom2=require("../../geometries/geom2"),geom3=require("../../geometries/geom3"),path2=require("../../geometries/path2"),measureEpsilon=require("../../measurements/measureEpsilon"),snapPolygons=require("./snapPolygons"),snapPath2=e=>{const s=measureEpsilon(e),r=path2.toPoints(e).map(e=>vec2.snap(vec2.create(),e,s));return path2.create(r)},snapGeom2=e=>{const s=measureEpsilon(e);let r=geom2.toSides(e).map(e=>[vec2.snap(vec2.create(),e[0],s),vec2.snap(vec2.create(),e[1],s)]);return r=r.filter(e=>!vec2.equals(e[0],e[1])),geom2.create(r)},snapGeom3=e=>{const s=measureEpsilon(e),r=geom3.toPolygons(e),o=snapPolygons(s,r);return geom3.create(o)},snap=(...e)=>{if(0===(e=flatten(e)).length)throw new Error("wrong number of arguments");const s=e.map(e=>path2.isA(e)?snapPath2(e):geom2.isA(e)?snapGeom2(e):geom3.isA(e)?snapGeom3(e):e);return 1===s.length?s[0]:s};module.exports=snap;
@@ -1190,7 +1190,7 @@ const geom2=require("../geometries/geom2"),geom3=require("../geometries/geom3"),
1190
1190
  const degToRad=d=>.017453292519943295*d;module.exports=degToRad;
1191
1191
 
1192
1192
  },{}],398:[function(require,module,exports){
1193
- const flatten=t=>t.reduce((t,a)=>Array.isArray(a)?t.concat(flatten(a)):t.concat(a),[]);module.exports=flatten;
1193
+ const flatten=t=>t.flat(1/0);module.exports=flatten;
1194
1194
 
1195
1195
  },{}],399:[function(require,module,exports){
1196
1196
  const fnNumberSort=(o,r)=>o-r;module.exports=fnNumberSort;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscad/modeling",
3
- "version": "2.12.6",
3
+ "version": "2.12.7",
4
4
  "description": "Constructive Solid Geometry (CSG) Library for JSCAD",
5
5
  "homepage": "https://openjscad.xyz/",
6
6
  "repository": "https://github.com/jscad/OpenJSCAD.org",
@@ -61,5 +61,5 @@
61
61
  "nyc": "15.1.0",
62
62
  "uglifyify": "5.0.2"
63
63
  },
64
- "gitHead": "11a9a2d9235d804360116f776076c9f3237a3eb0"
64
+ "gitHead": "138ee568542545f27629166bd93fff653cc3c26d"
65
65
  }
@@ -49,11 +49,11 @@ class PolygonTreeNode {
49
49
  this.removed = true
50
50
  this.polygon = null
51
51
 
52
- // remove ourselves from the parent's children list:
53
- const parentschildren = this.parent.children
54
- const i = parentschildren.indexOf(this)
55
- if (i < 0) throw new Error('Assertion failed')
56
- parentschildren.splice(i, 1)
52
+ // Note: We intentionally do NOT splice from parent.children here.
53
+ // All iteration paths (getPolygons, splitByPlane, clipPolygons) already
54
+ // check isRemoved() or polygon !== null, so removed nodes are skipped.
55
+ // Avoiding splice eliminates O(n²) cost when many nodes are removed.
56
+ // Dead nodes are cleaned up lazily in getPolygons().
57
57
 
58
58
  // invalidate the parent's polygon, and of all parents above it:
59
59
  this.parent.recursivelyInvalidatePolygon()
@@ -80,6 +80,19 @@ class PolygonTreeNode {
80
80
  }
81
81
 
82
82
  getPolygons (result) {
83
+ // Compact root's children array to remove dead nodes (lazy cleanup from remove()).
84
+ // Note: This method is only called on the root node via Tree.allPolygons() at the
85
+ // end of boolean operations. The children array is internal and not exposed, so
86
+ // mutating it here is safe. Non-root nodes are traversed via the queue below,
87
+ // which skips removed nodes via the `if (node.polygon)` check.
88
+ if (this.isRootNode() && this.children.length > 0) {
89
+ const compacted = []
90
+ for (let i = 0; i < this.children.length; i++) {
91
+ if (!this.children[i].removed) compacted.push(this.children[i])
92
+ }
93
+ this.children = compacted
94
+ }
95
+
83
96
  let children = [this]
84
97
  const queue = [children]
85
98
  let i, j, l, node
@@ -7,6 +7,25 @@ const poly3 = require('../../../geometries/poly3')
7
7
 
8
8
  const splitLineSegmentByPlane = require('./splitLineSegmentByPlane')
9
9
 
10
+ const EPS_SQUARED = EPS * EPS
11
+
12
+ // Remove consecutive duplicate vertices from a polygon vertex list.
13
+ // Compares last vertex to first to handle wraparound.
14
+ // Returns a new array (does not modify input).
15
+ // IMPORTANT: Caller must ensure vertices.length >= 3 before calling.
16
+ const removeConsecutiveDuplicates = (vertices) => {
17
+ const result = []
18
+ let prevvertex = vertices[vertices.length - 1]
19
+ for (let i = 0; i < vertices.length; i++) {
20
+ const vertex = vertices[i]
21
+ if (vec3.squaredDistance(vertex, prevvertex) >= EPS_SQUARED) {
22
+ result.push(vertex)
23
+ }
24
+ prevvertex = vertex
25
+ }
26
+ return result
27
+ }
28
+
10
29
  // Returns object:
11
30
  // .type:
12
31
  // 0: coplanar-front
@@ -83,35 +102,18 @@ const splitPolygonByPlane = (splane, polygon) => {
83
102
  }
84
103
  isback = nextisback
85
104
  } // for vertexindex
86
- // remove duplicate vertices:
87
- const EPS_SQUARED = EPS * EPS
88
- if (backvertices.length >= 3) {
89
- let prevvertex = backvertices[backvertices.length - 1]
90
- for (let vertexindex = 0; vertexindex < backvertices.length; vertexindex++) {
91
- const vertex = backvertices[vertexindex]
92
- if (vec3.squaredDistance(vertex, prevvertex) < EPS_SQUARED) {
93
- backvertices.splice(vertexindex, 1)
94
- vertexindex--
95
- }
96
- prevvertex = vertex
97
- }
98
- }
105
+ // remove consecutive duplicate vertices (check length before calling to avoid function overhead)
99
106
  if (frontvertices.length >= 3) {
100
- let prevvertex = frontvertices[frontvertices.length - 1]
101
- for (let vertexindex = 0; vertexindex < frontvertices.length; vertexindex++) {
102
- const vertex = frontvertices[vertexindex]
103
- if (vec3.squaredDistance(vertex, prevvertex) < EPS_SQUARED) {
104
- frontvertices.splice(vertexindex, 1)
105
- vertexindex--
106
- }
107
- prevvertex = vertex
107
+ const frontFiltered = removeConsecutiveDuplicates(frontvertices)
108
+ if (frontFiltered.length >= 3) {
109
+ result.front = poly3.fromPointsAndPlane(frontFiltered, pplane)
108
110
  }
109
111
  }
110
- if (frontvertices.length >= 3) {
111
- result.front = poly3.fromPointsAndPlane(frontvertices, pplane)
112
- }
113
112
  if (backvertices.length >= 3) {
114
- result.back = poly3.fromPointsAndPlane(backvertices, pplane)
113
+ const backFiltered = removeConsecutiveDuplicates(backvertices)
114
+ if (backFiltered.length >= 3) {
115
+ result.back = poly3.fromPointsAndPlane(backFiltered, pplane)
116
+ }
115
117
  }
116
118
  }
117
119
  }
@@ -0,0 +1,132 @@
1
+ const test = require('ava')
2
+
3
+ const { poly3 } = require('../../../geometries')
4
+ const plane = require('../../../maths/plane')
5
+
6
+ const splitPolygonByPlane = require('./splitPolygonByPlane')
7
+
8
+ test('splitPolygonByPlane: test coplanar-front polygon returns type 0.', (t) => {
9
+ // Polygon in XY plane at z=0
10
+ const polygon = poly3.create([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]])
11
+ // Plane is also XY plane at z=0, normal pointing up
12
+ const splane = plane.fromPoints(plane.create(), [0, 0, 0], [1, 0, 0], [1, 1, 0])
13
+
14
+ const result = splitPolygonByPlane(splane, polygon)
15
+ t.is(result.type, 0) // coplanar-front
16
+ t.is(result.front, null)
17
+ t.is(result.back, null)
18
+ })
19
+
20
+ test('splitPolygonByPlane: test polygon entirely in front returns type 2.', (t) => {
21
+ // Polygon at z=5
22
+ const polygon = poly3.create([[0, 0, 5], [1, 0, 5], [1, 1, 5], [0, 1, 5]])
23
+ // Plane at z=0
24
+ const splane = [0, 0, 1, 0] // normal (0,0,1), w=0
25
+
26
+ const result = splitPolygonByPlane(splane, polygon)
27
+ t.is(result.type, 2) // front
28
+ t.is(result.front, null)
29
+ t.is(result.back, null)
30
+ })
31
+
32
+ test('splitPolygonByPlane: test polygon entirely in back returns type 3.', (t) => {
33
+ // Polygon at z=-5
34
+ const polygon = poly3.create([[0, 0, -5], [1, 0, -5], [1, 1, -5], [0, 1, -5]])
35
+ // Plane at z=0
36
+ const splane = [0, 0, 1, 0] // normal (0,0,1), w=0
37
+
38
+ const result = splitPolygonByPlane(splane, polygon)
39
+ t.is(result.type, 3) // back
40
+ t.is(result.front, null)
41
+ t.is(result.back, null)
42
+ })
43
+
44
+ test('splitPolygonByPlane: test spanning polygon returns type 4 with front and back.', (t) => {
45
+ // Polygon spanning z=0 plane (from z=-1 to z=1)
46
+ const polygon = poly3.create([[0, 0, -1], [1, 0, -1], [1, 0, 1], [0, 0, 1]])
47
+ // Plane at z=0
48
+ const splane = [0, 0, 1, 0] // normal (0,0,1), w=0
49
+
50
+ const result = splitPolygonByPlane(splane, polygon)
51
+ t.is(result.type, 4) // spanning
52
+ t.not(result.front, null)
53
+ t.not(result.back, null)
54
+
55
+ // Front polygon should have z >= 0
56
+ const frontPoints = poly3.toPoints(result.front)
57
+ t.true(frontPoints.length >= 3)
58
+ frontPoints.forEach((p) => {
59
+ t.true(p[2] >= -1e-5, `front point z=${p[2]} should be >= 0`)
60
+ })
61
+
62
+ // Back polygon should have z <= 0
63
+ const backPoints = poly3.toPoints(result.back)
64
+ t.true(backPoints.length >= 3)
65
+ backPoints.forEach((p) => {
66
+ t.true(p[2] <= 1e-5, `back point z=${p[2]} should be <= 0`)
67
+ })
68
+ })
69
+
70
+ test('splitPolygonByPlane: test duplicate vertices are removed from split result.', (t) => {
71
+ // Create a polygon that when split would produce duplicate vertices
72
+ // Triangle with one vertex on the plane
73
+ const polygon = poly3.create([[0, 0, 0], [1, 0, 1], [1, 0, -1]])
74
+ // Plane at z=0
75
+ const splane = [0, 0, 1, 0]
76
+
77
+ const result = splitPolygonByPlane(splane, polygon)
78
+ t.is(result.type, 4) // spanning
79
+
80
+ // Verify no consecutive duplicate vertices in front
81
+ if (result.front) {
82
+ const frontPoints = poly3.toPoints(result.front)
83
+ for (let i = 0; i < frontPoints.length; i++) {
84
+ const curr = frontPoints[i]
85
+ const next = frontPoints[(i + 1) % frontPoints.length]
86
+ const dx = curr[0] - next[0]
87
+ const dy = curr[1] - next[1]
88
+ const dz = curr[2] - next[2]
89
+ const distSq = dx * dx + dy * dy + dz * dz
90
+ t.true(distSq > 1e-10, 'front polygon should not have duplicate consecutive vertices')
91
+ }
92
+ }
93
+
94
+ // Verify no consecutive duplicate vertices in back
95
+ if (result.back) {
96
+ const backPoints = poly3.toPoints(result.back)
97
+ for (let i = 0; i < backPoints.length; i++) {
98
+ const curr = backPoints[i]
99
+ const next = backPoints[(i + 1) % backPoints.length]
100
+ const dx = curr[0] - next[0]
101
+ const dy = curr[1] - next[1]
102
+ const dz = curr[2] - next[2]
103
+ const distSq = dx * dx + dy * dy + dz * dz
104
+ t.true(distSq > 1e-10, 'back polygon should not have duplicate consecutive vertices')
105
+ }
106
+ }
107
+ })
108
+
109
+ test('splitPolygonByPlane: test complex spanning polygon splits correctly.', (t) => {
110
+ // Hexagon spanning the XY plane
111
+ const polygon = poly3.create([
112
+ [1, 0, -1],
113
+ [0.5, 0.866, -1],
114
+ [-0.5, 0.866, 1],
115
+ [-1, 0, 1],
116
+ [-0.5, -0.866, 1],
117
+ [0.5, -0.866, -1]
118
+ ])
119
+ // Plane at z=0
120
+ const splane = [0, 0, 1, 0]
121
+
122
+ const result = splitPolygonByPlane(splane, polygon)
123
+ t.is(result.type, 4) // spanning
124
+ t.not(result.front, null)
125
+ t.not(result.back, null)
126
+
127
+ // Both resulting polygons should be valid (at least 3 vertices)
128
+ const frontPoints = poly3.toPoints(result.front)
129
+ const backPoints = poly3.toPoints(result.back)
130
+ t.true(frontPoints.length >= 3, 'front polygon should have at least 3 vertices')
131
+ t.true(backPoints.length >= 3, 'back polygon should have at least 3 vertices')
132
+ })