@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
+ * Memory impact test for PolygonTreeNode optimization
3
+ *
4
+ * Measures heap usage before and after boolean operations
5
+ */
6
+
7
+ const { primitives, booleans } = require('../src')
8
+ const { sphere, cube } = primitives
9
+ const { union, subtract, intersect } = booleans
10
+
11
+ // Force GC if available
12
+ const gc = global.gc || (() => {})
13
+
14
+ const formatBytes = (bytes) => {
15
+ if (bytes < 1024) return `${bytes} B`
16
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
17
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`
18
+ }
19
+
20
+ const measureMemory = (label, fn, iterations = 5) => {
21
+ gc()
22
+ const before = process.memoryUsage()
23
+
24
+ const results = []
25
+ for (let i = 0; i < iterations; i++) {
26
+ results.push(fn())
27
+ }
28
+
29
+ gc()
30
+ const after = process.memoryUsage()
31
+
32
+ const heapDiff = after.heapUsed - before.heapUsed
33
+ const externalDiff = after.external - before.external
34
+
35
+ console.log(`${label}:`)
36
+ console.log(` Heap used: ${formatBytes(before.heapUsed)} -> ${formatBytes(after.heapUsed)} (${heapDiff >= 0 ? '+' : ''}${formatBytes(heapDiff)})`)
37
+ console.log(` Iterations: ${iterations}, Results retained: ${results.length}`)
38
+
39
+ // Return results to prevent GC from collecting them during measurement
40
+ return { heapDiff, results }
41
+ }
42
+
43
+ const runMemoryTests = () => {
44
+ console.log('Memory Impact Test')
45
+ console.log('==================')
46
+ console.log(`Node ${process.version}, GC available: ${!!global.gc}`)
47
+ if (!global.gc) {
48
+ console.log('Run with: node --expose-gc benchmarks/memory-test.js')
49
+ }
50
+ console.log()
51
+
52
+ // Test 1: Union of spheres (many polygons clipped)
53
+ console.log('--- Union Operations (high clip count) ---')
54
+ const s32a = sphere({ segments: 32 })
55
+ const s32b = sphere({ segments: 32, center: [0.5, 0, 0] })
56
+
57
+ measureMemory('union-sphere-32 x5', () => union(s32a, s32b), 5)
58
+ console.log()
59
+
60
+ // Test 2: Larger spheres
61
+ const s64a = sphere({ segments: 64 })
62
+ const s64b = sphere({ segments: 64, center: [0.5, 0, 0] })
63
+
64
+ measureMemory('union-sphere-64 x3', () => union(s64a, s64b), 3)
65
+ console.log()
66
+
67
+ // Test 3: Subtract (fewer clips)
68
+ console.log('--- Subtract Operations (moderate clip count) ---')
69
+ measureMemory('subtract-sphere-32 x5', () => subtract(s32a, s32b), 5)
70
+ console.log()
71
+
72
+ // Test 4: Chain of operations (accumulating dead nodes)
73
+ console.log('--- Chained Operations (accumulating) ---')
74
+ measureMemory('union-chain-10-cubes', () => {
75
+ let result = cube({ size: 2 })
76
+ for (let i = 1; i < 10; i++) {
77
+ result = union(result, cube({ size: 2, center: [i * 1.5, 0, 0] }))
78
+ }
79
+ return result
80
+ }, 3)
81
+ console.log()
82
+
83
+ // Test 5: Many small operations
84
+ console.log('--- Many Small Operations ---')
85
+ measureMemory('union-25-cubes-array', () => {
86
+ const cubes = []
87
+ for (let i = 0; i < 25; i++) {
88
+ cubes.push(cube({ size: 1, center: [i * 1.2, 0, 0] }))
89
+ }
90
+ return union(cubes)
91
+ }, 3)
92
+ console.log()
93
+
94
+ // Test 6: Measure polygon counts
95
+ console.log('--- Result Polygon Counts ---')
96
+ const unionResult = union(s32a, s32b)
97
+ const subtractResult = subtract(s32a, s32b)
98
+ console.log(` sphere-32 polygons: ${s32a.polygons.length}`)
99
+ console.log(` union result polygons: ${unionResult.polygons.length}`)
100
+ console.log(` subtract result polygons: ${subtractResult.polygons.length}`)
101
+ }
102
+
103
+ runMemoryTests()
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Primitive shape benchmarks
3
+ *
4
+ * Tests sphere, cylinder, torus creation - the key primitives
5
+ * that involve trigonometry and significant vertex generation.
6
+ */
7
+
8
+ const { primitives } = require('../src')
9
+ const { cube, sphere, cylinder, cylinderElliptic, torus, geodesicSphere, roundedCuboid, roundedCylinder } = primitives
10
+
11
+ const name = 'Primitives'
12
+
13
+ const benchmarks = [
14
+ // Cubes (baseline - very fast)
15
+ {
16
+ name: 'cube-default',
17
+ fn: () => cube()
18
+ },
19
+ {
20
+ name: 'roundedCuboid-default',
21
+ fn: () => roundedCuboid()
22
+ },
23
+
24
+ // Spheres - key optimization target (trig + many vertices)
25
+ {
26
+ name: 'sphere-16-segments',
27
+ fn: () => sphere({ segments: 16 })
28
+ },
29
+ {
30
+ name: 'sphere-32-segments',
31
+ fn: () => sphere({ segments: 32 })
32
+ },
33
+ {
34
+ name: 'sphere-64-segments',
35
+ fn: () => sphere({ segments: 64 })
36
+ },
37
+ {
38
+ name: 'sphere-128-segments',
39
+ fn: () => sphere({ segments: 128 })
40
+ },
41
+ {
42
+ name: 'geodesicSphere-frequency-6',
43
+ fn: () => geodesicSphere({ frequency: 6 })
44
+ },
45
+
46
+ // Cylinders - common primitive
47
+ {
48
+ name: 'cylinder-16-segments',
49
+ fn: () => cylinder({ segments: 16 })
50
+ },
51
+ {
52
+ name: 'cylinder-32-segments',
53
+ fn: () => cylinder({ segments: 32 })
54
+ },
55
+ {
56
+ name: 'cylinder-64-segments',
57
+ fn: () => cylinder({ segments: 64 })
58
+ },
59
+ {
60
+ name: 'cylinderElliptic-32-segments',
61
+ fn: () => cylinderElliptic({ segments: 32 })
62
+ },
63
+ {
64
+ name: 'roundedCylinder-32-segments',
65
+ fn: () => roundedCylinder({ segments: 32 })
66
+ },
67
+
68
+ // Torus - uses extrudeRotate internally
69
+ {
70
+ name: 'torus-16x16',
71
+ fn: () => torus({ innerSegments: 16, outerSegments: 16 })
72
+ },
73
+ {
74
+ name: 'torus-32x32',
75
+ fn: () => torus({ innerSegments: 32, outerSegments: 32 })
76
+ },
77
+ {
78
+ name: 'torus-64x64',
79
+ fn: () => torus({ innerSegments: 64, outerSegments: 64 })
80
+ }
81
+ ]
82
+
83
+ module.exports = { name, benchmarks }
package/benchmarks/run.js CHANGED
@@ -85,12 +85,46 @@ const runSuite = (name, benchmarks) => {
85
85
  return results
86
86
  }
87
87
 
88
+ /**
89
+ * Generate timestamp for output file
90
+ */
91
+ const getTimestamp = () => {
92
+ const now = new Date()
93
+ return now.toISOString().replace(/[:.]/g, '-').slice(0, 19)
94
+ }
95
+
96
+ /**
97
+ * Save results to JSON file
98
+ */
99
+ const saveResults = (allResults, outputDir) => {
100
+ const timestamp = getTimestamp()
101
+ const outputFile = path.join(outputDir, `bench-${timestamp}.json`)
102
+
103
+ const output = {
104
+ timestamp: new Date().toISOString(),
105
+ node: process.version,
106
+ runs: BENCHMARK_RUNS,
107
+ warmup: WARMUP_RUNS,
108
+ results: allResults
109
+ }
110
+
111
+ fs.writeFileSync(outputFile, JSON.stringify(output, null, 2))
112
+ console.log(`Results saved to: ${outputFile}`)
113
+ return outputFile
114
+ }
115
+
88
116
  /**
89
117
  * Load and run benchmark files
90
118
  */
91
119
  const main = () => {
92
120
  const args = process.argv.slice(2)
93
121
  const benchDir = __dirname
122
+ const outputDir = path.join(benchDir, 'results')
123
+
124
+ // Ensure results directory exists
125
+ if (!fs.existsSync(outputDir)) {
126
+ fs.mkdirSync(outputDir, { recursive: true })
127
+ }
94
128
 
95
129
  console.log('@jscad/modeling benchmarks')
96
130
  console.log(`Runs: ${BENCHMARK_RUNS} (+ ${WARMUP_RUNS} warmup)`)
@@ -134,6 +168,9 @@ const main = () => {
134
168
 
135
169
  console.log('\n' + '═'.repeat(60))
136
170
  console.log('Benchmark complete')
171
+
172
+ // Save results to timestamped file
173
+ saveResults(allResults, outputDir)
137
174
  }
138
175
 
139
176
  // Export for programmatic use
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Typical workflow benchmarks
3
+ *
4
+ * Tests common modeling patterns that users actually do:
5
+ * - Create primitive, boolean op, transform
6
+ * - Multiple booleans in sequence
7
+ * - Mixed geometry types (cube vs cylinder)
8
+ */
9
+
10
+ const { primitives, booleans, transforms } = require('../src')
11
+ const { cube, sphere, cylinder, torus } = primitives
12
+ const { union, subtract, intersect } = booleans
13
+ const { translate, rotate, scale } = transforms
14
+
15
+ const name = 'Workflows'
16
+
17
+ // Pre-create some geometries for fair comparison
18
+ const cube10 = cube({ size: 10 })
19
+ const cyl16 = cylinder({ radius: 3, height: 15, segments: 16 })
20
+ const cyl32 = cylinder({ radius: 3, height: 15, segments: 32 })
21
+ const sphere16 = sphere({ radius: 5, segments: 16 })
22
+ const sphere32 = sphere({ radius: 5, segments: 32 })
23
+
24
+ const benchmarks = [
25
+ // Classic workflow: cube with hole
26
+ {
27
+ name: 'workflow-cube-subtract-cylinder-16',
28
+ fn: () => subtract(cube({ size: 10 }), cylinder({ radius: 3, height: 15, segments: 16 }))
29
+ },
30
+ {
31
+ name: 'workflow-cube-subtract-cylinder-32',
32
+ fn: () => subtract(cube({ size: 10 }), cylinder({ radius: 3, height: 15, segments: 32 }))
33
+ },
34
+
35
+ // Pre-created geometry (isolates boolean cost)
36
+ {
37
+ name: 'workflow-precreated-cube-subtract-cyl16',
38
+ fn: () => subtract(cube10, cyl16)
39
+ },
40
+ {
41
+ name: 'workflow-precreated-cube-subtract-cyl32',
42
+ fn: () => subtract(cube10, cyl32)
43
+ },
44
+
45
+ // Full pipeline: create, boolean, transform
46
+ {
47
+ name: 'workflow-full-cube-subtract-cyl-rotate',
48
+ fn: () => rotate([0, Math.PI / 4, 0], subtract(cube({ size: 10 }), cylinder({ radius: 3, height: 15, segments: 16 })))
49
+ },
50
+ {
51
+ name: 'workflow-full-cube-subtract-cyl-translate',
52
+ fn: () => translate([10, 0, 0], subtract(cube({ size: 10 }), cylinder({ radius: 3, height: 15, segments: 16 })))
53
+ },
54
+
55
+ // Multiple holes (common pattern)
56
+ {
57
+ name: 'workflow-cube-3-holes',
58
+ fn: () => {
59
+ const box = cube({ size: 10 })
60
+ const hole1 = cylinder({ radius: 1.5, height: 15, segments: 16 })
61
+ const hole2 = translate([3, 0, 0], hole1)
62
+ const hole3 = translate([-3, 0, 0], hole1)
63
+ return subtract(box, hole1, hole2, hole3)
64
+ }
65
+ },
66
+
67
+ // Union of shapes (assembly pattern)
68
+ {
69
+ name: 'workflow-union-cube-sphere-16',
70
+ fn: () => union(cube({ size: 8 }), translate([0, 0, 6], sphere({ radius: 5, segments: 16 })))
71
+ },
72
+ {
73
+ name: 'workflow-union-cube-sphere-32',
74
+ fn: () => union(cube({ size: 8 }), translate([0, 0, 6], sphere({ radius: 5, segments: 32 })))
75
+ },
76
+
77
+ // Intersect (less common but important)
78
+ {
79
+ name: 'workflow-intersect-cube-sphere-16',
80
+ fn: () => intersect(cube({ size: 10 }), sphere({ radius: 7, segments: 16 }))
81
+ },
82
+ {
83
+ name: 'workflow-intersect-cube-sphere-32',
84
+ fn: () => intersect(cube({ size: 10 }), sphere({ radius: 7, segments: 32 }))
85
+ },
86
+
87
+ // Chained operations
88
+ {
89
+ name: 'workflow-chain-subtract-union',
90
+ fn: () => {
91
+ const base = cube({ size: 10 })
92
+ const hole = cylinder({ radius: 3, height: 15, segments: 16 })
93
+ const top = translate([0, 0, 5], sphere({ radius: 3, segments: 16 }))
94
+ return union(subtract(base, hole), top)
95
+ }
96
+ },
97
+
98
+ // Scaling after boolean (tests transform application)
99
+ {
100
+ name: 'workflow-subtract-then-scale',
101
+ fn: () => scale([2, 2, 2], subtract(cube({ size: 10 }), cylinder({ radius: 3, height: 15, segments: 16 })))
102
+ }
103
+ ]
104
+
105
+ module.exports = { name, benchmarks }