@jbroll/jscad-modeling 2.12.8 → 2.13.1
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/.claude/settings.local.json +8 -12
- package/bench/splitPolygon.bench.js +143 -0
- package/benchmarks/compare.js +673 -0
- package/dist/jscad-modeling.min.js +110 -101
- 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/splitPolygonByPlane.js +64 -29
- package/src/operations/minkowski/index.d.ts +4 -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 +8 -10
|
@@ -3,28 +3,24 @@
|
|
|
3
3
|
"allow": [
|
|
4
4
|
"Bash(wc:*)",
|
|
5
5
|
"Bash(git add:*)",
|
|
6
|
-
"Bash(git commit
|
|
6
|
+
"Bash(git commit:*)",
|
|
7
|
+
"Bash(git checkout:*)",
|
|
8
|
+
"Bash(git push:*)",
|
|
7
9
|
"Bash(gh auth:*)",
|
|
8
|
-
"Bash(gh issue create --title \"perf\\(modeling\\): O\\(n²\\) array concatenation in extrudeFromSlices.js\" --body \"$\\(cat <<''EOF''\n## Summary\n`extrudeFromSlices.js` uses `Array.concat\\(\\)` inside a loop, causing O\\(n²\\) memory operations.\n\n## Usage Frequency: VERY HIGH\nThis function is the core of all extrusion operations:\n- `extrudeLinearGeom2.js`\n- `extrudeRotate.js` \n- `extrudeHelical.js`\n\nEvery extrusion operation goes through this code path.\n\n## Problem\n**File:** `packages/modeling/src/operations/extrusions/extrudeFromSlices.js`\n**Lines:** 84, 98, 102-103, 108\n\n```javascript\nfor \\(let s = 0; s < numberOfSlices; s++\\) {\n // ...\n if \\(prevSlice\\) {\n polygons = polygons.concat\\(extrudeWalls\\(prevSlice, currentSlice\\)\\) // Creates new array each iteration\n }\n}\n// ...\npolygons = polygons.concat\\(endPolygons\\)\npolygons = polygons.concat\\(startPolygons\\)\n```\n\nEach `concat\\(\\)` creates a new array and copies all existing elements. For N slices, this performs O\\(n²\\) copy operations.\n\n## Suggested Fix\nUse `push\\(\\)` with spread operator:\n\n```javascript\nfor \\(let s = 0; s < numberOfSlices; s++\\) {\n if \\(prevSlice\\) {\n polygons.push\\(...extrudeWalls\\(prevSlice, currentSlice\\)\\)\n }\n}\npolygons.push\\(...endPolygons\\)\npolygons.push\\(...startPolygons\\)\n```\n\n## Impact\nHigh - affects performance of all extrusion operations, especially with many slices.\nEOF\n\\)\")",
|
|
9
|
-
"Bash(gh issue create --repo jscad/OpenJSCAD.org --title \"perf\\(modeling\\): O\\(n²\\) array concatenation in extrudeFromSlices.js\" --body \"$\\(cat <<''EOF''\n## Summary\n`extrudeFromSlices.js` uses `Array.concat\\(\\)` inside a loop, causing O\\(n²\\) memory operations.\n\n## Usage Frequency: VERY HIGH\nThis function is the core of all extrusion operations:\n- `extrudeLinearGeom2.js`\n- `extrudeRotate.js` \n- `extrudeHelical.js`\n\nEvery extrusion operation goes through this code path.\n\n## Problem\n**File:** `packages/modeling/src/operations/extrusions/extrudeFromSlices.js` \n**Lines:** 84, 98, 102-103, 108\n\n```javascript\nfor \\(let s = 0; s < numberOfSlices; s++\\) {\n // ...\n if \\(prevSlice\\) {\n polygons = polygons.concat\\(extrudeWalls\\(prevSlice, currentSlice\\)\\) // Creates new array each iteration\n }\n}\n// ...\npolygons = polygons.concat\\(endPolygons\\)\npolygons = polygons.concat\\(startPolygons\\)\n```\n\nEach `concat\\(\\)` creates a new array and copies all existing elements. For N slices, this performs O\\(n²\\) copy operations.\n\n## Suggested Fix\nUse `push\\(\\)` with spread operator:\n\n```javascript\nfor \\(let s = 0; s < numberOfSlices; s++\\) {\n if \\(prevSlice\\) {\n polygons.push\\(...extrudeWalls\\(prevSlice, currentSlice\\)\\)\n }\n}\npolygons.push\\(...endPolygons\\)\npolygons.push\\(...startPolygons\\)\n```\n\n## Impact\nHigh - affects performance of all extrusion operations, especially with many slices.\nEOF\n\\)\")",
|
|
10
|
-
"Bash(gh issue create --repo jscad/OpenJSCAD.org --title \"perf\\(modeling\\): O\\(n²\\) array concatenation in scissionGeom3.js\" --body \"$\\(cat <<''EOF''\n## Summary\n`scissionGeom3.js` uses `Array.concat\\(\\)` inside nested loops, causing O\\(n²\\) memory operations.\n\n## Usage Frequency: LOW\nUsed only by `scission\\(\\)` operation which splits a geometry into disconnected pieces. Most typical models don''t use scission - it''s a specialized operation for:\n- Separating imported models that contain multiple parts\n- Post-processing boolean results that created separate islands\n\n**Typical model usage:** Rare - maybe 1% of models\n\n## Problem\n**File:** `packages/modeling/src/operations/booleans/scissionGeom3.js` \n**Lines:** 40-42\n\n```javascript\nconst indexesPerPolygon = polygons.map\\(\\(polygon\\) => {\n let indexes = []\n polygon.vertices.forEach\\(\\(point\\) => {\n indexes = indexes.concat\\(findMapping\\(indexesPerPoint, vec3.snap\\(temp, point, eps\\)\\)\\)\n }\\)\n return { e: 1, d: sortNb\\(indexes\\) }\n}\\)\n```\n\nFor each vertex of each polygon, `concat\\(\\)` creates a new array. With N polygons averaging V vertices, this is O\\(N*V\\) array allocations.\n\n## Suggested Fix\nUse `push\\(\\)` with spread:\n\n```javascript\nconst indexesPerPolygon = polygons.map\\(\\(polygon\\) => {\n const indexes = []\n polygon.vertices.forEach\\(\\(point\\) => {\n indexes.push\\(...findMapping\\(indexesPerPoint, vec3.snap\\(temp, point, eps\\)\\)\\)\n }\\)\n return { e: 1, d: sortNb\\(indexes\\) }\n}\\)\n```\n\n## Impact\nLow overall \\(rare operation\\), but when used on complex geometry it can be slow.\nEOF\n\\)\")",
|
|
11
10
|
"Bash(gh issue create:*)",
|
|
12
|
-
"Bash(npm run bench:*)",
|
|
13
11
|
"Bash(gh issue view:*)",
|
|
14
|
-
"Bash(git commit -m \"$\\(cat <<''EOF''\nperf\\(modeling\\): add benchmark suite for performance testing\n\nAdds benchmarks for:\n- Booleans \\(union, subtract, intersect at varying complexity\\)\n- Extrusions \\(extrudeLinear, extrudeRotate with varying slices\\)\n- Measurements \\(boundingBox, volume, area\\)\n- Transforms \\(translate, rotate, scale, center\\)\n- Utils \\(flatten with varying array sizes\\)\n\nRun with: npm run bench\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
15
12
|
"Bash(gh issue comment:*)",
|
|
13
|
+
"Bash(gh pr create:*)",
|
|
14
|
+
"Bash(npm run bench:*)",
|
|
16
15
|
"Bash(npm test:*)",
|
|
16
|
+
"Bash(npm install)",
|
|
17
|
+
"Bash(npm whoami:*)",
|
|
17
18
|
"Bash(npx ava:*)",
|
|
18
19
|
"Bash(node -e:*)",
|
|
19
|
-
"Bash(git commit:*)",
|
|
20
20
|
"Bash(ls:*)",
|
|
21
21
|
"Bash(grep:*)",
|
|
22
|
-
"Bash(npm install)",
|
|
23
|
-
"Bash(git checkout:*)",
|
|
24
|
-
"Bash(git push:*)",
|
|
25
|
-
"Bash(gh pr create:*)",
|
|
26
22
|
"Bash(cat:*)",
|
|
27
|
-
"Bash(npm
|
|
23
|
+
"Bash(npm run test:tsd:*)"
|
|
28
24
|
]
|
|
29
25
|
}
|
|
30
26
|
}
|
|
@@ -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)')
|