@jscad/modeling 2.12.6 → 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/CHANGELOG.md +12 -299
- package/bench/booleans.bench.js +103 -0
- package/bench/primitives.bench.js +108 -0
- package/dist/jscad-modeling.min.js +404 -395
- package/package.json +2 -2
- package/src/geometries/geom3/index.d.ts +1 -0
- package/src/geometries/geom3/index.js +1 -0
- package/src/geometries/geom3/isConvex.d.ts +3 -0
- package/src/geometries/geom3/isConvex.js +68 -0
- package/src/geometries/geom3/isConvex.test.js +45 -0
- package/src/geometries/path2/appendArc.js +1 -1
- package/src/geometries/path2/appendArc.test.js +16 -20
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/operations/booleans/index.d.ts +1 -0
- package/src/operations/booleans/index.js +1 -0
- package/src/operations/booleans/trees/PolygonTreeNode.js +18 -5
- package/src/operations/booleans/trees/splitPolygonByPlane.js +27 -25
- package/src/operations/booleans/trees/splitPolygonByPlane.test.js +132 -0
- package/src/operations/booleans/unionGeom3.test.js +35 -0
- package/src/operations/extrusions/extrudeFromSlices.js +14 -4
- package/src/operations/extrusions/extrudeRectangular.test.js +3 -3
- 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 +1 -0
- package/src/operations/minkowski/index.js +17 -0
- package/src/operations/minkowski/minkowskiSum.d.ts +4 -0
- package/src/operations/minkowski/minkowskiSum.js +224 -0
- package/src/operations/minkowski/minkowskiSum.test.js +195 -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
- package/src/operations/modifiers/snap.test.js +24 -15
- package/src/primitives/arc.js +2 -2
- package/src/primitives/arc.test.js +122 -111
- package/src/utils/flatten.js +1 -1
- package/src/utils/flatten.test.js +94 -0
|
@@ -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')
|