@jscad/modeling 3.0.0-alpha.0 → 3.0.2-alpha.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 +49 -0
- package/LICENSE +1 -1
- package/dist/jscad-modeling.es.js +2 -2
- package/dist/jscad-modeling.min.js +2 -2
- package/package.json +2 -2
- package/rollup.config.js +1 -1
- package/src/colors/colorize.js +1 -5
- package/src/colors/colorize.test.js +8 -8
- package/src/geometries/geom2/transform.js +9 -1
- package/src/geometries/geom2/transform.test.js +57 -0
- package/src/geometries/geom3/fromPointsConvex.d.ts +4 -0
- package/src/geometries/geom3/fromPointsConvex.js +25 -0
- package/src/geometries/geom3/fromPointsConvex.test.js +32 -0
- package/src/geometries/geom3/index.d.ts +1 -0
- package/src/geometries/geom3/index.js +1 -0
- package/src/geometries/index.js +3 -4
- package/src/geometries/path2/appendBezier.js +1 -1
- package/src/geometries/poly3/index.js +1 -1
- package/src/geometries/poly3/measureBoundingBox.js +2 -0
- package/src/geometries/poly3/measureBoundingSphere.d.ts +2 -1
- package/src/geometries/poly3/measureBoundingSphere.js +25 -8
- package/src/geometries/poly3/measureBoundingSphere.test.js +12 -8
- package/src/geometries/poly3/type.d.ts +1 -1
- package/src/geometries/slice/validate.js +1 -2
- package/src/index.js +41 -0
- package/src/maths/index.js +1 -0
- package/src/maths/mat4/isOnlyTransformScale.js +1 -1
- package/src/measurements/measureAggregateArea.js +0 -1
- package/src/measurements/measureAggregateBoundingBox.js +0 -1
- package/src/measurements/measureAggregateEpsilon.js +0 -1
- package/src/measurements/measureAggregateVolume.js +0 -1
- package/src/measurements/measureArea.js +0 -1
- package/src/measurements/measureBoundingBox.js +0 -1
- package/src/measurements/measureBoundingSphere.js +2 -6
- package/src/measurements/measureEpsilon.js +0 -1
- package/src/measurements/measureVolume.js +0 -1
- package/src/operations/booleans/index.d.ts +1 -0
- package/src/operations/booleans/intersect.js +5 -5
- package/src/operations/booleans/intersect.test.js +6 -7
- package/src/operations/booleans/intersectGeom2.js +2 -6
- package/src/operations/booleans/intersectGeom2.test.js +25 -1
- package/src/operations/booleans/intersectGeom3.js +2 -6
- package/src/operations/booleans/intersectGeom3.test.js +5 -1
- package/src/operations/booleans/martinez/compareEvents.js +2 -7
- package/src/operations/booleans/martinez/connectEdges.js +30 -41
- package/src/operations/booleans/martinez/contour.js +1 -1
- package/src/operations/booleans/martinez/divideSegment.js +12 -11
- package/src/operations/booleans/martinez/fillQueue.js +24 -28
- package/src/operations/booleans/martinez/index.js +2 -1
- package/src/operations/booleans/martinez/possibleIntersection.js +41 -30
- package/src/operations/booleans/martinez/segmentIntersection.js +7 -9
- package/src/operations/booleans/martinez/splaytree.js +59 -457
- package/src/operations/booleans/martinez/subdivideSegments.js +4 -4
- package/src/operations/booleans/martinez/sweepEvent.js +3 -17
- package/src/operations/booleans/mayOverlap.js +0 -1
- package/src/operations/booleans/scission.d.ts +5 -0
- package/src/operations/booleans/scission.js +3 -5
- package/src/operations/booleans/scission.test.js +6 -0
- package/src/operations/booleans/subtract.js +5 -5
- package/src/operations/booleans/subtract.test.js +6 -7
- package/src/operations/booleans/subtractGeom2.js +2 -6
- package/src/operations/booleans/subtractGeom2.test.js +25 -1
- package/src/operations/booleans/subtractGeom3.js +2 -6
- package/src/operations/booleans/subtractGeom3.test.js +5 -1
- package/src/operations/booleans/trees/Node.js +25 -27
- package/src/operations/booleans/trees/PolygonTreeNode.js +153 -106
- package/src/operations/booleans/trees/Tree.js +9 -4
- package/src/operations/booleans/trees/splitLineSegmentByPlane.js +5 -3
- package/src/operations/booleans/trees/splitPolygonByPlane.d.ts +33 -0
- package/src/operations/booleans/trees/splitPolygonByPlane.js +39 -34
- package/src/operations/booleans/union.js +5 -5
- package/src/operations/booleans/union.test.js +6 -7
- package/src/operations/booleans/unionGeom2.js +2 -6
- package/src/operations/booleans/unionGeom2.test.js +25 -1
- package/src/operations/booleans/unionGeom3.js +2 -6
- package/src/operations/booleans/unionGeom3.test.js +6 -1
- package/src/operations/extrusions/extrudeFromSlices.test.js +8 -1
- package/src/operations/extrusions/extrudeHelical.js +2 -8
- package/src/operations/extrusions/extrudeLinear.js +1 -5
- package/src/operations/extrusions/extrudeLinear.test.js +7 -1
- package/src/operations/extrusions/extrudeRotate.js +3 -2
- package/src/operations/extrusions/extrudeRotate.test.js +13 -1
- package/src/operations/extrusions/extrudeWalls.js +3 -1
- package/src/operations/extrusions/project.js +1 -5
- package/src/operations/hulls/hull.js +6 -5
- package/src/operations/hulls/hull.test.js +56 -3
- package/src/operations/hulls/hullChain.js +11 -6
- package/src/operations/hulls/hullChain.test.js +12 -2
- package/src/operations/hulls/hullGeom2.js +5 -6
- package/src/operations/hulls/hullGeom3.js +9 -18
- package/src/operations/hulls/hullPath2.js +6 -7
- package/src/operations/hulls/hullPath2.test.js +1 -1
- package/src/operations/hulls/hullPoints2.d.ts +3 -0
- package/src/operations/hulls/hullPoints2.js +24 -30
- package/src/operations/hulls/hullPoints3.d.ts +4 -0
- package/src/operations/hulls/hullPoints3.js +21 -0
- package/src/operations/hulls/index.d.ts +2 -0
- package/src/operations/hulls/index.js +3 -1
- package/src/operations/modifiers/generalize.js +2 -6
- package/src/operations/modifiers/index.js +1 -1
- package/src/operations/modifiers/mergePolygons.js +2 -3
- package/src/operations/modifiers/reTesselateCoplanarPolygons.js +7 -7
- package/src/operations/modifiers/snap.js +2 -6
- package/src/operations/offsets/offset.js +1 -5
- package/src/operations/offsets/offsetFromPoints.test.js +0 -1
- package/src/operations/offsets/offsetGeom2.test.js +1 -0
- package/src/operations/offsets/offsetGeom3.js +0 -2
- package/src/operations/offsets/offsetGeom3.test.js +9 -1
- package/src/operations/offsets/offsetPath2.js +3 -3
- package/src/operations/transforms/align.js +8 -7
- package/src/operations/transforms/align.test.js +2 -2
- package/src/operations/transforms/center.js +6 -9
- package/src/operations/transforms/center.test.js +19 -1
- package/src/operations/transforms/mirror.js +5 -8
- package/src/operations/transforms/mirror.test.js +7 -7
- package/src/operations/transforms/rotate.js +5 -8
- package/src/operations/transforms/scale.js +5 -8
- package/src/operations/transforms/transform.js +2 -5
- package/src/operations/transforms/translate.js +5 -8
- package/src/primitives/arc.js +2 -0
- package/src/primitives/arc.test.js +11 -11
- package/src/primitives/circle.test.js +18 -8
- package/src/primitives/cube.test.js +10 -0
- package/src/primitives/cuboid.test.js +10 -0
- package/src/primitives/cylinder.test.js +12 -0
- package/src/primitives/cylinderElliptic.test.js +21 -1
- package/src/primitives/ellipse.test.js +18 -8
- package/src/primitives/ellipsoid.test.js +12 -0
- package/src/primitives/geodesicSphere.test.js +8 -0
- package/src/primitives/line.test.js +1 -1
- package/src/primitives/polygon.d.ts +1 -0
- package/src/primitives/polygon.js +13 -4
- package/src/primitives/polygon.test.js +15 -0
- package/src/primitives/polyhedron.js +1 -0
- package/src/primitives/polyhedron.test.js +8 -2
- package/src/primitives/rectangle.test.js +9 -3
- package/src/primitives/roundedCuboid.js +1 -1
- package/src/primitives/roundedCuboid.test.js +20 -4
- package/src/primitives/roundedCylinder.js +1 -1
- package/src/primitives/roundedCylinder.test.js +20 -0
- package/src/primitives/roundedRectangle.js +1 -1
- package/src/primitives/roundedRectangle.test.js +15 -6
- package/src/primitives/sphere.test.js +12 -0
- package/src/primitives/square.test.js +10 -4
- package/src/primitives/star.test.js +14 -6
- package/src/primitives/torus.js +1 -1
- package/src/primitives/torus.test.js +11 -1
- package/src/primitives/triangle.test.js +17 -9
- package/src/utils/coalesce.d.ts +3 -0
- package/src/utils/coalesce.js +20 -0
- package/src/utils/index.js +2 -2
- package/src/maths/mat4/leftMultiplyVec2.d.ts +0 -4
- package/src/maths/mat4/leftMultiplyVec2.js +0 -26
- package/src/maths/mat4/leftMultiplyVec3.d.ts +0 -4
- package/src/maths/mat4/leftMultiplyVec3.js +0 -27
- package/src/maths/mat4/mirror.d.ts +0 -4
- package/src/maths/mat4/mirror.js +0 -32
- package/src/maths/mat4/rightMultiplyVec2.d.ts +0 -4
- package/src/maths/mat4/rightMultiplyVec2.js +0 -27
- package/src/maths/mat4/rightMultiplyVec3.d.ts +0 -4
- package/src/maths/mat4/rightMultiplyVec3.js +0 -28
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import test from 'ava'
|
|
2
2
|
|
|
3
3
|
import { geom2, geom3 } from '../../geometries/index.js'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { measureArea, measureVolume } from '../../measurements/index.js'
|
|
6
|
+
|
|
5
7
|
import { square } from '../../primitives/square.js'
|
|
6
8
|
|
|
7
9
|
import { hullChain } from './index.js'
|
|
8
10
|
|
|
9
11
|
test('hullChain: hullChain single geometry', (t) => {
|
|
10
|
-
const result = hullChain([
|
|
12
|
+
const result = hullChain([square({ size: 1 })])
|
|
11
13
|
t.notThrows(() => geom2.validate(result))
|
|
12
14
|
t.is(measureArea(result), 1)
|
|
13
15
|
t.is(geom2.toPoints(result).length, 4)
|
|
@@ -22,6 +24,7 @@ test('hullChain (two, geom2)', (t) => {
|
|
|
22
24
|
let pts = geom2.toPoints(obs)
|
|
23
25
|
|
|
24
26
|
t.notThrows(() => geom2.validate(obs))
|
|
27
|
+
t.is(measureArea(obs), 9)
|
|
25
28
|
t.is(pts.length, 4)
|
|
26
29
|
|
|
27
30
|
// different
|
|
@@ -29,6 +32,7 @@ test('hullChain (two, geom2)', (t) => {
|
|
|
29
32
|
pts = geom2.toPoints(obs)
|
|
30
33
|
|
|
31
34
|
t.notThrows(() => geom2.validate(obs))
|
|
35
|
+
t.is(measureArea(obs), 81)
|
|
32
36
|
t.is(pts.length, 6)
|
|
33
37
|
})
|
|
34
38
|
|
|
@@ -43,6 +47,7 @@ test('hullChain (three, geom2)', (t) => {
|
|
|
43
47
|
|
|
44
48
|
// the sides change based on the bestplane chosen in trees/Node.js
|
|
45
49
|
t.notThrows(() => geom2.validate(obs))
|
|
50
|
+
t.is(measureArea(obs), 126)
|
|
46
51
|
t.is(pts.length, 10)
|
|
47
52
|
|
|
48
53
|
// closed
|
|
@@ -51,6 +56,7 @@ test('hullChain (three, geom2)', (t) => {
|
|
|
51
56
|
|
|
52
57
|
// the sides change based on the bestplane chosen in trees/Node.js
|
|
53
58
|
t.notThrows(() => geom2.validate(obs))
|
|
59
|
+
t.is(measureArea(obs), 148.21875)
|
|
54
60
|
t.is(pts.length, 10)
|
|
55
61
|
})
|
|
56
62
|
|
|
@@ -85,6 +91,8 @@ test('hullChain (three, geom3)', (t) => {
|
|
|
85
91
|
let pts = geom3.toPoints(obs)
|
|
86
92
|
|
|
87
93
|
t.notThrows.skip(() => geom3.validate(obs))
|
|
94
|
+
t.is(measureArea(obs), 266.1454764345133)
|
|
95
|
+
t.is(measureVolume(obs), 239.2012987012987)
|
|
88
96
|
t.is(pts.length, 23)
|
|
89
97
|
|
|
90
98
|
// closed
|
|
@@ -92,5 +100,7 @@ test('hullChain (three, geom3)', (t) => {
|
|
|
92
100
|
pts = geom3.toPoints(obs)
|
|
93
101
|
|
|
94
102
|
t.notThrows.skip(() => geom3.validate(obs))
|
|
103
|
+
t.is(measureArea(obs), 272.2887171436021)
|
|
104
|
+
t.is(measureVolume(obs), 261.96982218883045)
|
|
95
105
|
t.is(pts.length, 28)
|
|
96
106
|
})
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { flatten } from '../../utils/flatten.js'
|
|
2
|
-
|
|
3
1
|
import * as geom2 from '../../geometries/geom2/index.js'
|
|
4
2
|
|
|
5
3
|
import { hullPoints2 } from './hullPoints2.js'
|
|
@@ -7,12 +5,13 @@ import { toUniquePoints } from './toUniquePoints.js'
|
|
|
7
5
|
|
|
8
6
|
/*
|
|
9
7
|
* Create a convex hull of the given geom2 geometries.
|
|
10
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* NOTE: The given geometries must be valid geom2 geometries.
|
|
10
|
+
*
|
|
11
|
+
* @param {Geom2[]} geometries - a flat list of 2D geometries
|
|
11
12
|
* @returns {Geom2} new geometry
|
|
12
13
|
*/
|
|
13
|
-
export const hullGeom2 = (
|
|
14
|
-
geometries = flatten(geometries)
|
|
15
|
-
|
|
14
|
+
export const hullGeom2 = (geometries) => {
|
|
16
15
|
// extract the unique points from the geometries
|
|
17
16
|
const unique = toUniquePoints(geometries)
|
|
18
17
|
|
|
@@ -1,30 +1,21 @@
|
|
|
1
|
-
import { flatten } from '../../utils/flatten.js'
|
|
2
|
-
|
|
3
1
|
import * as geom3 from '../../geometries/geom3/index.js'
|
|
4
|
-
import * as poly3 from '../../geometries/poly3/index.js'
|
|
5
2
|
|
|
6
|
-
import {
|
|
3
|
+
import { hullPoints3 } from './hullPoints3.js'
|
|
7
4
|
import { toUniquePoints } from './toUniquePoints.js'
|
|
8
5
|
|
|
9
6
|
/*
|
|
10
|
-
* Create a convex hull of the given geometries
|
|
11
|
-
*
|
|
7
|
+
* Create a convex hull of the given geom3 geometries.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: The given geometries must be valid geom3 geometries.
|
|
10
|
+
*
|
|
11
|
+
* @param {Geom3[]} geometries - a flat list of 3D geometries
|
|
12
12
|
* @returns {Geom3} new geometry
|
|
13
13
|
*/
|
|
14
|
-
export const hullGeom3 = (
|
|
15
|
-
geometries = flatten(geometries)
|
|
16
|
-
|
|
17
|
-
if (geometries.length === 1) return geometries[0]
|
|
18
|
-
|
|
14
|
+
export const hullGeom3 = (geometries) => {
|
|
19
15
|
// extract the unique vertices from the geometries
|
|
20
16
|
const unique = toUniquePoints(geometries)
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const polygons = faces.map((face) => {
|
|
25
|
-
const vertices = face.map((index) => unique[index])
|
|
26
|
-
return poly3.create(vertices)
|
|
27
|
-
})
|
|
18
|
+
if (unique.length === 0) return geom3.create()
|
|
28
19
|
|
|
29
|
-
return geom3.create(
|
|
20
|
+
return geom3.create(hullPoints3(unique))
|
|
30
21
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { flatten } from '../../utils/flatten.js'
|
|
2
|
-
|
|
3
1
|
import * as path2 from '../../geometries/path2/index.js'
|
|
4
2
|
|
|
5
3
|
import { hullPoints2 } from './hullPoints2.js'
|
|
6
4
|
import { toUniquePoints } from './toUniquePoints.js'
|
|
7
5
|
|
|
8
6
|
/*
|
|
9
|
-
* Create a convex hull of the given geometries
|
|
10
|
-
*
|
|
7
|
+
* Create a convex hull of the given path2 geometries.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: The given geometries must be valid path2 geometry.
|
|
10
|
+
*
|
|
11
|
+
* @param {Path2[]} geometries - a flat list of path2 geometries
|
|
11
12
|
* @returns {Path2} new geometry
|
|
12
13
|
*/
|
|
13
|
-
export const hullPath2 = (
|
|
14
|
-
geometries = flatten(geometries)
|
|
15
|
-
|
|
14
|
+
export const hullPath2 = (geometries) => {
|
|
16
15
|
// extract the unique points from the geometries
|
|
17
16
|
const unique = toUniquePoints(geometries)
|
|
18
17
|
|
|
@@ -9,7 +9,7 @@ test('hullPath2', (t) => {
|
|
|
9
9
|
const geometry1 = path2.fromPoints({ closed }, [[0, 0], [-4, 4], [-4, -4]])
|
|
10
10
|
const geometry2 = path2.fromPoints({ closed }, [[0, 0], [4, -4], [4, 4]])
|
|
11
11
|
|
|
12
|
-
const obs = hullPath2(geometry1, geometry2)
|
|
12
|
+
const obs = hullPath2([geometry1, geometry2])
|
|
13
13
|
t.notThrows(() => path2.validate(obs))
|
|
14
14
|
const pts = path2.toPoints(obs)
|
|
15
15
|
t.is(pts.length, 4)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as vec2 from '../../maths/vec2/index.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
4
|
* Create a convex hull of the given set of points, where each point is an array of [x,y].
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* @see https://en.wikipedia.org/wiki/Graham_scan
|
|
6
7
|
* @param {Array} uniquePoints - list of UNIQUE points from which to create a hull
|
|
7
8
|
* @returns {Array} a list of points that form the hull
|
|
9
|
+
* @alias module:modeling/hulls.hullPoints2
|
|
8
10
|
*/
|
|
9
11
|
export const hullPoints2 = (uniquePoints) => {
|
|
10
12
|
// find min point
|
|
@@ -15,28 +17,32 @@ export const hullPoints2 = (uniquePoints) => {
|
|
|
15
17
|
}
|
|
16
18
|
})
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
// use faster fakeAtan2 instead of Math.atan2
|
|
22
|
-
const angle = fakeAtan2(point[1] - min[1], point[0] - min[0])
|
|
23
|
-
const distSq = vec2.squaredDistance(point, min)
|
|
24
|
-
points.push({ point, angle, distSq })
|
|
25
|
-
})
|
|
20
|
+
// calculations relative to min point
|
|
21
|
+
const squaredDistance = (point) => vec2.squaredDistance(point, min)
|
|
22
|
+
const polarAngle = (point) => (point[0] === min[0] && point[1] === min[1]) ? -Infinity : -(point[0] - min[0]) / (point[1] - min[1])
|
|
26
23
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
// points are sorted by polar angle in clockwise order
|
|
25
|
+
const sorted = uniquePoints
|
|
26
|
+
sorted.sort((pt1, pt2) => {
|
|
27
|
+
const pa1 = polarAngle(pt1)
|
|
28
|
+
const pa2 = polarAngle(pt2)
|
|
29
|
+
if (pa1 === pa2) {
|
|
30
|
+
// sort by the relative distances to min point
|
|
31
|
+
return squaredDistance(pt1) - squaredDistance(pt2)
|
|
32
|
+
}
|
|
33
|
+
// sort by polar angles to min point
|
|
34
|
+
return pa1 - pa2
|
|
35
|
+
})
|
|
31
36
|
|
|
32
37
|
const stack = [] // start with empty stack
|
|
33
|
-
|
|
38
|
+
sorted.forEach((point) => {
|
|
34
39
|
let cnt = stack.length
|
|
35
|
-
while (cnt > 1 && ccw(stack[cnt - 2], stack[cnt - 1], point
|
|
36
|
-
|
|
40
|
+
while (cnt > 1 && ccw(stack[cnt - 2], stack[cnt - 1], point) <= Number.EPSILON) {
|
|
41
|
+
// get rid of colinear and interior (clockwise) points
|
|
42
|
+
stack.pop()
|
|
37
43
|
cnt = stack.length
|
|
38
44
|
}
|
|
39
|
-
stack.push(point
|
|
45
|
+
stack.push(point)
|
|
40
46
|
})
|
|
41
47
|
|
|
42
48
|
return stack
|
|
@@ -44,15 +50,3 @@ export const hullPoints2 = (uniquePoints) => {
|
|
|
44
50
|
|
|
45
51
|
// returns: < 0 clockwise, 0 colinear, > 0 counter-clockwise
|
|
46
52
|
const ccw = (v1, v2, v3) => (v2[0] - v1[0]) * (v3[1] - v1[1]) - (v2[1] - v1[1]) * (v3[0] - v1[0])
|
|
47
|
-
|
|
48
|
-
// Returned "angle" is really 1/tan (inverse of slope) made negative to increase with angle.
|
|
49
|
-
// This function is strictly for sorting in this algorithm.
|
|
50
|
-
const fakeAtan2 = (y, x) => {
|
|
51
|
-
// The "if" is a special case for when the minimum vector found in loop above is present.
|
|
52
|
-
// We need to ensure that it sorts as the minimum point. Otherwise, this becomes NaN.
|
|
53
|
-
if (y === 0 && x === 0) {
|
|
54
|
-
return -Infinity
|
|
55
|
-
} else {
|
|
56
|
-
return -x / y
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as poly3 from '../../geometries/poly3/index.js'
|
|
2
|
+
|
|
3
|
+
import { runner } from './quickhull/index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a convex hull of the given set of points, where each point is an array of [x,y,z].
|
|
7
|
+
*
|
|
8
|
+
* @param {Array} uniquePoints - list of UNIQUE points from which to create a hull
|
|
9
|
+
* @returns {Array} a list of polygons (poly3)
|
|
10
|
+
* @alias module:modeling/hulls.hullPoints3
|
|
11
|
+
*/
|
|
12
|
+
export const hullPoints3 = (uniquePoints) => {
|
|
13
|
+
const faces = runner(uniquePoints, { skipTriangulation: true })
|
|
14
|
+
|
|
15
|
+
const polygons = faces.map((face) => {
|
|
16
|
+
const vertices = face.map((index) => uniquePoints[index])
|
|
17
|
+
return poly3.create(vertices)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return polygons
|
|
21
|
+
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* In all cases, the function returns the results, and never changes the original shapes.
|
|
5
5
|
* @module modeling/hulls
|
|
6
6
|
* @example
|
|
7
|
-
* import { hull, hullChain } from '@jscad/modeling'
|
|
7
|
+
* import { hull, hullChain, hullPoints2, hullPoints3 } from '@jscad/modeling'
|
|
8
8
|
*/
|
|
9
9
|
export { hull } from './hull.js'
|
|
10
10
|
export { hullChain } from './hullChain.js'
|
|
11
|
+
export { hullPoints2 } from './hullPoints2.js'
|
|
12
|
+
export { hullPoints3 } from './hullPoints3.js'
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { flatten } from '../../utils/flatten.js'
|
|
2
|
-
|
|
3
1
|
import { measureEpsilon } from '../../measurements/measureEpsilon.js'
|
|
4
2
|
|
|
5
3
|
import * as geom2 from '../../geometries/geom2/index.js'
|
|
@@ -67,14 +65,12 @@ const generalizeGeom3 = (options, geometry) => {
|
|
|
67
65
|
* @alias module:modeling/modifiers.generalize
|
|
68
66
|
*/
|
|
69
67
|
export const generalize = (options, ...geometries) => {
|
|
70
|
-
geometries = flatten(geometries)
|
|
71
|
-
if (geometries.length === 0) throw new Error('wrong number of arguments')
|
|
72
|
-
|
|
73
68
|
const results = geometries.map((geometry) => {
|
|
74
69
|
if (path2.isA(geometry)) return generalizePath2(options, geometry)
|
|
75
70
|
if (geom2.isA(geometry)) return generalizeGeom2(options, geometry)
|
|
76
71
|
if (geom3.isA(geometry)) return generalizeGeom3(options, geometry)
|
|
77
|
-
|
|
72
|
+
if (Array.isArray(geometry)) return generalize(options, ...geometry)
|
|
73
|
+
return geometry
|
|
78
74
|
})
|
|
79
75
|
return results.length === 1 ? results[0] : results
|
|
80
76
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* In all cases, these functions returns the results, and never changes the original geometry.
|
|
4
4
|
* @module modeling/modifiers
|
|
5
5
|
* @example
|
|
6
|
-
* import { generalize, snap } from '@jscad/modeling'
|
|
6
|
+
* import { generalize, snap, retessellate } from '@jscad/modeling'
|
|
7
7
|
*/
|
|
8
8
|
export { generalize } from './generalize.js'
|
|
9
9
|
export { snap } from './snap.js'
|
|
@@ -67,7 +67,6 @@ const calculateAngle = (prevVertex, midVertex, nextVertex, normal) => {
|
|
|
67
67
|
|
|
68
68
|
// create a polygon starting from the given edge (if possible)
|
|
69
69
|
const createPolygonAnd = (edge) => {
|
|
70
|
-
let polygon
|
|
71
70
|
const vertices = []
|
|
72
71
|
while (edge.next) {
|
|
73
72
|
const next = edge.next
|
|
@@ -81,8 +80,8 @@ const createPolygonAnd = (edge) => {
|
|
|
81
80
|
|
|
82
81
|
edge = next
|
|
83
82
|
}
|
|
84
|
-
if (vertices.length > 0)
|
|
85
|
-
return
|
|
83
|
+
if (vertices.length > 0) return poly3.create(vertices)
|
|
84
|
+
return null
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
/*
|
|
@@ -31,7 +31,7 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
|
|
|
31
31
|
// Make a list of all encountered y coordinates
|
|
32
32
|
// And build a map of all polygons that have a vertex at a certain y coordinate:
|
|
33
33
|
const yCoordinateBins = new Map()
|
|
34
|
-
const yCoordinateBinningFactor = 10 / EPS
|
|
34
|
+
const yCoordinateBinningFactor = 10 / EPS // FIXME
|
|
35
35
|
for (let polygonIndex = 0; polygonIndex < numPolygons; polygonIndex++) {
|
|
36
36
|
const poly3d = sourcePolygons[polygonIndex]
|
|
37
37
|
let vertices2d = []
|
|
@@ -68,7 +68,7 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
|
|
|
68
68
|
}
|
|
69
69
|
let polygonIndexes = yCoordinateToPolygonIndexes.get(y)
|
|
70
70
|
if (!polygonIndexes) {
|
|
71
|
-
polygonIndexes =
|
|
71
|
+
polygonIndexes = []
|
|
72
72
|
yCoordinateToPolygonIndexes.set(y, polygonIndexes)
|
|
73
73
|
}
|
|
74
74
|
polygonIndexes[polygonIndex] = true
|
|
@@ -242,7 +242,7 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
|
|
|
242
242
|
const prevOutPolygon = newOutPolygonRow[newOutPolygonRow.length - 1]
|
|
243
243
|
const d1 = vec2.distance(outPolygon.topLeft, prevOutPolygon.topRight)
|
|
244
244
|
const d2 = vec2.distance(outPolygon.bottomLeft, prevOutPolygon.bottomRight)
|
|
245
|
-
if ((d1 < EPS) && (d2 < EPS)) {
|
|
245
|
+
if ((d1 < EPS) && (d2 < EPS)) { // FIXME
|
|
246
246
|
// we can join this polygon with the one to the left:
|
|
247
247
|
outPolygon.topLeft = prevOutPolygon.topLeft
|
|
248
248
|
outPolygon.leftLine = prevOutPolygon.leftLine
|
|
@@ -263,8 +263,8 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
|
|
|
263
263
|
// We have a match if the sidelines are equal or if the top coordinates
|
|
264
264
|
// are on the sidelines of the previous polygon
|
|
265
265
|
const prevPolygon = prevOutPolygonRow[ii]
|
|
266
|
-
if (vec2.distance(prevPolygon.bottomLeft, thisPolygon.topLeft) < EPS) {
|
|
267
|
-
if (vec2.distance(prevPolygon.bottomRight, thisPolygon.topRight) < EPS) {
|
|
266
|
+
if (vec2.distance(prevPolygon.bottomLeft, thisPolygon.topLeft) < EPS) { // FIXME
|
|
267
|
+
if (vec2.distance(prevPolygon.bottomRight, thisPolygon.topRight) < EPS) { // FIXME
|
|
268
268
|
// Yes, the top of this polygon matches the bottom of the previous:
|
|
269
269
|
matchedIndexes.add(ii)
|
|
270
270
|
// Now check if the joined polygon would remain convex:
|
|
@@ -300,7 +300,7 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
|
|
|
300
300
|
// Finish the polygon with the last point(s):
|
|
301
301
|
const prevPolygon = prevOutPolygonRow[ii]
|
|
302
302
|
prevPolygon.outPolygon.rightPoints.push(prevPolygon.bottomRight)
|
|
303
|
-
if (vec2.distance(prevPolygon.bottomRight, prevPolygon.bottomLeft) > EPS) {
|
|
303
|
+
if (vec2.distance(prevPolygon.bottomRight, prevPolygon.bottomLeft) > EPS) { // FIXME
|
|
304
304
|
// polygon ends with a horizontal line:
|
|
305
305
|
prevPolygon.outPolygon.leftPoints.push(prevPolygon.bottomLeft)
|
|
306
306
|
}
|
|
@@ -324,7 +324,7 @@ export const reTesselateCoplanarPolygons = (sourcePolygons) => {
|
|
|
324
324
|
rightPoints: []
|
|
325
325
|
}
|
|
326
326
|
thisPolygon.outPolygon.leftPoints.push(thisPolygon.topLeft)
|
|
327
|
-
if (vec2.distance(thisPolygon.topLeft, thisPolygon.topRight) > EPS) {
|
|
327
|
+
if (vec2.distance(thisPolygon.topLeft, thisPolygon.topRight) > EPS) { // FIXME
|
|
328
328
|
// we have a horizontal line at the top:
|
|
329
329
|
thisPolygon.outPolygon.rightPoints.push(thisPolygon.topRight)
|
|
330
330
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { flatten } from '../../utils/flatten.js'
|
|
2
|
-
|
|
3
1
|
import * as vec2 from '../../maths/vec2/index.js'
|
|
4
2
|
|
|
5
3
|
import * as geom2 from '../../geometries/geom2/index.js'
|
|
@@ -48,20 +46,18 @@ const snapGeom3 = (geometry) => {
|
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
/**
|
|
51
|
-
* Snap the given geometries to the
|
|
49
|
+
* Snap the given geometries to the precision (calculated epsilon) of the geometry.
|
|
52
50
|
* @see measurements.measureEpsilon()
|
|
53
51
|
* @param {...Object} geometries - the geometries to snap
|
|
54
52
|
* @return {Object|Array} the snapped geometry, or a list of snapped geometries
|
|
55
53
|
* @alias module:modeling/modifiers.snap
|
|
56
54
|
*/
|
|
57
55
|
export const snap = (...geometries) => {
|
|
58
|
-
geometries = flatten(geometries)
|
|
59
|
-
if (geometries.length === 0) throw new Error('wrong number of arguments')
|
|
60
|
-
|
|
61
56
|
const results = geometries.map((geometry) => {
|
|
62
57
|
if (path2.isA(geometry)) return snapPath2(geometry)
|
|
63
58
|
if (geom2.isA(geometry)) return snapGeom2(geometry)
|
|
64
59
|
if (geom3.isA(geometry)) return snapGeom3(geometry)
|
|
60
|
+
if (Array.isArray(geometry)) return snap(...geometry)
|
|
65
61
|
return geometry
|
|
66
62
|
})
|
|
67
63
|
return results.length === 1 ? results[0] : results
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { flatten } from '../../utils/flatten.js'
|
|
2
|
-
|
|
3
1
|
import * as geom2 from '../../geometries/geom2/index.js'
|
|
4
2
|
import * as geom3 from '../../geometries/geom3/index.js'
|
|
5
3
|
import * as path2 from '../../geometries/path2/index.js'
|
|
@@ -23,13 +21,11 @@ import { offsetPath2 } from './offsetPath2.js'
|
|
|
23
21
|
* let small = offset({ delta: -4, corners: 'chamfer' }, square({size: 40})) // contract
|
|
24
22
|
*/
|
|
25
23
|
export const offset = (options, ...objects) => {
|
|
26
|
-
objects = flatten(objects)
|
|
27
|
-
if (objects.length === 0) throw new Error('wrong number of arguments')
|
|
28
|
-
|
|
29
24
|
const results = objects.map((object) => {
|
|
30
25
|
if (path2.isA(object)) return offsetPath2(options, object)
|
|
31
26
|
if (geom2.isA(object)) return offsetGeom2(options, object)
|
|
32
27
|
if (geom3.isA(object)) return offsetGeom3(options, object)
|
|
28
|
+
if (Array.isArray(object)) return offset(options, ...object)
|
|
33
29
|
return object
|
|
34
30
|
})
|
|
35
31
|
return results.length === 1 ? results[0] : results
|
|
@@ -200,7 +200,6 @@ test('offset (corners: edge): offset of a path2 produces expected offset path2',
|
|
|
200
200
|
|
|
201
201
|
test('offset (corners: round): offset of a path2 produces expected offset path2', (t) => {
|
|
202
202
|
const openline = [[-5, -5], [5, -5], [5, 5], [3, 5], [3, 0], [-3, 0], [-3, 5], [-5, 5]]
|
|
203
|
-
const closeline = [[-5, -5], [5, -5], [5, 5], [3, 5], [3, 0], [-3, 0], [-3, 5], [-5, 5], [-5, -5]]
|
|
204
203
|
|
|
205
204
|
let pts = offsetFromPoints({ delta: 1, corners: 'round', segments: 16 }, openline)
|
|
206
205
|
let obs = path2.create(pts)
|
|
@@ -3,8 +3,11 @@ import test from 'ava'
|
|
|
3
3
|
import { comparePoints } from '../../../test/helpers/index.js'
|
|
4
4
|
|
|
5
5
|
import { colorize } from '../../colors/index.js'
|
|
6
|
+
|
|
6
7
|
import { geom3, poly3 } from '../../geometries/index.js'
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
import { measureArea, measureVolume } from '../../measurements/index.js'
|
|
10
|
+
|
|
8
11
|
import { cube, sphere } from '../../primitives/index.js'
|
|
9
12
|
|
|
10
13
|
import { offset } from './index.js'
|
|
@@ -13,6 +16,7 @@ test('offset: offset empty geom3', (t) => {
|
|
|
13
16
|
const geometry = geom3.create()
|
|
14
17
|
const result = offset({ }, geometry)
|
|
15
18
|
t.notThrows(() => geom3.validate(result))
|
|
19
|
+
t.is(measureArea(result), 0)
|
|
16
20
|
t.is(measureVolume(result), 0)
|
|
17
21
|
t.is(geom3.toPolygons(result).length, 0)
|
|
18
22
|
t.is(geom3.toPoints(result).length, 0)
|
|
@@ -50,6 +54,8 @@ test('offset: offset of a geom3 produces expected changes to polygons', (t) => {
|
|
|
50
54
|
]
|
|
51
55
|
|
|
52
56
|
t.notThrows.skip(() => geom3.validate(obs))
|
|
57
|
+
t.is(measureArea(obs), 3178.8059464475555)
|
|
58
|
+
t.is(measureVolume(obs), 13504.574121271067)
|
|
53
59
|
t.is(pts.length, 62)
|
|
54
60
|
t.true(comparePoints(pts[0], exp0))
|
|
55
61
|
t.true(comparePoints(pts[61], exp61))
|
|
@@ -58,6 +64,8 @@ test('offset: offset of a geom3 produces expected changes to polygons', (t) => {
|
|
|
58
64
|
const obs2 = offset({ delta: 5 }, geometry2)
|
|
59
65
|
const pts2 = geom3.toPoints(obs2)
|
|
60
66
|
t.notThrows.skip(() => geom3.validate(obs2))
|
|
67
|
+
t.is(measureArea(obs), 3178.8059464475555)
|
|
68
|
+
t.is(measureVolume(obs), 13504.574121271067)
|
|
61
69
|
t.is(pts2.length, 864)
|
|
62
70
|
})
|
|
63
71
|
|
|
@@ -85,9 +85,9 @@ export const offsetPath2 = (options, geometry) => {
|
|
|
85
85
|
internal: offsetFromPoints({ delta: -delta, corners, segments, closed }, points)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const output = geometry.isClosed
|
|
89
|
-
createGeometryFromClosedPath(paths)
|
|
90
|
-
createGeometryFromOpenPath(paths, segments, corners, delta)
|
|
88
|
+
const output = geometry.isClosed
|
|
89
|
+
? createGeometryFromClosedPath(paths)
|
|
90
|
+
: createGeometryFromOpenPath(paths, segments, corners, delta)
|
|
91
91
|
if (geometry.color) output.color = geometry.color
|
|
92
92
|
return output
|
|
93
93
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as vec3 from '../../maths/vec3/index.js'
|
|
2
|
+
|
|
3
|
+
import { coalesce } from '../../utils/coalesce.js'
|
|
2
4
|
import { padArrayToLength } from '../../utils/padArrayToLength.js'
|
|
3
5
|
|
|
4
6
|
import { measureAggregateBoundingBox } from '../../measurements/measureAggregateBoundingBox.js'
|
|
@@ -34,9 +36,9 @@ const populateRelativeToFromBounds = (relativeTo, modes, bounds) => {
|
|
|
34
36
|
return relativeTo
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
const alignGeometries = (
|
|
38
|
-
const bounds = measureAggregateBoundingBox(
|
|
39
|
-
const translation =
|
|
39
|
+
const alignGeometries = (geometries, modes, relativeTo) => {
|
|
40
|
+
const bounds = measureAggregateBoundingBox(geometries)
|
|
41
|
+
const translation = vec3.create()
|
|
40
42
|
for (let i = 0; i < 3; i++) {
|
|
41
43
|
if (modes[i] === 'center') {
|
|
42
44
|
translation[i] = relativeTo[i] - (bounds[0][i] + bounds[1][i]) / 2
|
|
@@ -47,7 +49,7 @@ const alignGeometries = (geometry, modes, relativeTo) => {
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
return translate(translation,
|
|
52
|
+
return translate(translation, geometries)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
@@ -73,14 +75,13 @@ export const align = (options, ...geometries) => {
|
|
|
73
75
|
|
|
74
76
|
options = validateOptions(options)
|
|
75
77
|
let { modes, relativeTo, grouped } = options
|
|
76
|
-
geometries = flatten(geometries)
|
|
77
|
-
if (geometries.length === 0) throw new Error('align(): No geometries were provided to act upon')
|
|
78
78
|
|
|
79
79
|
if (relativeTo.filter((val) => val == null).length) {
|
|
80
80
|
const bounds = measureAggregateBoundingBox(geometries)
|
|
81
81
|
relativeTo = populateRelativeToFromBounds(relativeTo, modes, bounds)
|
|
82
82
|
}
|
|
83
83
|
if (grouped) {
|
|
84
|
+
geometries = coalesce(geometries)
|
|
84
85
|
geometries = alignGeometries(geometries, modes, relativeTo)
|
|
85
86
|
} else {
|
|
86
87
|
geometries = geometries.map((geometry) => alignGeometries(geometry, modes, relativeTo))
|
|
@@ -53,7 +53,7 @@ test('align: multiple objects ungrouped returns geometry aligned, different mode
|
|
|
53
53
|
cube({ size: 4, center: [10, 10, 10] }),
|
|
54
54
|
cube({ size: 2, center: [4, 4, 4] })
|
|
55
55
|
]
|
|
56
|
-
const aligned = align({ modes: ['center', 'min', 'max'], relativeTo: [30, 30, 30] }, original)
|
|
56
|
+
const aligned = align({ modes: ['center', 'min', 'max'], relativeTo: [30, 30, 30] }, ...original)
|
|
57
57
|
const bounds = measureAggregateBoundingBox(aligned)
|
|
58
58
|
const expectedBounds = [[28, 30, 26], [32, 34, 30]]
|
|
59
59
|
t.notThrows(() => geom3.validate(aligned[0]))
|
|
@@ -79,7 +79,7 @@ test('align: multiple objects ungrouped, relativeTo is nulls, returns geometry a
|
|
|
79
79
|
cube({ size: 2, center: [4, 4, 4] }),
|
|
80
80
|
cube({ size: 4, center: [10, 10, 10] })
|
|
81
81
|
]
|
|
82
|
-
const aligned = align({ modes: ['center', 'min', 'max'], relativeTo: [null, null, null], grouped: false }, original)
|
|
82
|
+
const aligned = align({ modes: ['center', 'min', 'max'], relativeTo: [null, null, null], grouped: false }, ...original)
|
|
83
83
|
const bounds = measureAggregateBoundingBox(aligned)
|
|
84
84
|
const expectedBounds = [[5.5, 3, 8], [9.5, 7, 12]]
|
|
85
85
|
t.notThrows(() => geom3.validate(aligned[0]))
|