@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.
@@ -0,0 +1,161 @@
1
+ const test = require('ava')
2
+
3
+ const { geom3 } = require('../../geometries')
4
+ const { cuboid, sphere } = require('../../primitives')
5
+ const { measureBoundingBox } = require('../../measurements')
6
+ const { subtract } = require('../booleans')
7
+
8
+ const minkowskiSum = require('./minkowskiSum')
9
+ const isConvex = require('./isConvex')
10
+
11
+ test('minkowskiSum: throws for non-geom3 inputs', (t) => {
12
+ t.throws(() => minkowskiSum('invalid', cuboid()), { message: /requires geom3/ })
13
+ t.throws(() => minkowskiSum(cuboid(), 'invalid'), { message: /requires geom3/ })
14
+ })
15
+
16
+ test('minkowskiSum: throws for less than two geometries', (t) => {
17
+ t.throws(() => minkowskiSum(), { message: /requires at least two/ })
18
+ t.throws(() => minkowskiSum(cuboid()), { message: /requires at least two/ })
19
+ })
20
+
21
+ test('minkowskiSum: throws for more than two geometries', (t) => {
22
+ t.throws(() => minkowskiSum(cuboid(), cuboid(), cuboid()), { message: /exactly two/ })
23
+ })
24
+
25
+ test('minkowskiSum: cube + cube produces correct bounds', (t) => {
26
+ // Cube1: size 10 (±5 from origin)
27
+ // Cube2: size 4 (±2 from origin)
28
+ // Minkowski sum should be size 14 (±7 from origin)
29
+ const cube1 = cuboid({ size: [10, 10, 10] })
30
+ const cube2 = cuboid({ size: [4, 4, 4] })
31
+
32
+ const result = minkowskiSum(cube1, cube2)
33
+
34
+ t.true(geom3.isA(result))
35
+
36
+ const bounds = measureBoundingBox(result)
37
+ // Allow small tolerance for floating point
38
+ t.true(Math.abs(bounds[0][0] - (-7)) < 0.001)
39
+ t.true(Math.abs(bounds[0][1] - (-7)) < 0.001)
40
+ t.true(Math.abs(bounds[0][2] - (-7)) < 0.001)
41
+ t.true(Math.abs(bounds[1][0] - 7) < 0.001)
42
+ t.true(Math.abs(bounds[1][1] - 7) < 0.001)
43
+ t.true(Math.abs(bounds[1][2] - 7) < 0.001)
44
+ })
45
+
46
+ test('minkowskiSum: cube + sphere produces correct bounds', (t) => {
47
+ // Cube: size 10 (±5 from origin)
48
+ // Sphere: radius 2
49
+ // Minkowski sum should be ±7 from origin
50
+ const cube = cuboid({ size: [10, 10, 10] })
51
+ const sph = sphere({ radius: 2, segments: 16 })
52
+
53
+ const result = minkowskiSum(cube, sph)
54
+
55
+ t.true(geom3.isA(result))
56
+
57
+ const bounds = measureBoundingBox(result)
58
+ // Allow small tolerance
59
+ t.true(Math.abs(bounds[0][0] - (-7)) < 0.1)
60
+ t.true(Math.abs(bounds[1][0] - 7) < 0.1)
61
+ })
62
+
63
+ test('minkowskiSum: sphere + sphere produces correct bounds', (t) => {
64
+ // Sphere1: radius 3
65
+ // Sphere2: radius 2
66
+ // Minkowski sum should be a sphere-like shape with radius ~5
67
+ const sph1 = sphere({ radius: 3, segments: 16 })
68
+ const sph2 = sphere({ radius: 2, segments: 16 })
69
+
70
+ const result = minkowskiSum(sph1, sph2)
71
+
72
+ t.true(geom3.isA(result))
73
+
74
+ const bounds = measureBoundingBox(result)
75
+ // Should be approximately ±5
76
+ t.true(Math.abs(bounds[0][0] - (-5)) < 0.2)
77
+ t.true(Math.abs(bounds[1][0] - 5) < 0.2)
78
+ })
79
+
80
+ test('minkowskiSum: empty geometry returns empty', (t) => {
81
+ const empty = geom3.create()
82
+ const cube = cuboid({ size: [10, 10, 10] })
83
+
84
+ const result = minkowskiSum(empty, cube)
85
+
86
+ t.true(geom3.isA(result))
87
+ t.is(geom3.toPolygons(result).length, 0)
88
+ })
89
+
90
+ test('minkowskiSum: result is convex', (t) => {
91
+ const cube = cuboid({ size: [10, 10, 10] })
92
+ const sph = sphere({ radius: 2, segments: 12 })
93
+
94
+ const result = minkowskiSum(cube, sph)
95
+
96
+ t.true(isConvex(result))
97
+ })
98
+
99
+ // Non-convex tests
100
+
101
+ test('minkowskiSum: non-convex + convex produces valid geometry', (t) => {
102
+ // Create L-shaped non-convex geometry
103
+ const big = cuboid({ size: [10, 10, 10] })
104
+ const corner = cuboid({ size: [6, 6, 12], center: [3, 3, 0] })
105
+ const lShape = subtract(big, corner)
106
+
107
+ t.false(isConvex(lShape))
108
+
109
+ const sph = sphere({ radius: 1, segments: 8 })
110
+
111
+ const result = minkowskiSum(lShape, sph)
112
+
113
+ t.true(geom3.isA(result))
114
+ t.true(geom3.toPolygons(result).length > 0)
115
+ })
116
+
117
+ test('minkowskiSum: non-convex + convex produces correct bounds', (t) => {
118
+ // Cube with hole through it
119
+ const cube = cuboid({ size: [10, 10, 10] })
120
+ const hole = cuboid({ size: [4, 4, 20] })
121
+ const cubeWithHole = subtract(cube, hole)
122
+
123
+ t.false(isConvex(cubeWithHole))
124
+
125
+ // Offset by sphere of radius 1
126
+ const sph = sphere({ radius: 1, segments: 8 })
127
+ const result = minkowskiSum(cubeWithHole, sph)
128
+
129
+ const bounds = measureBoundingBox(result)
130
+
131
+ // Original cube is ±5, plus sphere radius 1 = ±6
132
+ t.true(Math.abs(bounds[0][0] - (-6)) < 0.2)
133
+ t.true(Math.abs(bounds[1][0] - 6) < 0.2)
134
+ })
135
+
136
+ test('minkowskiSum: convex + non-convex swaps operands', (t) => {
137
+ // Minkowski sum is commutative, so A⊕B = B⊕A
138
+ const cube = cuboid({ size: [10, 10, 10] })
139
+ const hole = cuboid({ size: [4, 4, 20] })
140
+ const cubeWithHole = subtract(cube, hole)
141
+
142
+ const sph = sphere({ radius: 1, segments: 8 })
143
+
144
+ // convex + non-convex should work (swaps internally)
145
+ const result = minkowskiSum(sph, cubeWithHole)
146
+
147
+ t.true(geom3.isA(result))
148
+ t.true(geom3.toPolygons(result).length > 0)
149
+ })
150
+
151
+ test('minkowskiSum: throws for two non-convex geometries', (t) => {
152
+ const cube1 = cuboid({ size: [10, 10, 10] })
153
+ const hole1 = cuboid({ size: [4, 4, 20] })
154
+ const nonConvex1 = subtract(cube1, hole1)
155
+
156
+ const cube2 = cuboid({ size: [8, 8, 8] })
157
+ const hole2 = cuboid({ size: [3, 3, 16] })
158
+ const nonConvex2 = subtract(cube2, hole2)
159
+
160
+ t.throws(() => minkowskiSum(nonConvex1, nonConvex2), { message: /two non-convex/ })
161
+ })
@@ -40,7 +40,7 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
40
40
  let miny
