@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.
- package/CHANGELOG.md +6 -303
- package/bench/booleans.bench.js +103 -0
- package/bench/primitives.bench.js +108 -0
- package/dist/jscad-modeling.min.js +8 -8
- package/package.json +2 -2
- package/src/operations/booleans/trees/PolygonTreeNode.js +18 -5
- package/src/operations/booleans/trees/splitPolygonByPlane.js +27 -25
- package/src/operations/booleans/trees/splitPolygonByPlane.test.js +132 -0
- package/src/operations/booleans/unionGeom3.test.js +35 -0
- package/src/operations/extrusions/extrudeFromSlices.js +14 -4
- package/src/operations/extrusions/extrudeRotate.js +4 -1
- package/src/operations/extrusions/extrudeRotate.test.js +33 -0
- package/src/operations/extrusions/extrudeWalls.js +2 -1
- package/src/operations/extrusions/extrudeWalls.test.js +72 -0
- package/src/operations/modifiers/reTesselateCoplanarPolygons.js +8 -3
- package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +36 -1
- package/src/operations/modifiers/retessellate.js +5 -2
- package/src/utils/flatten.js +1 -1
- package/src/utils/flatten.test.js +94 -0
|
@@ -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(){
|
|
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
|
|
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:
|
|
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.
|
|
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=>{
|
|
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,
|
|
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(
|
|
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.
|
|
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.
|
|
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": "
|
|
64
|
+
"gitHead": "138ee568542545f27629166bd93fff653cc3c26d"
|
|
65
65
|
}
|
|
@@ -49,11 +49,11 @@ class PolygonTreeNode {
|
|
|
49
49
|
this.removed = true
|
|
50
50
|
this.polygon = null
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
+
})
|