@jbroll/jscad-modeling 2.12.7 → 2.13.0

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.
Files changed (36) hide show
  1. package/bench/booleans.bench.js +103 -0
  2. package/bench/primitives.bench.js +108 -0
  3. package/bench/splitPolygon.bench.js +143 -0
  4. package/benchmarks/compare.js +673 -0
  5. package/benchmarks/memory-test.js +103 -0
  6. package/benchmarks/primitives.bench.js +83 -0
  7. package/benchmarks/run.js +37 -0
  8. package/benchmarks/workflows.bench.js +105 -0
  9. package/dist/jscad-modeling.min.js +116 -107
  10. package/isolate-0x1e680000-4181-v8.log +6077 -0
  11. package/package.json +2 -1
  12. package/src/geometries/poly3/create.js +5 -1
  13. package/src/geometries/poly3/create.test.js +1 -1
  14. package/src/geometries/poly3/fromPointsAndPlane.js +2 -3
  15. package/src/geometries/poly3/invert.js +7 -1
  16. package/src/geometries/poly3/measureBoundingSphere.js +9 -7
  17. package/src/index.d.ts +1 -0
  18. package/src/index.js +1 -0
  19. package/src/operations/booleans/trees/PolygonTreeNode.js +14 -5
  20. package/src/operations/booleans/trees/splitPolygonByPlane.js +64 -29
  21. package/src/operations/booleans/unionGeom3.test.js +35 -0
  22. package/src/operations/extrusions/extrudeRotate.js +4 -1
  23. package/src/operations/extrusions/extrudeRotate.test.js +33 -0
  24. package/src/operations/extrusions/extrudeWalls.js +2 -1
  25. package/src/operations/extrusions/extrudeWalls.test.js +72 -0
  26. package/src/operations/minkowski/index.d.ts +2 -0
  27. package/src/operations/minkowski/index.js +18 -0
  28. package/src/operations/minkowski/isConvex.d.ts +5 -0
  29. package/src/operations/minkowski/isConvex.js +67 -0
  30. package/src/operations/minkowski/isConvex.test.js +48 -0
  31. package/src/operations/minkowski/minkowskiSum.d.ts +6 -0
  32. package/src/operations/minkowski/minkowskiSum.js +223 -0
  33. package/src/operations/minkowski/minkowskiSum.test.js +161 -0
  34. package/src/operations/modifiers/reTesselateCoplanarPolygons.js +16 -13
  35. package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +36 -1
  36. package/src/operations/modifiers/retessellate.js +5 -2
