@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.
- package/bench/booleans.bench.js +103 -0
- package/bench/primitives.bench.js +108 -0
- package/bench/splitPolygon.bench.js +143 -0
- package/benchmarks/compare.js +673 -0
- package/benchmarks/memory-test.js +103 -0
- package/benchmarks/primitives.bench.js +83 -0
- package/benchmarks/run.js +37 -0
- package/benchmarks/workflows.bench.js +105 -0
- package/dist/jscad-modeling.min.js +116 -107
- package/isolate-0x1e680000-4181-v8.log +6077 -0
- package/package.json +2 -1
- package/src/geometries/poly3/create.js +5 -1
- package/src/geometries/poly3/create.test.js +1 -1
- package/src/geometries/poly3/fromPointsAndPlane.js +2 -3
- package/src/geometries/poly3/invert.js +7 -1
- package/src/geometries/poly3/measureBoundingSphere.js +9 -7
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/operations/booleans/trees/PolygonTreeNode.js +14 -5
- package/src/operations/booleans/trees/splitPolygonByPlane.js +64 -29
- package/src/operations/booleans/unionGeom3.test.js +35 -0
- 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/minkowski/index.d.ts +2 -0
- package/src/operations/minkowski/index.js +18 -0
- package/src/operations/minkowski/isConvex.d.ts +5 -0
- package/src/operations/minkowski/isConvex.js +67 -0
- package/src/operations/minkowski/isConvex.test.js +48 -0
- package/src/operations/minkowski/minkowskiSum.d.ts +6 -0
- package/src/operations/minkowski/minkowskiSum.js +223 -0
- package/src/operations/minkowski/minkowskiSum.test.js +161 -0
- package/src/operations/modifiers/reTesselateCoplanarPolygons.js +16 -13
- package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +36 -1
- 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 }
|