Skip to content

Commit ff6f608

Browse files
authored
feat(modeling): add Minkowski sum operation for 3D geometries
1 parent fc2f7c4 commit ff6f608

15 files changed

Lines changed: 957 additions & 385 deletions

File tree

packages/modeling/dist/jscad-modeling.min.js

Lines changed: 394 additions & 385 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/modeling/src/geometries/geom3/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as fromPoints } from './fromPoints'
55
export { default as fromCompactBinary } from './fromCompactBinary'
66
export { default as invert } from './invert'
77
export { default as isA } from './isA'
8+
export { isConvex } from './isConvex'
89
export { default as toPoints } from './toPoints'
910
export { default as toPolygons } from './toPolygons'
1011
export { default as toString } from './toString'

packages/modeling/src/geometries/geom3/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module.exports = {
2828
fromCompactBinary: require('./fromCompactBinary'),
2929
invert: require('./invert'),
3030
isA: require('./isA'),
31+
isConvex: require('./isConvex'),
3132
toPoints: require('./toPoints'),
3233
toPolygons: require('./toPolygons'),
3334
toString: require('./toString'),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type Geom3 from './type'
2+
3+
export function isConvex(geometry: Geom3): boolean
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const { EPS } = require('../../maths/constants')
2+
const vec3 = require('../../maths/vec3')
3+
4+
const geom3 = require('./isA')
5+
const toPolygons = require('./toPolygons')
6+
const poly3 = require('../poly3')
7+
8+
/**
9+
* Test if a 3D geometry is convex.
10+
*
11+
* A polyhedron is convex if every vertex lies on or behind every face plane
12+
* (i.e., on the interior side of the plane).
13+
*
14+
* @param {geom3} geometry - the geometry to test
15+
* @returns {boolean} true if the geometry is convex
16+
* @alias module:modeling/geometries/geom3.isConvex
17+
*
18+
* @example
19+
* const { geom3, primitives } = require('@jscad/modeling')
20+
* const cube = primitives.cuboid({ size: [10, 10, 10] })
21+
* console.log(geom3.isConvex(cube)) // true
22+
*/
23+
const isConvex = (geometry) => {
24+
if (!geom3(geometry)) {
25+
throw new Error('isConvex requires a geom3 geometry')
26+
}
27+
28+
const polygons = toPolygons(geometry)
29+
30+
if (polygons.length === 0) {
31+
return true // Empty geometry is trivially convex
32+
}
33+
34+
// Collect all unique vertices
35+
const vertices = []
36+
const found = new Set()
37+
for (let i = 0; i < polygons.length; i++) {
38+
const verts = polygons[i].vertices
39+
for (let j = 0; j < verts.length; j++) {
40+
const v = verts[j]
41+
const key = `${v[0]},${v[1]},${v[2]}`
42+
if (!found.has(key)) {
43+
found.add(key)
44+
vertices.push(v)
45+
}
46+
}
47+
}
48+
49+
// For each face plane, check that all vertices are on or behind it
50+
for (let i = 0; i < polygons.length; i++) {
51+
const plane = poly3.plane(polygons[i])
52+
53+
for (let j = 0; j < vertices.length; j++) {
54+
const v = vertices[j]
55+
// Distance from point to plane: dot(normal, point) - w
56+
const distance = vec3.dot(plane, v) - plane[3]
57+
58+
// If any vertex is in front of any face (positive distance), not convex
59+
if (distance > EPS) {
60+
return false
61+
}
62+
}
63+
}
64+
65+
return true
66+
}
67+
68+
module.exports = isConvex
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const test = require('ava')
2+
3+
const { geometries, primitives, booleans } = require('../../index')
4+
const { geom3 } = geometries
5+
6+
test('isConvex: throws for non-geom3 input', (t) => {
7+
t.throws(() => geom3.isConvex('invalid'), { message: /requires a geom3/ })
8+
t.throws(() => geom3.isConvex(null), { message: /requires a geom3/ })
9+
})
10+
11+
test('isConvex: empty geometry is convex', (t) => {
12+
const empty = geom3.create()
13+
t.true(geom3.isConvex(empty))
14+
})
15+
16+
test('isConvex: cuboid is convex', (t) => {
17+
const cube = primitives.cuboid({ size: [10, 10, 10] })
18+
t.true(geom3.isConvex(cube))
19+
})
20+
21+
test('isConvex: sphere is convex', (t) => {
22+
const sph = primitives.sphere({ radius: 5, segments: 16 })
23+
t.true(geom3.isConvex(sph))
24+
})
25+
26+
test('isConvex: cylinder is convex', (t) => {
27+
const cyl = primitives.cylinderElliptic({ height: 10, startRadius: [3, 3], endRadius: [3, 3], segments: 16 })
28+
t.true(geom3.isConvex(cyl))
29+
})
30+
31+
test('isConvex: cube with hole is not convex', (t) => {
32+
const cube = primitives.cuboid({ size: [10, 10, 10] })
33+
const hole = primitives.cuboid({ size: [4, 4, 20] }) // Hole through the cube
34+
35+
const withHole = booleans.subtract(cube, hole)
36+
t.false(geom3.isConvex(withHole))
37+
})
38+
39+
test('isConvex: L-shaped solid is not convex', (t) => {
40+
const big = primitives.cuboid({ size: [10, 10, 10], center: [0, 0, 0] })
41+
const corner = primitives.cuboid({ size: [6, 6, 12], center: [3, 3, 0] })
42+
43+
const lShape = booleans.subtract(big, corner)
44+
t.false(geom3.isConvex(lShape))
45+
})

packages/modeling/src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * as booleans from './operations/booleans'
1111
export * as expansions from './operations/expansions'
1212
export * as extrusions from './operations/extrusions'
1313
export * as hulls from './operations/hulls'
14+
export * as minkowski from './operations/minkowski'
1415
export * as modifiers from './operations/modifiers'
1516
export * as transforms from './operations/transforms'
1617

packages/modeling/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = {
1212
expansions: require('./operations/expansions'),
1313
extrusions: require('./operations/extrusions'),
1414
hulls: require('./operations/hulls'),
15+
minkowski: require('./operations/minkowski'),
1516
modifiers: require('./operations/modifiers'),
1617
transforms: require('./operations/transforms')
1718
}

packages/modeling/src/operations/booleans/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as intersect } from './intersect'
2+
export { minkowskiSum as minkowski } from '../minkowski/minkowskiSum'
23
export { default as subtract } from './subtract'
34
export { default as union } from './union'
45
export { default as scission } from './scission'

packages/modeling/src/operations/booleans/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
module.exports = {
1010
intersect: require('./intersect'),
11+
minkowski: require('../minkowski/minkowskiSum'),
1112
scission: require('./scission'),
1213
subtract: require('./subtract'),
1314
union: require('./union')

0 commit comments

Comments
 (0)