@@ -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')
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Direct benchmark for splitPolygonByPlane - the hot path in boolean operations
3
+ *
4
+ * Run with: node --expose-gc bench/splitPolygon.bench.js
5
+ */
6
+
7
+ const { sphere, torus } = require('../src/primitives')
8
+ const splitPolygonByPlane = require('../src/operations/booleans/trees/splitPolygonByPlane')
9
+ const poly3 = require('../src/geometries/poly3')
10
+ const plane = require('../src/maths/plane')
11
+
12
+ // Benchmark helper
13
+ const benchmark = (name, setup, fn, iterations = 1000) => {
14
+ const data = setup()
15
+
16
+ // Force GC if available
17
+ if (global.gc) global.gc()
18
+
19
+ // Warmup
20
+ for (let i = 0; i < 100; i++) fn(data)
21
+
22
+ if (global.gc) global.gc()
23
+ const heapBefore = process.memoryUsage().heapUsed
24
+
25
+ const start = process.hrtime.bigint()
26
+ for (let i = 0; i < iterations; i++) {
27
+ fn(data)
28
+ }
29
+ const end = process.hrtime.bigint()
30
+
31
+ if (global.gc) global.gc()
32
+ const heapAfter = process.memoryUsage().heapUsed
33
+
34
+ const totalNs = Number(end - start)
35
+ const avgNs = totalNs / iterations
36
+ const avgUs = avgNs / 1000
37
+ const heapDelta = (heapAfter - heapBefore) / 1024
38
+
39
+ console.log(`${name.padEnd(50)} ${avgUs.toFixed(2).padStart(8)} µs/op heap: ${heapDelta > 0 ? '+' : ''}${heapDelta.toFixed(0)} KB`)
40
+ return avgUs
41
+ }
42
+
43
+ console.log('='.repeat(80))
44
+ console.log('splitPolygonByPlane Direct Benchmark')
45
+ console.log('='.repeat(80))
46
+ console.log()
47
+
48
+ // Get polygons from a sphere
49
+ const testSphere = sphere({ radius: 5, segments: 32 })
50
+ const polygons = testSphere.polygons
51
+
52
+ console.log(`Test geometry: sphere(32) with ${polygons.length} polygons`)
53
+ console.log(`Average vertices per polygon: ${(polygons.reduce((sum, p) => sum + p.vertices.length, 0) / polygons.length).toFixed(1)}`)
54
+ console.log()
55
+
56
+ // Test 1: Coplanar case (fast path - type 0 or 1)
57
+ console.log('--- Coplanar Cases (fast path) ---')
58
+ benchmark('coplanar-front (type 0)', () => {
59
+ const polygon = polygons[0]
60
+ const pplane = poly3.plane(polygon)
61
+ return { polygon, splane: pplane }
62
+ }, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)
63
+
64
+ // Test 2: Entirely front (type 2)
65
+ console.log()
66
+ console.log('--- One-side Cases (no split needed) ---')
67
+ benchmark('entirely front (type 2)', () => {
68
+ const polygon = polygons[0]
69
+ // Create a plane far behind the polygon
70
+ const splane = [0, 0, 1, -100]
71
+ return { polygon, splane }
72
+ }, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)
73
+
74
+ benchmark('entirely back (type 3)', () => {
75
+ const polygon = polygons[0]
76
+ // Create a plane far in front of the polygon
77
+ const splane = [0, 0, 1, 100]
78
+ return { polygon, splane }
79
+ }, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)
80
+
81
+ // Test 3: Spanning case (type 4) - this is where allocations hurt
82
+ console.log()
83
+ console.log('--- Spanning Cases (allocations happen here) ---')
84
+ benchmark('spanning split (type 4) - triangle', () => {
85
+ // A triangle that will be split
86
+ const polygon = poly3.create([[0, 0, 0], [10, 0, 0], [5, 10, 0]])
87
+ const splane = [1, 0, 0, 5] // Split down the middle
88
+ return { polygon, splane }
89
+ }, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)
90
+
91
+ benchmark('spanning split (type 4) - quad', () => {
92
+ const polygon = poly3.create([[0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]])
93
+ const splane = [1, 0, 0, 5]
94
+ return { polygon, splane }
95
+ }, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)
96
+
97
+ benchmark('spanning split (type 4) - hexagon', () => {
98
+ const polygon = poly3.create([
99
+ [0, 0, 0], [5, 0, 0], [7.5, 4, 0],
100
+ [5, 8, 0], [0, 8, 0], [-2.5, 4, 0]
101
+ ])
102
+ const splane = [1, 0, 0, 2.5]
103
+ return { polygon, splane }
104
+ }, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)
105
+
106
+ // Test 4: Realistic mix from actual boolean operation
107
+ console.log()
108
+ console.log('--- Realistic Mix (simulating boolean op) ---')
109
+ benchmark('mixed types from sphere polygons', () => {
110
+ // Use multiple polygons with a plane that hits various cases
111
+ const splane = [0.577, 0.577, 0.577, 0] // Diagonal plane through origin
112
+ return { polygons: polygons.slice(0, 50), splane }
113
+ }, (data) => {
114
+ let spanning = 0
115
+ for (const polygon of data.polygons) {
116
+ const result = splitPolygonByPlane(data.splane, polygon)
117
+ if (result.type === 4) spanning++
118
+ }
119
+ return spanning
120
+ }, 1000)
121
+
122
+ // Measure allocation overhead specifically
123
+ console.log()
124
+ console.log('--- Allocation Stress Test ---')
125
+ benchmark('10k spanning splits (allocation heavy)', () => {
126
+ const polygon = poly3.create([[0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]])
127
+ const splane = [1, 0, 0, 5]
128
+ return { polygon, splane }
129
+ }, (data) => {
130
+ for (let i = 0; i < 100; i++) {
131
+ splitPolygonByPlane(data.splane, data.polygon)
132
+ }
133
+ }, 100)
134
+
135
+ console.log()
136
+ console.log('='.repeat(80))
137
+ console.log('Benchmark complete')
138
+ console.log()
139
+ console.log('Note: "spanning split" cases allocate the most:')
140
+ console.log(' - result object { type, front, back }')
141
+ console.log(' - vertexIsBack[] array')
142
+ console.log(' - frontvertices[] and backvertices[] arrays')
143
+ console.log(' - Two new poly3 objects (front and back)')