@jbroll/jscad-modeling 2.12.7 → 2.12.8
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/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 +8 -8
- package/package.json +1 -1
- package/src/operations/booleans/trees/PolygonTreeNode.js +14 -5
- 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/modifiers/reTesselateCoplanarPolygons.js +8 -3
- package/src/operations/modifiers/reTesselateCoplanarPolygons.test.js +36 -1
- 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,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 }
|
|
@@ -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){this.isRootNode()&&this.children.length>0&&(this.children=this.children.filter(e=>!e.removed));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,r,h,d,c;for(i=0;i<s.length;i++)for(c=s[i],r=0,h=c.length;r<h;r++)(d=c[r]).children.length>0?s.push(d.children):d._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),r=i[3]+EPS,h=i,d=vec3.dot(e,h)-e[3];if(d>r)n.push(this);else if(d<-r)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 r=" ".repeat(n);for(l=0,s=t.length;l<s;l++)e+=`${r}PolygonTreeNode (${(i=t[l]).isRootNode()}): ${i.children.length}`,i.polygon?e+=`\n ${r}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=>{if(e.length<3)return 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=[],r=-EPS;for(let n=0;n<s;n++){const s=vec3.dot(e,l[n])-e[3],o=s<r;i.push(o),s>EPS&&(t=!0),s<r&&(p=!0)}if(t||p)if(p)if(t){n.type=4;const t=[],p=[];let r=i[0];for(let n=0;n<s;n++){const o=l[n];let u=n+1;u>=s&&(u=0);const c=i[u];if(r===c)r?p.push(o):t.push(o);else{const n=l[u],s=splitLineSegmentByPlane(e,o,n);r?(p.push(o),p.push(s),t.push(s)):(t.push(o),t.push(s),p.push(s))}r=c}const u=removeConsecutiveDuplicates(p),c=removeConsecutiveDuplicates(t);c.length>=3&&(n.front=poly3.fromPointsAndPlane(c,o)),u.length>=3&&(n.back=poly3.fromPointsAndPlane(u,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=>{const e=[],r=[t];for(;r.length;){const t=r.pop();if(Array.isArray(t))for(let e=t.length-1;e>=0;e--)r.push(t[e]);else e.push(t)}return e};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": "@jbroll/jscad-modeling",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.8",
|
|
4
4
|
"description": "Constructive Solid Geometry (CSG) Library for JSCAD (performance-optimized fork)",
|
|
5
5
|
"homepage": "https://github.com/jbroll/OpenJSCAD.org",
|
|
6
6
|
"repository": "https://github.com/jbroll/OpenJSCAD.org",
|
|
@@ -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,15 @@ 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
|
+
this.children = this.children.filter((c) => !c.removed)
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
let children = [this]
|
|
84
93
|
const queue = [children]
|
|
85
94
|
let i, j, l, node
|
|
@@ -131,3 +131,38 @@ test('union of geom3 with rounding issues #137', (t) => {
|
|
|
131
131
|
t.notThrows(() => geom3.validate(obs))
|
|
132
132
|
t.is(pts.length, 6) // number of polygons in union
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
// Test for push loop optimization: verify union works correctly with multiple geometries
|
|
136
|
+
// This ensures the concat-to-push-loop optimization handles array merging properly
|
|
137
|
+
test('union of geom3 with multiple overlapping geometries', (t) => {
|
|
138
|
+
// Create several overlapping cuboids to generate a complex union
|
|
139
|
+
const geometry1 = cuboid({ size: [10, 10, 10] })
|
|
140
|
+
const geometry2 = center({ relativeTo: [5, 0, 0] }, cuboid({ size: [10, 10, 10] }))
|
|
141
|
+
const geometry3 = center({ relativeTo: [0, 5, 0] }, cuboid({ size: [10, 10, 10] }))
|
|
142
|
+
|
|
143
|
+
// Union should work correctly with multiple geometries
|
|
144
|
+
const obs = union(geometry1, geometry2, geometry3)
|
|
145
|
+
const pts = geom3.toPoints(obs)
|
|
146
|
+
|
|
147
|
+
// Skip manifold validation - focus on testing polygon merging works correctly
|
|
148
|
+
// (CSG on overlapping boxes can produce non-manifold edges at coplanar faces)
|
|
149
|
+
// Should produce a merged geometry with polygons from all inputs
|
|
150
|
+
t.true(pts.length > 6) // more than a single cube
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Test for push loop optimization: verify non-overlapping geometries combine correctly
|
|
154
|
+
test('union of multiple non-overlapping geom3 preserves all polygons', (t) => {
|
|
155
|
+
// Create multiple small cuboids that don't overlap
|
|
156
|
+
const cubes = []
|
|
157
|
+
for (let i = 0; i < 10; i++) {
|
|
158
|
+
cubes.push(center({ relativeTo: [i * 5, 0, 0] }, cuboid({ size: [2, 2, 2] })))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Union all of them
|
|
162
|
+
const obs = union(...cubes)
|
|
163
|
+
const pts = geom3.toPoints(obs)
|
|
164
|
+
|
|
165
|
+
t.notThrows(() => geom3.validate(obs))
|
|
166
|
+
// Each cuboid has 6 faces, so 10 cuboids = 60 polygons
|
|
167
|
+
t.is(pts.length, 60)
|
|
168
|
+
})
|
|
@@ -114,13 +114,16 @@ const extrudeRotate = (options, geometry) => {
|
|
|
114
114
|
slice.reverse(baseSlice, baseSlice)
|
|
115
115
|
|
|
116
116
|
const matrix = mat4.create()
|
|
117
|
+
const xRotationMatrix = mat4.fromXRotation(mat4.create(), TAU / 4) // compute once, reuse
|
|
118
|
+
const zRotationMatrix = mat4.create() // reuse for Z rotation
|
|
117
119
|
const createSlice = (progress, index, base) => {
|
|
118
120
|
let Zrotation = rotationPerSlice * index + startAngle
|
|
119
121
|
// fix rounding error when rotating TAU radians
|
|
120
122
|
if (totalRotation === TAU && index === segments) {
|
|
121
123
|
Zrotation = startAngle
|
|
122
124
|
}
|
|
123
|
-
mat4.
|
|
125
|
+
mat4.fromZRotation(zRotationMatrix, Zrotation)
|
|
126
|
+
mat4.multiply(matrix, zRotationMatrix, xRotationMatrix)
|
|
124
127
|
|
|
125
128
|
return slice.transform(matrix, base)
|
|
126
129
|
}
|
|
@@ -158,4 +158,37 @@ test('extrudeRotate: (overlap +/-) extruding of a geom2 produces an expected geo
|
|
|
158
158
|
t.true(comparePolygonsAsPoints(pts, exp))
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
+
// Test for mat4 reuse optimization: verify rotation matrices are computed correctly
|
|
162
|
+
// This ensures the optimization of computing xRotationMatrix once doesn't break anything
|
|
163
|
+
test('extrudeRotate: (mat4 reuse) rotation matrices produce correct geometry', (t) => {
|
|
164
|
+
// Simple rectangle that will be rotated to form a tube-like shape
|
|
165
|
+
const geometry2 = geom2.fromPoints([[5, -1], [5, 1], [6, 1], [6, -1]])
|
|
166
|
+
|
|
167
|
+
// Full rotation with many segments to test matrix reuse across iterations
|
|
168
|
+
const geometry3 = extrudeRotate({ segments: 32 }, geometry2)
|
|
169
|
+
const pts = geom3.toPoints(geometry3)
|
|
170
|
+
|
|
171
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
172
|
+
// 32 segments * 8 walls per segment (4 edges * 2 triangles) = 256 polygons
|
|
173
|
+
t.is(pts.length, 256)
|
|
174
|
+
|
|
175
|
+
// Verify the geometry is closed (first and last slices connect properly)
|
|
176
|
+
// This tests the Zrotation rounding error fix at index === segments
|
|
177
|
+
const geometry3b = extrudeRotate({ segments: 16 }, geometry2)
|
|
178
|
+
t.notThrows(() => geom3.validate(geometry3b))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Test for mat4 reuse with partial rotation (tests both capped and matrix reuse)
|
|
182
|
+
test('extrudeRotate: (mat4 reuse) partial rotation produces correct caps', (t) => {
|
|
183
|
+
const geometry2 = geom2.fromPoints([[5, -1], [5, 1], [6, 1], [6, -1]])
|
|
184
|
+
|
|
185
|
+
// Quarter rotation - should have start and end caps
|
|
186
|
+
const geometry3 = extrudeRotate({ segments: 8, angle: TAU / 4 }, geometry2)
|
|
187
|
+
const pts = geom3.toPoints(geometry3)
|
|
188
|
+
|
|
189
|
+
t.notThrows(() => geom3.validate(geometry3))
|
|
190
|
+
// Should produce valid geometry with caps
|
|
191
|
+
t.true(pts.length > 0)
|
|
192
|
+
})
|
|
193
|
+
|
|
161
194
|
// TEST HOLES
|
|
@@ -26,10 +26,11 @@ const repartitionEdges = (newlength, edges) => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const divisor = vec3.fromValues(multiple, multiple, multiple)
|
|
29
|
+
const increment = vec3.create() // reuse across all edge iterations
|
|
29
30
|
|
|
30
31
|
const newEdges = []
|
|
31
32
|
edges.forEach((edge) => {
|
|
32
|
-
|
|
33
|
+
vec3.subtract(increment, edge[1], edge[0])
|
|
33
34
|
vec3.divide(increment, increment, divisor)
|
|
34
35
|
|
|
35
36
|
// repartition the edge
|
|
@@ -80,3 +80,75 @@ test('extrudeWalls (different shapes)', (t) => {
|
|
|
80
80
|
walls = extrudeWalls(slice3, slice.transform(matrix, slice2))
|
|
81
81
|
t.is(walls.length, 24)
|
|
82
82
|
})
|
|
83
|
+
|
|
84
|
+
// Test for vec3 reuse optimization in repartitionEdges
|
|
85
|
+
// When shapes have different edge counts, edges are repartitioned using vec3 operations
|
|
86
|
+
test('extrudeWalls (repartitionEdges vec3 reuse)', (t) => {
|
|
87
|
+
const matrix = mat4.fromTranslation(mat4.create(), [0, 0, 5])
|
|
88
|
+
|
|
89
|
+
// Triangle (3 edges)
|
|
90
|
+
const triangle = [
|
|
91
|
+
[[0, 10], [-8.66, -5]],
|
|
92
|
+
[[-8.66, -5], [8.66, -5]],
|
|
93
|
+
[[8.66, -5], [0, 10]]
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
// Hexagon (6 edges) - LCM with triangle is 6, so triangle edges get split
|
|
97
|
+
const hexagon = [
|
|
98
|
+
[[0, 10], [-8.66, 5]],
|
|
99
|
+
[[-8.66, 5], [-8.66, -5]],
|
|
100
|
+
[[-8.66, -5], [0, -10]],
|
|
101
|
+
[[0, -10], [8.66, -5]],
|
|
102
|
+
[[8.66, -5], [8.66, 5]],
|
|
103
|
+
[[8.66, 5], [0, 10]]
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const sliceTriangle = slice.fromSides(triangle)
|
|
107
|
+
const sliceHexagon = slice.fromSides(hexagon)
|
|
108
|
+
|
|
109
|
+
// Triangle to hexagon requires repartitioning (3 -> 6 edges)
|
|
110
|
+
// This exercises the vec3 reuse optimization in repartitionEdges
|
|
111
|
+
const walls = extrudeWalls(sliceTriangle, slice.transform(matrix, sliceHexagon))
|
|
112
|
+
|
|
113
|
+
// 6 edges * 2 triangles per edge = 12 wall polygons
|
|
114
|
+
t.is(walls.length, 12)
|
|
115
|
+
|
|
116
|
+
// Verify all walls are valid triangles
|
|
117
|
+
walls.forEach((wall) => {
|
|
118
|
+
t.is(wall.vertices.length, 3)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Test for vec3 reuse with higher repartition multiple
|
|
123
|
+
test('extrudeWalls (repartitionEdges with high multiple)', (t) => {
|
|
124
|
+
const matrix = mat4.fromTranslation(mat4.create(), [0, 0, 10])
|
|
125
|
+
|
|
126
|
+
// Square (4 edges)
|
|
127
|
+
const square = [
|
|
128
|
+
[[-5, 5], [-5, -5]],
|
|
129
|
+
[[-5, -5], [5, -5]],
|
|
130
|
+
[[5, -5], [5, 5]],
|
|
131
|
+
[[5, 5], [-5, 5]]
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
// Octagon (8 edges) - LCM with square is 8, so square edges get doubled
|
|
135
|
+
const octagon = [
|
|
136
|
+
[[0, 5], [-3.54, 3.54]],
|
|
137
|
+
[[-3.54, 3.54], [-5, 0]],
|
|
138
|
+
[[-5, 0], [-3.54, -3.54]],
|
|
139
|
+
[[-3.54, -3.54], [0, -5]],
|
|
140
|
+
[[0, -5], [3.54, -3.54]],
|
|
141
|
+
[[3.54, -3.54], [5, 0]],
|
|
142
|
+
[[5, 0], [3.54, 3.54]],
|
|
143
|
+
[[3.54, 3.54], [0, 5]]
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
const sliceSquare = slice.fromSides(square)
|
|
147
|
+
const sliceOctagon = slice.fromSides(octagon)
|
|
148
|
+
|
|
149
|
+
// Square to octagon requires repartitioning (4 -> 8 edges)
|
|
150
|
+
const walls = extrudeWalls(sliceSquare, slice.transform(matrix, sliceOctagon))
|
|
151
|
+
|
|
152
|
+
// 8 edges * 2 triangles per edge = 16 wall polygons
|
|
153
|
+
t.is(walls.length, 16)
|
|
154
|
+
})
|
|
@@ -120,6 +120,7 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
|
|
|
120
120
|
// at the the left and right side of the polygon
|
|
121
121
|
// Iterate over all polygons that have a corner at this y coordinate:
|
|
122
122
|
const polygonindexeswithcorner = ycoordinatetopolygonindexes.get(ycoordinate)
|
|
123
|
+
let removeCount = 0 // track removals to filter at end (avoids O(n²) splice)
|
|
123
124
|
for (let activepolygonindex = 0; activepolygonindex < activepolygons.length; ++activepolygonindex) {
|
|
124
125
|
const activepolygon = activepolygons[activepolygonindex]
|
|
125
126
|
const polygonindex = activepolygon.polygonindex
|
|
@@ -143,9 +144,9 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
|
|
|
143
144
|
}
|
|
144
145
|
if ((newleftvertexindex !== activepolygon.leftvertexindex) && (newleftvertexindex === newrightvertexindex)) {
|
|
145
146
|
// We have increased leftvertexindex or decreased rightvertexindex, and now they point to the same vertex
|
|
146
|
-
// This means that this is the bottom point of the polygon.
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
// This means that this is the bottom point of the polygon. Mark it for removal:
|
|
148
|
+
activepolygon._remove = true
|
|
149
|
+
removeCount++
|
|
149
150
|
} else {
|
|
150
151
|
activepolygon.leftvertexindex = newleftvertexindex
|
|
151
152
|
activepolygon.rightvertexindex = newrightvertexindex
|
|
@@ -160,6 +161,10 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
|
|
|
160
161
|
}
|
|
161
162
|
} // if polygon has corner here
|
|
162
163
|
} // for activepolygonindex
|
|
164
|
+
// Filter out marked polygons in single pass (O(n) instead of O(n²) splice)
|
|
165
|
+
if (removeCount > 0) {
|
|
166
|
+
activepolygons = activepolygons.filter((p) => !p._remove)
|
|
167
|
+
}
|
|
163
168
|
let nextycoordinate
|
|
164
169
|
if (yindex >= ycoordinates.length - 1) {
|
|
165
170
|
// last row, all polygons must be finished here:
|
|
@@ -16,7 +16,7 @@ const rotatePoly3 = (angles, polygon) => {
|
|
|
16
16
|
return poly3.transform(matrix, polygon)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
test
|
|
19
|
+
test('retessellateCoplanarPolygons: should merge coplanar polygons', (t) => {
|
|
20
20
|
const polyA = poly3.create([[-5, -5, 0], [5, -5, 0], [5, 5, 0], [-5, 5, 0]])
|
|
21
21
|
const polyB = poly3.create([[5, -5, 0], [8, 0, 0], [5, 5, 0]])
|
|
22
22
|
const polyC = poly3.create([[-5, 5, 0], [-8, 0, 0], [-5, -5, 0]])
|
|
@@ -68,3 +68,38 @@ test.only('retessellateCoplanarPolygons: should merge coplanar polygons', (t) =>
|
|
|
68
68
|
obs = reTesselateCoplanarPolygons([polyH, polyI, polyJ, polyK, polyL])
|
|
69
69
|
t.is(obs.length, 1)
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
// Test for mark-and-filter optimization: multiple polygons that reach their
|
|
73
|
+
// bottom point at the same y-coordinate (triggering the removal code path)
|
|
74
|
+
test('retessellateCoplanarPolygons: should correctly handle multiple polygon removals', (t) => {
|
|
75
|
+
// Create multiple triangular polygons that all end at the same y-coordinate
|
|
76
|
+
// This exercises the mark-and-filter removal optimization
|
|
77
|
+
const poly1 = poly3.create([[0, 0, 0], [2, 0, 0], [1, 3, 0]]) // triangle pointing up
|
|
78
|
+
const poly2 = poly3.create([[3, 0, 0], [5, 0, 0], [4, 3, 0]]) // triangle pointing up
|
|
79
|
+
const poly3a = poly3.create([[6, 0, 0], [8, 0, 0], [7, 3, 0]]) // triangle pointing up
|
|
80
|
+
|
|
81
|
+
// These polygons share the same plane and have vertices at y=0 and y=3
|
|
82
|
+
// During retessellation, all three will be active and then removed at y=3
|
|
83
|
+
const obs = reTesselateCoplanarPolygons([poly1, poly2, poly3a])
|
|
84
|
+
|
|
85
|
+
// Each triangle should be preserved (they don't overlap)
|
|
86
|
+
t.is(obs.length, 3)
|
|
87
|
+
|
|
88
|
+
// Verify each polygon has 3 vertices (triangles)
|
|
89
|
+
obs.forEach((polygon) => {
|
|
90
|
+
t.is(polygon.vertices.length, 3)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Test for mark-and-filter with overlapping polygons that get merged
|
|
95
|
+
test('retessellateCoplanarPolygons: should merge adjacent polygons with shared edges', (t) => {
|
|
96
|
+
// Two adjacent squares sharing an edge at x=5
|
|
97
|
+
const poly1 = poly3.create([[0, 0, 0], [5, 0, 0], [5, 5, 0], [0, 5, 0]])
|
|
98
|
+
const poly2 = poly3.create([[5, 0, 0], [10, 0, 0], [10, 5, 0], [5, 5, 0]])
|
|
99
|
+
|
|
100
|
+
const obs = reTesselateCoplanarPolygons([poly1, poly2])
|
|
101
|
+
|
|
102
|
+
// Should merge into a single rectangle
|
|
103
|
+
t.is(obs.length, 1)
|
|
104
|
+
t.is(obs[0].vertices.length, 4) // rectangle has 4 vertices
|
|
105
|
+
})
|
|
@@ -22,8 +22,11 @@ const retessellate = (geometry) => {
|
|
|
22
22
|
const destPolygons = []
|
|
23
23
|
classified.forEach((group) => {
|
|
24
24
|
if (Array.isArray(group)) {
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const coplanarPolygons = reTesselateCoplanarPolygons(group)
|
|
26
|
+
// Use loop instead of spread to avoid stack overflow with large arrays
|
|
27
|
+
for (let i = 0; i < coplanarPolygons.length; i++) {
|
|
28
|
+
destPolygons.push(coplanarPolygons[i])
|
|
29
|
+
}
|
|
27
30
|
} else {
|
|
28
31
|
destPolygons.push(group)
|
|
29
32
|
}
|