41
41
  let maxy
42
42
  for (let i = 0; i < numvertices; i++) {
43
- let pos2d = orthobasis.to2D(poly3d.vertices[i])
43
+ const pos2d = orthobasis.to2D(poly3d.vertices[i])
44
44
  // perform binning of y coordinates: If we have multiple vertices very
45
45
  // close to each other, give them the same y coordinate:
46
46
  const ycoordinatebin = Math.floor(pos2d[1] * ycoordinateBinningFactor)
@@ -55,7 +55,8 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
55
55
  newy = pos2d[1]
56
56
  ycoordinatebins.set(ycoordinatebin, pos2d[1])
57
57
  }
58
- pos2d = vec2.fromValues(pos2d[0], newy)
58
+ // Modify y in place instead of creating new vec2
59
+ pos2d[1] = newy
59
60
  vertices2d.push(pos2d)
60
61
  const y = pos2d[1]
61
62
  if ((i === 0) || (y < miny)) {
@@ -226,14 +227,11 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
226
227
  for (const activepolygonKey in activepolygons) {
227
228
  const activepolygon = activepolygons[activepolygonKey]
228
229
 
229
- let x = interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, ycoordinate)
230
- const topleft = vec2.fromValues(x, ycoordinate)
231
- x = interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, ycoordinate)
232
- const topright = vec2.fromValues(x, ycoordinate)
233
- x = interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, nextycoordinate)
234
- const bottomleft = vec2.fromValues(x, nextycoordinate)
235
- x = interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, nextycoordinate)
236
- const bottomright = vec2.fromValues(x, nextycoordinate)
230
+ // Inline vec2 creation to avoid function call overhead
231
+ const topleft = [interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, ycoordinate), ycoordinate]
232
+ const topright = [interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, ycoordinate), ycoordinate]
233
+ const bottomleft = [interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, nextycoordinate), nextycoordinate]
234
+ const bottomright = [interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, nextycoordinate), nextycoordinate]
237
235
  const outpolygon = {
238
236
  topleft: topleft,
239
237
  topright: topright,