brepjs 13.0.0 → 13.2.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/dist/2d.cjs +3 -3
- package/dist/2d.js +3 -3
- package/dist/{arrayAccess-CccV7jov.js → arrayAccess-Dps31ERU.js} +3 -3
- package/dist/{arrayAccess-BF8Hm4-H.cjs → arrayAccess-peFKE9Ob.cjs} +3 -3
- package/dist/{blueprint-mSGgCL3V.js → blueprint-DYCdRlW5.js} +8 -8
- package/dist/{blueprint-BP3P8Ado.cjs → blueprint-PLJan-W5.cjs} +8 -8
- package/dist/{blueprintFns-BQ-MP_Vy.cjs → blueprintFns-Bsx25BG7.cjs} +3 -3
- package/dist/{blueprintFns-DWFkjbDT.js → blueprintFns-eWh7NpZx.js} +3 -3
- package/dist/{boolean2D-Cqfwz60G.js → boolean2D-52qVCooY.js} +10 -10
- package/dist/{boolean2D-D76Hc7Wx.cjs → boolean2D-CtB21ajK.cjs} +10 -10
- package/dist/{booleanFns-CYPXeNVN.cjs → booleanFns-BrptUFkP.cjs} +25 -13
- package/dist/{booleanFns-B79ALtKn.js → booleanFns-iM6UPb8e.js} +25 -13
- package/dist/brepjs.cjs +43 -311
- package/dist/brepjs.js +23 -306
- package/dist/core/errors.d.ts +7 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core.cjs +3 -3
- package/dist/core.js +3 -3
- package/dist/{cornerFinder-BSwshSGB.js → cornerFinder-C7aDyYLJ.js} +2 -2
- package/dist/{cornerFinder-sg0JNgux.cjs → cornerFinder-SF-xmMO1.cjs} +2 -2
- package/dist/{curveFns-D_s3LdNT.js → curveFns-C-jU1_Y_.js} +2 -2
- package/dist/{curveFns-CJjkUGyW.cjs → curveFns-ywh7Ctyk.cjs} +2 -2
- package/dist/{drawFns-oyqai_HD.js → drawFns-D-0p86Lf.js} +13 -13
- package/dist/{drawFns-0CYuQn0J.cjs → drawFns-DknEB-Qs.cjs} +13 -13
- package/dist/{errors-B1fl3mAU.js → errors-B_T0aMQF.js} +7 -0
- package/dist/{errors-C85KVJr-.cjs → errors-DupKEMqI.cjs} +7 -0
- package/dist/{extrudeFns-0kBZvqJz.cjs → extrudeFns-CGCIbydL.cjs} +2 -2
- package/dist/{extrudeFns-lDvV4ir2.js → extrudeFns-LsH1rDMa.js} +2 -2
- package/dist/{faceFns-8BurpAGN.cjs → faceFns-8dGb8q3J.cjs} +2 -2
- package/dist/{faceFns-Bne5RIvn.js → faceFns-EnGcKFAr.js} +2 -2
- package/dist/{helpers-CxexSe1n.js → helpers-Rf0vhX6I.js} +6 -6
- package/dist/{helpers-D8DIMw2U.cjs → helpers-pQpV9Mwh.cjs} +6 -6
- package/dist/{historyFns-BTeasREV.js → historyFns-XkjLAQyu.js} +5 -5
- package/dist/{historyFns-BAzQr6EP.cjs → historyFns-lNalnOdR.cjs} +5 -5
- package/dist/{importFns-D__wN_Pq.cjs → importFns-BSH9cGIp.cjs} +36 -4
- package/dist/{importFns-clldr3EF.js → importFns-Bgs-FYAP.js} +31 -5
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/io/stepConfigFns.d.ts +27 -0
- package/dist/io/stepConfigFns.d.ts.map +1 -0
- package/dist/io.cjs +3 -2
- package/dist/io.d.ts +1 -0
- package/dist/io.d.ts.map +1 -1
- package/dist/io.js +3 -3
- package/dist/kernel/brepkit/booleanOps.d.ts +7 -1
- package/dist/kernel/brepkit/booleanOps.d.ts.map +1 -1
- package/dist/kernel/brepkit/brepkitAdapter.d.ts +5 -4
- package/dist/kernel/brepkit/brepkitAdapter.d.ts.map +1 -1
- package/dist/kernel/brepkit/evolutionOps.d.ts +4 -4
- package/dist/kernel/brepkit/evolutionOps.d.ts.map +1 -1
- package/dist/kernel/interfaces/booleanOps.d.ts +3 -1
- package/dist/kernel/interfaces/booleanOps.d.ts.map +1 -1
- package/dist/kernel/interfaces/evolutionOps.d.ts +4 -4
- package/dist/kernel/interfaces/evolutionOps.d.ts.map +1 -1
- package/dist/kernel/occt/booleanOps.d.ts +7 -1
- package/dist/kernel/occt/booleanOps.d.ts.map +1 -1
- package/dist/kernel/occt/defaultAdapter.d.ts +5 -4
- package/dist/kernel/occt/defaultAdapter.d.ts.map +1 -1
- package/dist/kernel/occt/evolutionOps.d.ts +2 -2
- package/dist/kernel/occt/evolutionOps.d.ts.map +1 -1
- package/dist/kernel/occt/historyOps.d.ts +4 -4
- package/dist/kernel/occt/historyOps.d.ts.map +1 -1
- package/dist/kernel/occt/wasmTypes/occtBuilders.d.ts +4 -0
- package/dist/kernel/occt/wasmTypes/occtBuilders.d.ts.map +1 -1
- package/dist/kernel/types.d.ts +30 -0
- package/dist/kernel/types.d.ts.map +1 -1
- package/dist/{measureFns-DrMZGJ6r.cjs → measureFns-CFdHa_fj.cjs} +3 -3
- package/dist/{measureFns-C4WqH4OT.js → measureFns-D7J6qUY_.js} +3 -3
- package/dist/measurement.cjs +1 -1
- package/dist/measurement.js +1 -1
- package/dist/{meshFns-DvOM43vV.cjs → meshFns-2XnDXgIh.cjs} +3 -3
- package/dist/{meshFns--M5PTyHG.js → meshFns-B7uklc4M.js} +3 -3
- package/dist/operations.cjs +2 -2
- package/dist/operations.js +2 -2
- package/dist/{planeOps-DPintPbl.js → planeOps-BuBXTLBr.js} +1 -1
- package/dist/{planeOps-DdkIuVjk.cjs → planeOps-cTxDywpG.cjs} +1 -1
- package/dist/primitiveFns-CASk8g16.js +1452 -0
- package/dist/primitiveFns-DKtvEA0i.cjs +1817 -0
- package/dist/query.cjs +2 -2
- package/dist/query.js +2 -2
- package/dist/result.cjs +1 -1
- package/dist/result.js +1 -1
- package/dist/{shapeTypes-GmE4D5Q_.cjs → shapeTypes-CElaawp7.cjs} +114 -9
- package/dist/{shapeTypes-D38b_BKF.js → shapeTypes-CYb8Byqj.js} +114 -9
- package/dist/sketching.cjs +2 -2
- package/dist/sketching.js +2 -2
- package/dist/{solidBuilders-FaTmd_PS.js → solidBuilders-ClJxiUa3.js} +3 -3
- package/dist/{solidBuilders-BqU0oT2q.cjs → solidBuilders-Cs4XyL58.cjs} +3 -3
- package/dist/{surfaceBuilders-DiCVk_Un.js → surfaceBuilders-DnGdDW8i.js} +3 -3
- package/dist/{surfaceBuilders-BzDQQ4EG.cjs → surfaceBuilders-ZUTb3z6i.cjs} +3 -3
- package/dist/topology/booleanDiagnosticFns.d.ts +18 -0
- package/dist/topology/booleanDiagnosticFns.d.ts.map +1 -0
- package/dist/topology/booleanFns.d.ts.map +1 -1
- package/dist/topology/evolutionFns.d.ts +71 -0
- package/dist/topology/evolutionFns.d.ts.map +1 -0
- package/dist/topology/healingFns.d.ts +17 -0
- package/dist/topology/healingFns.d.ts.map +1 -1
- package/dist/topology/modifierFns.d.ts +15 -0
- package/dist/topology/modifierFns.d.ts.map +1 -1
- package/dist/topology/positionFns.d.ts +15 -0
- package/dist/topology/positionFns.d.ts.map +1 -0
- package/dist/topology.cjs +18 -6
- package/dist/topology.d.ts +6 -1
- package/dist/topology.d.ts.map +1 -1
- package/dist/topology.js +7 -7
- package/dist/vectors.cjs +1 -1
- package/dist/vectors.js +1 -1
- package/package.json +1 -1
- package/dist/primitiveFns-BXufrcii.cjs +0 -1063
- package/dist/primitiveFns-u3Bbdvlw.js +0 -806
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
import { Y as getKernel, _ as isSolid, c as createSolid, h as isShape3D, p as isFace, r as castShapeWithKnownType, t as castShape, x as isClosedWire, y as isWire } from "./shapeTypes-CYb8Byqj.js";
|
|
2
|
+
import { C as isErr, _ as andThen, d as validationError, i as kernelError, k as ok, l as typeCastError, t as BrepErrorCode, w as isOk, y as err } from "./errors-B_T0aMQF.js";
|
|
3
|
+
import { _ as DEG2RAD, v as HASH_CODE_MAX } from "./vecOps-B9-MTeC8.js";
|
|
4
|
+
import { _ as downcast } from "./faceFns-EnGcKFAr.js";
|
|
5
|
+
import { F as getEdges, I as getFaces, L as getOrCreateCache, b as propagateAllMetadata, v as translate, y as collectInputFaceHashes, z as getWires } from "./arrayAccess-Dps31ERU.js";
|
|
6
|
+
import { a as makeNonPlanarFace, c as makeBSplineApproximation, d as makeEllipse, f as makeEllipseArc, g as makeThreePointArc, h as makeTangentArc, i as makeNewFaceWithinFace, l as makeBezierCurve, m as makeLine, o as makePolygon, p as makeHelix, r as makeFace, s as assembleWire, t as addHolesInFace, u as makeCircle } from "./surfaceBuilders-DnGdDW8i.js";
|
|
7
|
+
import { a as makeOffset, c as makeTorus, i as makeEllipsoid, l as makeVertex, n as makeCone, o as makeSolid, r as makeCylinder, s as makeSphere, t as makeCompound, u as weldShellsAndFaces } from "./solidBuilders-ClJxiUa3.js";
|
|
8
|
+
//#region src/topology/threeHelpers.ts
|
|
9
|
+
/**
|
|
10
|
+
* Convert a ShapeMesh into BufferGeometry-compatible typed arrays.
|
|
11
|
+
*
|
|
12
|
+
* The returned arrays can be used directly with Three.js:
|
|
13
|
+
* ```ts
|
|
14
|
+
* const geo = new THREE.BufferGeometry();
|
|
15
|
+
* geo.setAttribute('position', new THREE.BufferAttribute(data.position, 3));
|
|
16
|
+
* geo.setAttribute('normal', new THREE.BufferAttribute(data.normal, 3));
|
|
17
|
+
* geo.setIndex(new THREE.BufferAttribute(data.index, 1));
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function toBufferGeometryData(mesh) {
|
|
21
|
+
return {
|
|
22
|
+
position: mesh.vertices,
|
|
23
|
+
normal: mesh.normals,
|
|
24
|
+
index: mesh.triangles
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Convert a ShapeMesh into grouped BufferGeometry data with face material groups.
|
|
29
|
+
*
|
|
30
|
+
* Each face becomes a separate group, allowing per-face materials in Three.js:
|
|
31
|
+
* ```ts
|
|
32
|
+
* const data = toGroupedBufferGeometryData(mesh);
|
|
33
|
+
* const geo = new THREE.BufferGeometry();
|
|
34
|
+
* geo.setAttribute('position', new THREE.BufferAttribute(data.position, 3));
|
|
35
|
+
* geo.setAttribute('normal', new THREE.BufferAttribute(data.normal, 3));
|
|
36
|
+
* geo.setIndex(new THREE.BufferAttribute(data.index, 1));
|
|
37
|
+
* for (const g of data.groups) {
|
|
38
|
+
* geo.addGroup(g.start, g.count, g.materialIndex);
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
function toGroupedBufferGeometryData(mesh) {
|
|
43
|
+
return {
|
|
44
|
+
position: mesh.vertices,
|
|
45
|
+
normal: mesh.normals,
|
|
46
|
+
index: mesh.triangles,
|
|
47
|
+
groups: mesh.faceGroups.map((g, i) => ({
|
|
48
|
+
start: g.start,
|
|
49
|
+
count: g.count,
|
|
50
|
+
materialIndex: i,
|
|
51
|
+
faceId: g.faceId
|
|
52
|
+
}))
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Convert an EdgeMesh into position data for THREE.LineSegments.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const geo = new THREE.BufferGeometry();
|
|
60
|
+
* geo.setAttribute('position', new THREE.BufferAttribute(data.position, 3));
|
|
61
|
+
* const lines = new THREE.LineSegments(geo, material);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function toLineGeometryData(mesh) {
|
|
65
|
+
return { position: mesh.lines };
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/topology/chamferAngleFns.ts
|
|
69
|
+
/**
|
|
70
|
+
* Chamfer with distance + angle — functional API.
|
|
71
|
+
*
|
|
72
|
+
* Provides chamferDistAngle() which chamfers edges using a distance
|
|
73
|
+
* measured along one face and an angle to determine the chamfer on the other.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Chamfer edges of a shape using distance + angle.
|
|
77
|
+
*
|
|
78
|
+
* The distance is measured along the face that contains the edge, and the
|
|
79
|
+
* angle (in degrees) determines how the chamfer cuts into the adjacent face.
|
|
80
|
+
*
|
|
81
|
+
* @param shape - The 3D shape to chamfer.
|
|
82
|
+
* @param edges - Edges to chamfer (must not be empty).
|
|
83
|
+
* @param distance - Chamfer distance along the face (must be positive).
|
|
84
|
+
* @param angleDeg - Chamfer angle in degrees (must be in range (0, 90)).
|
|
85
|
+
* @returns Ok with the chamfered shape, or Err on invalid input or kernel failure.
|
|
86
|
+
*
|
|
87
|
+
* @remarks Uses `BRepFilletAPI_MakeChamfer.AddDA(dist, angle, edge, face)` internally.
|
|
88
|
+
*/
|
|
89
|
+
function chamferDistAngle(shape, edges, distance, angleDeg) {
|
|
90
|
+
if (edges.length === 0) return err(validationError("CHAMFER_ANGLE_NO_EDGES", "chamferDistAngle requires at least one edge", void 0, { edgeCount: 0 }));
|
|
91
|
+
if (distance <= 0) return err(validationError("CHAMFER_ANGLE_BAD_DISTANCE", `distance must be positive, got ${distance}`, void 0, { distance }));
|
|
92
|
+
if (angleDeg <= 0 || angleDeg >= 90) return err(validationError("CHAMFER_ANGLE_BAD_ANGLE", `angleDeg must be in range (0, 90), got ${angleDeg}`, void 0, { angleDeg }));
|
|
93
|
+
let raw;
|
|
94
|
+
try {
|
|
95
|
+
const kernel = getKernel();
|
|
96
|
+
const rawEdges = edges.map((e) => e.wrapped);
|
|
97
|
+
raw = kernel.chamferDistAngle(shape.wrapped, rawEdges, distance, angleDeg);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return err(kernelError("CHAMFER_ANGLE_FAILED", `chamferDistAngle kernel call failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
100
|
+
distance,
|
|
101
|
+
angleDeg,
|
|
102
|
+
edgeCount: edges.length
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
const downcastResult = downcast(raw);
|
|
106
|
+
if (isErr(downcastResult)) return downcastResult;
|
|
107
|
+
const wrapped = castShape(downcastResult.value);
|
|
108
|
+
if (!isShape3D(wrapped)) {
|
|
109
|
+
wrapped[Symbol.dispose]();
|
|
110
|
+
return err(typeCastError("CHAMFER_ANGLE_NOT_3D", "chamferDistAngle did not produce a 3D shape"));
|
|
111
|
+
}
|
|
112
|
+
return ok(wrapped);
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/topology/adjacencyFns.ts
|
|
116
|
+
/**
|
|
117
|
+
* Topology adjacency queries — find related sub-shapes within a parent shape.
|
|
118
|
+
*
|
|
119
|
+
* Uses cached topology extraction and an edge→faces adjacency map
|
|
120
|
+
* (built once per parent shape and cached) to avoid redundant WASM calls.
|
|
121
|
+
*/
|
|
122
|
+
function wrapAll(shapes, type) {
|
|
123
|
+
return shapes.map((s) => castShapeWithKnownType(s, type));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Iterate sub-shapes of `parentKernel` of the given `type`, deduplicate by
|
|
127
|
+
* hash+isSame, and return branded handles of type `T`.
|
|
128
|
+
*
|
|
129
|
+
* Used by edgesOfFace, wiresOfFace, and verticesOfEdge — all of which need
|
|
130
|
+
* the same deduplicated-children pattern on a raw KernelShape.
|
|
131
|
+
*/
|
|
132
|
+
function deduplicatedSubShapes(parentKernel, type) {
|
|
133
|
+
const kernel = getKernel();
|
|
134
|
+
const items = kernel.iterShapes(parentKernel, type);
|
|
135
|
+
const results = [];
|
|
136
|
+
const seen = /* @__PURE__ */ new Map();
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
const hash = kernel.hashCode(item, HASH_CODE_MAX);
|
|
139
|
+
const bucket = seen.get(hash);
|
|
140
|
+
if (!bucket) {
|
|
141
|
+
seen.set(hash, [item]);
|
|
142
|
+
results.push(item);
|
|
143
|
+
} else if (!bucket.some((r) => kernel.isSame(r, item))) {
|
|
144
|
+
bucket.push(item);
|
|
145
|
+
results.push(item);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return wrapAll(results, type);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build or retrieve the cached edge→faces adjacency map for a parent shape.
|
|
152
|
+
* Maps edge hash codes to edge-face pairs, storing the edge alongside each
|
|
153
|
+
* face so facesOfEdge can verify via isSame without re-extracting face edges.
|
|
154
|
+
*/
|
|
155
|
+
function getEdgeToFacesMap(parent) {
|
|
156
|
+
const cache = getOrCreateCache(parent);
|
|
157
|
+
if (cache.edgeToFaces) return cache.edgeToFaces;
|
|
158
|
+
const kernel = getKernel();
|
|
159
|
+
const edgeToFaces = /* @__PURE__ */ new Map();
|
|
160
|
+
const allFaces = kernel.iterShapes(parent.wrapped, "face");
|
|
161
|
+
for (const f of allFaces) {
|
|
162
|
+
const edges = kernel.iterShapes(f, "edge");
|
|
163
|
+
for (const e of edges) {
|
|
164
|
+
const hash = kernel.hashCode(e, HASH_CODE_MAX);
|
|
165
|
+
let bucket = edgeToFaces.get(hash);
|
|
166
|
+
if (!bucket) {
|
|
167
|
+
bucket = [];
|
|
168
|
+
edgeToFaces.set(hash, bucket);
|
|
169
|
+
}
|
|
170
|
+
if (!bucket.some((entry) => kernel.isSame(entry.edge, e) && kernel.isSame(entry.face, f))) bucket.push({
|
|
171
|
+
edge: e,
|
|
172
|
+
face: f
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
cache.edgeToFaces = edgeToFaces;
|
|
177
|
+
return edgeToFaces;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get all faces adjacent to a given edge within a parent shape.
|
|
181
|
+
*
|
|
182
|
+
* An edge typically borders exactly two faces in a solid, or one face
|
|
183
|
+
* if the edge is on a boundary.
|
|
184
|
+
*
|
|
185
|
+
* @param parent - The parent shape to search within.
|
|
186
|
+
* @param edge - The edge whose adjacent faces to find.
|
|
187
|
+
* @returns Array of unique faces containing the given edge.
|
|
188
|
+
*/
|
|
189
|
+
function facesOfEdge(parent, edge) {
|
|
190
|
+
const kernel = getKernel();
|
|
191
|
+
const edgeToFaces = getEdgeToFacesMap(parent);
|
|
192
|
+
const hash = kernel.hashCode(edge.wrapped, HASH_CODE_MAX);
|
|
193
|
+
const bucket = edgeToFaces.get(hash) ?? [];
|
|
194
|
+
const results = [];
|
|
195
|
+
const seen = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const entry of bucket) {
|
|
197
|
+
if (!kernel.isSame(entry.edge, edge.wrapped)) continue;
|
|
198
|
+
const fHash = kernel.hashCode(entry.face, HASH_CODE_MAX);
|
|
199
|
+
const fBucket = seen.get(fHash);
|
|
200
|
+
if (!fBucket) {
|
|
201
|
+
seen.set(fHash, [entry.face]);
|
|
202
|
+
results.push(entry.face);
|
|
203
|
+
} else if (!fBucket.some((r) => kernel.isSame(r, entry.face))) {
|
|
204
|
+
fBucket.push(entry.face);
|
|
205
|
+
results.push(entry.face);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return wrapAll(results, "face");
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get all edges bounding a face.
|
|
212
|
+
*
|
|
213
|
+
* @param face - The face whose edges to enumerate.
|
|
214
|
+
* @returns Array of unique edges forming the face boundary.
|
|
215
|
+
*/
|
|
216
|
+
function edgesOfFace(face) {
|
|
217
|
+
return deduplicatedSubShapes(face.wrapped, "edge");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get all wires of a face (outer wire + inner hole wires).
|
|
221
|
+
* All wires bounding a face are closed by definition.
|
|
222
|
+
*
|
|
223
|
+
* @param face - The face whose wires to enumerate.
|
|
224
|
+
*/
|
|
225
|
+
function wiresOfFace(face) {
|
|
226
|
+
return deduplicatedSubShapes(face.wrapped, "wire");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the start and end vertices of an edge.
|
|
230
|
+
*
|
|
231
|
+
* @param edge - The edge whose vertices to retrieve.
|
|
232
|
+
* @returns Array of 1-2 vertices (1 if degenerate/closed, 2 otherwise).
|
|
233
|
+
*/
|
|
234
|
+
function verticesOfEdge(edge) {
|
|
235
|
+
return deduplicatedSubShapes(edge.wrapped, "vertex");
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get all faces that share at least one edge with the given face.
|
|
239
|
+
*
|
|
240
|
+
* The returned list does not include the input face itself.
|
|
241
|
+
* Uses the cached edge→faces adjacency map for the parent shape.
|
|
242
|
+
*
|
|
243
|
+
* @param parent - The parent shape to search within.
|
|
244
|
+
* @param face - The face whose neighbors to find.
|
|
245
|
+
* @returns Array of unique adjacent faces (excluding the input face).
|
|
246
|
+
*/
|
|
247
|
+
function adjacentFaces(parent, face) {
|
|
248
|
+
const kernel = getKernel();
|
|
249
|
+
const edgeToFaces = getEdgeToFacesMap(parent);
|
|
250
|
+
const faceEdgeHandles = deduplicatedSubShapes(face.wrapped, "edge");
|
|
251
|
+
const neighborRaw = [];
|
|
252
|
+
const seen = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const edgeHandle of faceEdgeHandles) {
|
|
254
|
+
const hash = kernel.hashCode(edgeHandle.wrapped, HASH_CODE_MAX);
|
|
255
|
+
const entries = edgeToFaces.get(hash) ?? [];
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (kernel.isSame(entry.face, face.wrapped)) continue;
|
|
258
|
+
const fHash = kernel.hashCode(entry.face, HASH_CODE_MAX);
|
|
259
|
+
const bucket = seen.get(fHash);
|
|
260
|
+
if (!bucket) {
|
|
261
|
+
seen.set(fHash, [entry.face]);
|
|
262
|
+
neighborRaw.push(entry.face);
|
|
263
|
+
} else if (!bucket.some((r) => kernel.isSame(r, entry.face))) {
|
|
264
|
+
bucket.push(entry.face);
|
|
265
|
+
neighborRaw.push(entry.face);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return wrapAll(neighborRaw, "face");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get all edges shared between two faces.
|
|
273
|
+
*
|
|
274
|
+
* @param face1 - The first face.
|
|
275
|
+
* @param face2 - The second face.
|
|
276
|
+
* @returns Array of edges present in both faces (via isSame comparison).
|
|
277
|
+
*/
|
|
278
|
+
function sharedEdges(face1, face2) {
|
|
279
|
+
const kernel = getKernel();
|
|
280
|
+
const edges1 = kernel.iterShapes(face1.wrapped, "edge");
|
|
281
|
+
const edges2 = kernel.iterShapes(face2.wrapped, "edge");
|
|
282
|
+
const edge2Map = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const e2 of edges2) {
|
|
284
|
+
const hash = kernel.hashCode(e2, HASH_CODE_MAX);
|
|
285
|
+
let bucket = edge2Map.get(hash);
|
|
286
|
+
if (!bucket) {
|
|
287
|
+
bucket = [];
|
|
288
|
+
edge2Map.set(hash, bucket);
|
|
289
|
+
}
|
|
290
|
+
bucket.push(e2);
|
|
291
|
+
}
|
|
292
|
+
const shared = [];
|
|
293
|
+
for (const e1 of edges1) if (edge2Map.get(kernel.hashCode(e1, 2147483647))?.some((e2) => kernel.isSame(e1, e2))) shared.push(e1);
|
|
294
|
+
return wrapAll(shared, "edge");
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/topology/booleanDiagnosticFns.ts
|
|
298
|
+
/**
|
|
299
|
+
* Boolean pre-validation diagnostics.
|
|
300
|
+
*/
|
|
301
|
+
/**
|
|
302
|
+
* Pre-validate operands before a boolean operation.
|
|
303
|
+
*
|
|
304
|
+
* Checks that both shapes are non-null and topologically valid.
|
|
305
|
+
* Returns a structured report of any issues found.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const check = checkBoolean(base, tool, 'fuse');
|
|
310
|
+
* if (!check.valid) {
|
|
311
|
+
* console.warn('Boolean will likely fail:', check.issues);
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
function checkBoolean(base, tool, op) {
|
|
316
|
+
return getKernel().checkBoolean(base.wrapped, tool.wrapped, op);
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/topology/evolutionFns.ts
|
|
320
|
+
/**
|
|
321
|
+
* Evolution-tracking variants of boolean and modifier operations.
|
|
322
|
+
*
|
|
323
|
+
* These functions mirror the standard fuse/cut/intersect/fillet/chamfer/shell
|
|
324
|
+
* operations but additionally return the ShapeEvolution data, enabling
|
|
325
|
+
* persistent face selections, constraint tracking, and custom face-level logic.
|
|
326
|
+
*/
|
|
327
|
+
function validateShape3D(shape, label) {
|
|
328
|
+
if (getKernel().isNull(shape.wrapped)) return err(validationError(BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
329
|
+
return ok(void 0);
|
|
330
|
+
}
|
|
331
|
+
function validateNotNull$1(shape, label) {
|
|
332
|
+
if (getKernel().isNull(shape.wrapped)) return err(validationError(BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
333
|
+
return ok(void 0);
|
|
334
|
+
}
|
|
335
|
+
function castToShape3D(shape, errorCode, errorMsg, suggestion) {
|
|
336
|
+
const wrapped = castShape(shape);
|
|
337
|
+
if (!isShape3D(wrapped)) {
|
|
338
|
+
const shapeType = shape.ShapeType();
|
|
339
|
+
const typeName = [
|
|
340
|
+
"COMPOUND",
|
|
341
|
+
"COMPSOLID",
|
|
342
|
+
"SOLID",
|
|
343
|
+
"SHELL",
|
|
344
|
+
"FACE",
|
|
345
|
+
"WIRE",
|
|
346
|
+
"EDGE",
|
|
347
|
+
"VERTEX",
|
|
348
|
+
"SHAPE"
|
|
349
|
+
][shapeType] ?? `UNKNOWN(${shapeType})`;
|
|
350
|
+
wrapped[Symbol.dispose]();
|
|
351
|
+
return err(typeCastError(errorCode, `${errorMsg}. Got ${typeName} instead.`, void 0, void 0, suggestion));
|
|
352
|
+
}
|
|
353
|
+
return ok(wrapped);
|
|
354
|
+
}
|
|
355
|
+
function resolveEdgeCallback$1(selectedEdges, callbackFn) {
|
|
356
|
+
const filteredEdges = [];
|
|
357
|
+
const hashToValue = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const edge of selectedEdges) {
|
|
359
|
+
const val = callbackFn(edge) ?? 0;
|
|
360
|
+
if (typeof val === "number" && val <= 0) continue;
|
|
361
|
+
if (Array.isArray(val) && (val[0] <= 0 || val[1] <= 0)) continue;
|
|
362
|
+
filteredEdges.push(edge);
|
|
363
|
+
hashToValue.set(getKernel().hashCode(edge.wrapped, HASH_CODE_MAX), val);
|
|
364
|
+
}
|
|
365
|
+
if (filteredEdges.length === 0) return null;
|
|
366
|
+
const kernelParam = (ocEdge) => {
|
|
367
|
+
return hashToValue.get(ocEdge.HashCode(2147483647)) ?? 1;
|
|
368
|
+
};
|
|
369
|
+
return {
|
|
370
|
+
edges: filteredEdges,
|
|
371
|
+
kernelParam
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function fuseWithEvolution(a, b, { optimisation = "none", simplify = false, signal, fuzzyValue } = {}) {
|
|
375
|
+
if (signal?.aborted) throw signal.reason;
|
|
376
|
+
const checkA = validateShape3D(a, "fuseWithEvolution: first operand");
|
|
377
|
+
if (isErr(checkA)) return checkA;
|
|
378
|
+
const checkB = validateShape3D(b, "fuseWithEvolution: second operand");
|
|
379
|
+
if (isErr(checkB)) return checkB;
|
|
380
|
+
const inputFaceHashes = collectInputFaceHashes([a, b]);
|
|
381
|
+
const { shape: resultShape, evolution } = getKernel().fuseWithHistory(a.wrapped, b.wrapped, inputFaceHashes, HASH_CODE_MAX, {
|
|
382
|
+
optimisation,
|
|
383
|
+
simplify,
|
|
384
|
+
fuzzyValue
|
|
385
|
+
});
|
|
386
|
+
const fuseResult = castToShape3D(resultShape, "FUSE_NOT_3D", "Fuse did not produce a 3D shape", "Common causes: overlapping coplanar faces, zero-thickness geometry, or non-manifold input. Try autoHeal() on inputs first.");
|
|
387
|
+
if (fuseResult.ok) {
|
|
388
|
+
propagateAllMetadata(evolution, [a, b], fuseResult.value);
|
|
389
|
+
return ok({
|
|
390
|
+
shape: fuseResult.value,
|
|
391
|
+
evolution
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return fuseResult;
|
|
395
|
+
}
|
|
396
|
+
function cutWithEvolution(base, tool, { optimisation = "none", simplify = false, signal, fuzzyValue } = {}) {
|
|
397
|
+
if (signal?.aborted) throw signal.reason;
|
|
398
|
+
const checkBase = validateShape3D(base, "cutWithEvolution: base");
|
|
399
|
+
if (isErr(checkBase)) return checkBase;
|
|
400
|
+
const checkTool = validateShape3D(tool, "cutWithEvolution: tool");
|
|
401
|
+
if (isErr(checkTool)) return checkTool;
|
|
402
|
+
const inputFaceHashes = collectInputFaceHashes([base, tool]);
|
|
403
|
+
const { shape: resultShape, evolution } = getKernel().cutWithHistory(base.wrapped, tool.wrapped, inputFaceHashes, HASH_CODE_MAX, {
|
|
404
|
+
optimisation,
|
|
405
|
+
simplify,
|
|
406
|
+
fuzzyValue
|
|
407
|
+
});
|
|
408
|
+
const cutResult = castToShape3D(resultShape, "CUT_NOT_3D", "Cut did not produce a 3D shape", "Common causes: tool does not fully intersect the base, or produces a zero-thickness sliver. Ensure the tool extends through the shape.");
|
|
409
|
+
if (cutResult.ok) {
|
|
410
|
+
propagateAllMetadata(evolution, [base, tool], cutResult.value);
|
|
411
|
+
return ok({
|
|
412
|
+
shape: cutResult.value,
|
|
413
|
+
evolution
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return cutResult;
|
|
417
|
+
}
|
|
418
|
+
function intersectWithEvolution(a, b, { simplify = false, signal, fuzzyValue } = {}) {
|
|
419
|
+
if (signal?.aborted) throw signal.reason;
|
|
420
|
+
const checkA = validateShape3D(a, "intersectWithEvolution: first operand");
|
|
421
|
+
if (isErr(checkA)) return checkA;
|
|
422
|
+
const checkB = validateShape3D(b, "intersectWithEvolution: second operand");
|
|
423
|
+
if (isErr(checkB)) return checkB;
|
|
424
|
+
const inputFaceHashes = collectInputFaceHashes([a, b]);
|
|
425
|
+
const { shape: resultShape, evolution } = getKernel().intersectWithHistory(a.wrapped, b.wrapped, inputFaceHashes, HASH_CODE_MAX, {
|
|
426
|
+
simplify,
|
|
427
|
+
fuzzyValue
|
|
428
|
+
});
|
|
429
|
+
const intResult = castToShape3D(resultShape, "INTERSECT_NOT_3D", "Intersect did not produce a 3D shape", "Shapes may not overlap. Verify they share a common volume before intersecting.");
|
|
430
|
+
if (intResult.ok) {
|
|
431
|
+
propagateAllMetadata(evolution, [a, b], intResult.value);
|
|
432
|
+
return ok({
|
|
433
|
+
shape: intResult.value,
|
|
434
|
+
evolution
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return intResult;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Apply a fillet (rounded edge) to selected edges, returning both
|
|
441
|
+
* the result shape and the face evolution data.
|
|
442
|
+
*
|
|
443
|
+
* @param shape - The shape to modify.
|
|
444
|
+
* @param edges - Edges to fillet. Pass `undefined` to fillet all edges.
|
|
445
|
+
* @param radius - Constant radius, variable radius `[r1, r2]`, or per-edge callback.
|
|
446
|
+
*/
|
|
447
|
+
function filletWithEvolution(shape, edges, radius) {
|
|
448
|
+
const check = validateNotNull$1(shape, "filletWithEvolution: shape");
|
|
449
|
+
if (isErr(check)) return check;
|
|
450
|
+
if (typeof radius === "number" && radius <= 0) return err(validationError("INVALID_FILLET_RADIUS", "Fillet radius must be positive", void 0, void 0, "Provide a positive radius value greater than 0"));
|
|
451
|
+
if (Array.isArray(radius) && (radius[0] <= 0 || radius[1] <= 0)) return err(validationError("INVALID_FILLET_RADIUS", "Fillet radii must both be positive", void 0, void 0, "Both radius values must be greater than 0"));
|
|
452
|
+
const selectedEdges = edges ?? getEdges(shape);
|
|
453
|
+
if (selectedEdges.length === 0) return err(validationError(BrepErrorCode.FILLET_NO_EDGES, "No edges found for fillet", void 0, void 0, "Check that the shape has edges, or adjust your edge finder criteria"));
|
|
454
|
+
try {
|
|
455
|
+
let filteredEdges;
|
|
456
|
+
let kernelRadius;
|
|
457
|
+
if (typeof radius === "function") {
|
|
458
|
+
const resolved = resolveEdgeCallback$1(selectedEdges, radius);
|
|
459
|
+
if (!resolved) return err(validationError(BrepErrorCode.FILLET_NO_EDGES, "No edges with positive radius for fillet", void 0, void 0, "Check that the radius callback returns positive values"));
|
|
460
|
+
filteredEdges = resolved.edges;
|
|
461
|
+
kernelRadius = resolved.kernelParam;
|
|
462
|
+
} else {
|
|
463
|
+
filteredEdges = [...selectedEdges];
|
|
464
|
+
kernelRadius = radius;
|
|
465
|
+
}
|
|
466
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
467
|
+
const { shape: resultShape, evolution } = getKernel().filletWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelRadius, inputFaceHashes, HASH_CODE_MAX);
|
|
468
|
+
const cast = castShape(resultShape);
|
|
469
|
+
if (!isShape3D(cast)) return err(kernelError(BrepErrorCode.FILLET_NOT_3D, "Fillet result is not a 3D shape"));
|
|
470
|
+
propagateAllMetadata(evolution, [shape], cast);
|
|
471
|
+
return ok({
|
|
472
|
+
shape: cast,
|
|
473
|
+
evolution
|
|
474
|
+
});
|
|
475
|
+
} catch (e) {
|
|
476
|
+
return err(kernelError("FILLET_FAILED", `Fillet operation failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
477
|
+
operation: "fillet",
|
|
478
|
+
edgeCount: selectedEdges.length,
|
|
479
|
+
radius
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Apply a chamfer (beveled edge) to selected edges, returning both
|
|
485
|
+
* the result shape and the face evolution data.
|
|
486
|
+
*
|
|
487
|
+
* @param shape - The shape to modify.
|
|
488
|
+
* @param edges - Edges to chamfer. Pass `undefined` to chamfer all edges.
|
|
489
|
+
* @param distance - Symmetric distance, asymmetric `[d1, d2]`, or per-edge callback.
|
|
490
|
+
*/
|
|
491
|
+
function chamferWithEvolution(shape, edges, distance) {
|
|
492
|
+
const check = validateNotNull$1(shape, "chamferWithEvolution: shape");
|
|
493
|
+
if (isErr(check)) return check;
|
|
494
|
+
if (typeof distance === "number" && distance <= 0) return err(validationError("INVALID_CHAMFER_DISTANCE", "Chamfer distance must be positive", void 0, void 0, "Provide a positive distance value greater than 0"));
|
|
495
|
+
if (Array.isArray(distance) && (distance[0] <= 0 || distance[1] <= 0)) return err(validationError("INVALID_CHAMFER_DISTANCE", "Chamfer distances must both be positive", void 0, void 0, "Both distance values must be greater than 0"));
|
|
496
|
+
const selectedEdges = edges ?? getEdges(shape);
|
|
497
|
+
if (selectedEdges.length === 0) return err(validationError(BrepErrorCode.CHAMFER_NO_EDGES, "No edges found for chamfer"));
|
|
498
|
+
try {
|
|
499
|
+
let filteredEdges;
|
|
500
|
+
let kernelDistance;
|
|
501
|
+
if (typeof distance === "function") {
|
|
502
|
+
const resolved = resolveEdgeCallback$1(selectedEdges, distance);
|
|
503
|
+
if (!resolved) return err(validationError(BrepErrorCode.CHAMFER_NO_EDGES, "No edges with positive distance for chamfer"));
|
|
504
|
+
filteredEdges = resolved.edges;
|
|
505
|
+
kernelDistance = resolved.kernelParam;
|
|
506
|
+
} else {
|
|
507
|
+
filteredEdges = [...selectedEdges];
|
|
508
|
+
kernelDistance = distance;
|
|
509
|
+
}
|
|
510
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
511
|
+
const { shape: resultShape, evolution } = getKernel().chamferWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelDistance, inputFaceHashes, HASH_CODE_MAX);
|
|
512
|
+
const cast = castShape(resultShape);
|
|
513
|
+
if (!isShape3D(cast)) return err(kernelError(BrepErrorCode.CHAMFER_NOT_3D, "Chamfer result is not a 3D shape"));
|
|
514
|
+
propagateAllMetadata(evolution, [shape], cast);
|
|
515
|
+
return ok({
|
|
516
|
+
shape: cast,
|
|
517
|
+
evolution
|
|
518
|
+
});
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return err(kernelError("CHAMFER_FAILED", `Chamfer operation failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
521
|
+
operation: "chamfer",
|
|
522
|
+
edgeCount: selectedEdges.length,
|
|
523
|
+
distance
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Create a hollow shell by removing faces and offsetting remaining walls,
|
|
529
|
+
* returning both the result shape and the face evolution data.
|
|
530
|
+
*
|
|
531
|
+
* @param shape - The solid to hollow out.
|
|
532
|
+
* @param faces - Faces to remove.
|
|
533
|
+
* @param thickness - Wall thickness.
|
|
534
|
+
* @param tolerance - Shell operation tolerance (default 1e-3).
|
|
535
|
+
*/
|
|
536
|
+
function shellWithEvolution(shape, faces, thickness, tolerance = .001) {
|
|
537
|
+
const check = validateNotNull$1(shape, "shellWithEvolution: shape");
|
|
538
|
+
if (isErr(check)) return check;
|
|
539
|
+
if (thickness <= 0) return err(validationError("INVALID_THICKNESS", "Shell thickness must be positive"));
|
|
540
|
+
if (faces.length === 0) return err(validationError("NO_FACES", "At least one face must be specified for shell"));
|
|
541
|
+
try {
|
|
542
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
543
|
+
const { shape: resultShape, evolution } = getKernel().shellWithHistory(shape.wrapped, faces.map((f) => f.wrapped), thickness, inputFaceHashes, HASH_CODE_MAX, tolerance);
|
|
544
|
+
const cast = castShape(resultShape);
|
|
545
|
+
if (!isShape3D(cast)) return err(kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
546
|
+
propagateAllMetadata(evolution, [shape], cast);
|
|
547
|
+
return ok({
|
|
548
|
+
shape: cast,
|
|
549
|
+
evolution
|
|
550
|
+
});
|
|
551
|
+
} catch (e) {
|
|
552
|
+
return err(kernelError("SHELL_FAILED", `Shell operation failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
553
|
+
operation: "shell",
|
|
554
|
+
faceCount: faces.length,
|
|
555
|
+
thickness
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/topology/positionFns.ts
|
|
561
|
+
/**
|
|
562
|
+
* Curve-based positioning operations.
|
|
563
|
+
*/
|
|
564
|
+
/**
|
|
565
|
+
* Position a shape at a point along a spine curve with Frenet frame orientation.
|
|
566
|
+
*
|
|
567
|
+
* The shape is translated and rotated so its origin aligns with the curve point
|
|
568
|
+
* and its Z axis aligns with the curve tangent at the given parameter.
|
|
569
|
+
*
|
|
570
|
+
* @param shape - The shape to position.
|
|
571
|
+
* @param spine - The spine curve (Edge or Wire) to position along.
|
|
572
|
+
* @param param - Normalized parameter (0 = start, 1 = end).
|
|
573
|
+
* @returns The repositioned shape.
|
|
574
|
+
*/
|
|
575
|
+
function positionOnCurve(shape, spine, param) {
|
|
576
|
+
try {
|
|
577
|
+
const wrapped = castShape(getKernel().positionOnCurve(shape.wrapped, spine.wrapped, param));
|
|
578
|
+
if (!isShape3D(wrapped)) return err(kernelError(BrepErrorCode.POSITION_ON_CURVE_FAILED, "positionOnCurve did not produce a 3D shape"));
|
|
579
|
+
return ok(wrapped);
|
|
580
|
+
} catch (e) {
|
|
581
|
+
return err(kernelError(BrepErrorCode.POSITION_ON_CURVE_FAILED, `Failed to position shape on curve at param ${param}`, e));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/topology/modifierFns.ts
|
|
586
|
+
/**
|
|
587
|
+
* Functional modifier operations — fillet, chamfer, shell, thicken, offset, draft.
|
|
588
|
+
*
|
|
589
|
+
* These are standalone functions that operate on branded shape types
|
|
590
|
+
* and return Result values.
|
|
591
|
+
*/
|
|
592
|
+
function validateNotNull(shape, label) {
|
|
593
|
+
if (getKernel().isNull(shape.wrapped)) return err(validationError(BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
594
|
+
return ok(void 0);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Validate that a scalar or `[a, b]` pair is positive.
|
|
598
|
+
* Returns an Err Result on failure, `undefined` on success.
|
|
599
|
+
*
|
|
600
|
+
* Function-type values (per-edge callbacks) are intentionally skipped here --
|
|
601
|
+
* they are validated lazily in {@link resolveEdgeCallback} when each edge is processed.
|
|
602
|
+
*/
|
|
603
|
+
function validatePositiveParam(value, msgs) {
|
|
604
|
+
if (typeof value === "number" && value <= 0) return err(validationError(msgs.code, msgs.scalar, void 0, void 0, msgs.scalarHint));
|
|
605
|
+
if (Array.isArray(value) && (value[0] <= 0 || value[1] <= 0)) return err(validationError(msgs.code, msgs.pair, void 0, void 0, msgs.pairHint));
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* When the user supplies a per-edge callback, pre-filter edges and build a
|
|
609
|
+
* hash-indexed lookup for the kernel. Returns `null` if no edges survive.
|
|
610
|
+
*/
|
|
611
|
+
function resolveEdgeCallback(selectedEdges, callbackFn) {
|
|
612
|
+
const filteredEdges = [];
|
|
613
|
+
const hashToValue = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const edge of selectedEdges) {
|
|
615
|
+
const val = callbackFn(edge) ?? 0;
|
|
616
|
+
if (typeof val === "number" && val <= 0) continue;
|
|
617
|
+
if (Array.isArray(val) && (val[0] <= 0 || val[1] <= 0)) continue;
|
|
618
|
+
filteredEdges.push(edge);
|
|
619
|
+
hashToValue.set(getKernel().hashCode(edge.wrapped, HASH_CODE_MAX), val);
|
|
620
|
+
}
|
|
621
|
+
if (filteredEdges.length === 0) return null;
|
|
622
|
+
const kernelParam = (ocEdge) => {
|
|
623
|
+
return hashToValue.get(ocEdge.HashCode(2147483647)) ?? 1;
|
|
624
|
+
};
|
|
625
|
+
return {
|
|
626
|
+
edges: filteredEdges,
|
|
627
|
+
kernelParam
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Cast a kernel result to a Shape3D, propagate metadata, and wrap in `ok()`.
|
|
632
|
+
* Returns an error if the result is not a 3D shape.
|
|
633
|
+
*/
|
|
634
|
+
function finalizeShape3D(evolution, resultShape, inputs, not3dCode, not3dMessage) {
|
|
635
|
+
const cast = castShape(resultShape);
|
|
636
|
+
if (!isShape3D(cast)) return err(kernelError(not3dCode, not3dMessage));
|
|
637
|
+
propagateAllMetadata(evolution, inputs, cast);
|
|
638
|
+
return ok(cast);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* When the user supplies a per-face callback for draft angle, pre-filter
|
|
642
|
+
* faces and build a hash-indexed lookup for the kernel.
|
|
643
|
+
*/
|
|
644
|
+
function resolveDraftCallback(faces, angle) {
|
|
645
|
+
if (typeof angle !== "function") return {
|
|
646
|
+
filteredFaces: [...faces],
|
|
647
|
+
kernelAngle: angle
|
|
648
|
+
};
|
|
649
|
+
const filteredFaces = [];
|
|
650
|
+
const hashToAngle = /* @__PURE__ */ new Map();
|
|
651
|
+
for (const face of faces) {
|
|
652
|
+
const a = angle(face);
|
|
653
|
+
if (a === null || a === 0 || Math.abs(a) >= 90) continue;
|
|
654
|
+
filteredFaces.push(face);
|
|
655
|
+
hashToAngle.set(getKernel().hashCode(face.wrapped, HASH_CODE_MAX), a);
|
|
656
|
+
}
|
|
657
|
+
const kernelAngle = (ocFace) => {
|
|
658
|
+
const a = hashToAngle.get(ocFace.HashCode(HASH_CODE_MAX));
|
|
659
|
+
if (a === void 0) throw new Error("draft: face hash not found — possible hash collision");
|
|
660
|
+
return a;
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
filteredFaces,
|
|
664
|
+
kernelAngle
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Thickens a surface (face or shell) into a solid by offsetting it.
|
|
669
|
+
*
|
|
670
|
+
* Takes a planar or non-planar surface shape and creates a solid
|
|
671
|
+
* by offsetting it by the given thickness. Positive thickness offsets
|
|
672
|
+
* along the surface normal; negative thickness offsets against it.
|
|
673
|
+
*/
|
|
674
|
+
function thicken(shape, thickness) {
|
|
675
|
+
const check = validateNotNull(shape, "thicken: shape");
|
|
676
|
+
if (isErr(check)) return check;
|
|
677
|
+
try {
|
|
678
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
679
|
+
const { shape: resultShape, evolution } = getKernel().thickenWithHistory(shape.wrapped, thickness, inputFaceHashes, HASH_CODE_MAX);
|
|
680
|
+
const cast = castShape(resultShape);
|
|
681
|
+
propagateAllMetadata(evolution, [shape], cast);
|
|
682
|
+
return ok(cast);
|
|
683
|
+
} catch (e) {
|
|
684
|
+
return err(kernelError("THICKEN_FAILED", `Thicken operation failed: ${e instanceof Error ? e.message : String(e)}`, e));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Apply a fillet (rounded edge) to selected edges of a 3D shape.
|
|
689
|
+
*
|
|
690
|
+
* @param shape - The shape to modify.
|
|
691
|
+
* @param edges - Edges to fillet. Pass `undefined` to fillet all edges.
|
|
692
|
+
* @param radius - Constant radius, variable radius `[r1, r2]`, or per-edge callback.
|
|
693
|
+
*/
|
|
694
|
+
function fillet(shape, edges, radius) {
|
|
695
|
+
const check = validateNotNull(shape, "fillet: shape");
|
|
696
|
+
if (isErr(check)) return check;
|
|
697
|
+
const paramErr = validatePositiveParam(radius, {
|
|
698
|
+
code: "INVALID_FILLET_RADIUS",
|
|
699
|
+
scalar: "Fillet radius must be positive",
|
|
700
|
+
pair: "Fillet radii must both be positive",
|
|
701
|
+
scalarHint: "Provide a positive radius value greater than 0",
|
|
702
|
+
pairHint: "Both radius values must be greater than 0"
|
|
703
|
+
});
|
|
704
|
+
if (paramErr) return paramErr;
|
|
705
|
+
const selectedEdges = edges ?? getEdges(shape);
|
|
706
|
+
if (selectedEdges.length === 0) return err(validationError(BrepErrorCode.FILLET_NO_EDGES, "No edges found for fillet", void 0, void 0, "Check that the shape has edges, or adjust your edge finder criteria"));
|
|
707
|
+
try {
|
|
708
|
+
let filteredEdges;
|
|
709
|
+
let kernelRadius;
|
|
710
|
+
if (typeof radius === "function") {
|
|
711
|
+
const resolved = resolveEdgeCallback(selectedEdges, radius);
|
|
712
|
+
if (!resolved) return err(validationError(BrepErrorCode.FILLET_NO_EDGES, "No edges with positive radius for fillet", void 0, void 0, "Check that the radius callback returns positive values"));
|
|
713
|
+
filteredEdges = resolved.edges;
|
|
714
|
+
kernelRadius = resolved.kernelParam;
|
|
715
|
+
} else {
|
|
716
|
+
filteredEdges = [...selectedEdges];
|
|
717
|
+
kernelRadius = radius;
|
|
718
|
+
}
|
|
719
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
720
|
+
const { shape: resultShape, evolution } = getKernel().filletWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelRadius, inputFaceHashes, HASH_CODE_MAX);
|
|
721
|
+
return finalizeShape3D(evolution, resultShape, [shape], BrepErrorCode.FILLET_NOT_3D, "Fillet result is not a 3D shape");
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return err(kernelError("FILLET_FAILED", `Fillet operation failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
724
|
+
operation: "fillet",
|
|
725
|
+
edgeCount: selectedEdges.length,
|
|
726
|
+
radius
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Apply a chamfer (beveled edge) to selected edges of a 3D shape.
|
|
732
|
+
*
|
|
733
|
+
* @param shape - The shape to modify.
|
|
734
|
+
* @param edges - Edges to chamfer. Pass `undefined` to chamfer all edges.
|
|
735
|
+
* @param distance - Symmetric distance, asymmetric `[d1, d2]`, or per-edge callback.
|
|
736
|
+
*/
|
|
737
|
+
function chamfer(shape, edges, distance) {
|
|
738
|
+
const check = validateNotNull(shape, "chamfer: shape");
|
|
739
|
+
if (isErr(check)) return check;
|
|
740
|
+
const paramErr = validatePositiveParam(distance, {
|
|
741
|
+
code: "INVALID_CHAMFER_DISTANCE",
|
|
742
|
+
scalar: "Chamfer distance must be positive",
|
|
743
|
+
pair: "Chamfer distances must both be positive",
|
|
744
|
+
scalarHint: "Provide a positive distance value greater than 0",
|
|
745
|
+
pairHint: "Both distance values must be greater than 0"
|
|
746
|
+
});
|
|
747
|
+
if (paramErr) return paramErr;
|
|
748
|
+
const selectedEdges = edges ?? getEdges(shape);
|
|
749
|
+
if (selectedEdges.length === 0) return err(validationError(BrepErrorCode.CHAMFER_NO_EDGES, "No edges found for chamfer"));
|
|
750
|
+
try {
|
|
751
|
+
let filteredEdges;
|
|
752
|
+
let kernelDistance;
|
|
753
|
+
if (typeof distance === "function") {
|
|
754
|
+
const resolved = resolveEdgeCallback(selectedEdges, distance);
|
|
755
|
+
if (!resolved) return err(validationError(BrepErrorCode.CHAMFER_NO_EDGES, "No edges with positive distance for chamfer"));
|
|
756
|
+
filteredEdges = resolved.edges;
|
|
757
|
+
kernelDistance = resolved.kernelParam;
|
|
758
|
+
} else {
|
|
759
|
+
filteredEdges = [...selectedEdges];
|
|
760
|
+
kernelDistance = distance;
|
|
761
|
+
}
|
|
762
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
763
|
+
const { shape: resultShape, evolution } = getKernel().chamferWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelDistance, inputFaceHashes, HASH_CODE_MAX);
|
|
764
|
+
return finalizeShape3D(evolution, resultShape, [shape], BrepErrorCode.CHAMFER_NOT_3D, "Chamfer result is not a 3D shape");
|
|
765
|
+
} catch (e) {
|
|
766
|
+
return err(kernelError("CHAMFER_FAILED", `Chamfer operation failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
767
|
+
operation: "chamfer",
|
|
768
|
+
edgeCount: selectedEdges.length,
|
|
769
|
+
distance
|
|
770
|
+
}));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Create a hollow shell by removing faces and offsetting remaining walls.
|
|
775
|
+
*
|
|
776
|
+
* @param shape - The solid to hollow out.
|
|
777
|
+
* @param faces - Faces to remove.
|
|
778
|
+
* @param thickness - Wall thickness.
|
|
779
|
+
* @param tolerance - Shell operation tolerance (default 1e-3).
|
|
780
|
+
*/
|
|
781
|
+
function shell(shape, faces, thickness, tolerance = .001) {
|
|
782
|
+
const check = validateNotNull(shape, "shell: shape");
|
|
783
|
+
if (isErr(check)) return check;
|
|
784
|
+
if (thickness <= 0) return err(validationError("INVALID_THICKNESS", "Shell thickness must be positive"));
|
|
785
|
+
if (faces.length === 0) return err(validationError("NO_FACES", "At least one face must be specified for shell"));
|
|
786
|
+
try {
|
|
787
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
788
|
+
const { shape: resultShape, evolution } = getKernel().shellWithHistory(shape.wrapped, faces.map((f) => f.wrapped), thickness, inputFaceHashes, HASH_CODE_MAX, tolerance);
|
|
789
|
+
const cast = castShape(resultShape);
|
|
790
|
+
if (!isShape3D(cast)) return err(kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
791
|
+
propagateAllMetadata(evolution, [shape], cast);
|
|
792
|
+
return ok(cast);
|
|
793
|
+
} catch (e) {
|
|
794
|
+
return err(kernelError("SHELL_FAILED", `Shell operation failed: ${e instanceof Error ? e.message : String(e)}`, e, {
|
|
795
|
+
operation: "shell",
|
|
796
|
+
faceCount: faces.length,
|
|
797
|
+
thickness
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Offset all faces of a shape by a given distance.
|
|
803
|
+
*
|
|
804
|
+
* @param shape - The shape to offset (must be a 3D shape with faces).
|
|
805
|
+
* @param distance - Offset distance (positive = outward, negative = inward).
|
|
806
|
+
* @param tolerance - Offset tolerance (default 1e-6).
|
|
807
|
+
*/
|
|
808
|
+
function offset(shape, distance, tolerance = 1e-6) {
|
|
809
|
+
const check = validateNotNull(shape, "offset: shape");
|
|
810
|
+
if (isErr(check)) return check;
|
|
811
|
+
if (distance === 0) return err(validationError("ZERO_OFFSET", "Offset distance cannot be zero"));
|
|
812
|
+
try {
|
|
813
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
814
|
+
const { shape: resultShape, evolution } = getKernel().offsetWithHistory(shape.wrapped, distance, inputFaceHashes, HASH_CODE_MAX, tolerance);
|
|
815
|
+
const cast = castShape(resultShape);
|
|
816
|
+
if (!isShape3D(cast)) return err(kernelError("OFFSET_RESULT_NOT_3D", "Offset result is not a 3D shape"));
|
|
817
|
+
propagateAllMetadata(evolution, [shape], cast);
|
|
818
|
+
return ok(cast);
|
|
819
|
+
} catch (e) {
|
|
820
|
+
return err(kernelError("OFFSET_FAILED", `Offset operation failed: ${e instanceof Error ? e.message : String(e)}`, e));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Apply a draft (taper) to selected faces of a 3D shape.
|
|
825
|
+
*
|
|
826
|
+
* Draft tilts faces by a specified angle relative to a pull direction,
|
|
827
|
+
* pivoting about a neutral plane. This is essential for injection molding
|
|
828
|
+
* and casting workflows where parts must release from a mold.
|
|
829
|
+
*
|
|
830
|
+
* @param shape - The solid to modify.
|
|
831
|
+
* @param faces - Faces to draft.
|
|
832
|
+
* @param pullDirection - Mold opening direction vector.
|
|
833
|
+
* @param neutralPlane - A point on the plane where faces are not displaced.
|
|
834
|
+
* @param angle - Constant angle in degrees, or per-face callback returning degrees (null to skip).
|
|
835
|
+
*/
|
|
836
|
+
function draft(shape, faces, pullDirection, neutralPlane, angle) {
|
|
837
|
+
const check = validateNotNull(shape, "draft: shape");
|
|
838
|
+
if (isErr(check)) return check;
|
|
839
|
+
if (typeof angle === "number") {
|
|
840
|
+
if (angle === 0) return err(validationError(BrepErrorCode.DRAFT_INVALID_ANGLE, "Draft angle cannot be zero", void 0, void 0, "Provide a non-zero angle in degrees"));
|
|
841
|
+
if (Math.abs(angle) >= 90) return err(validationError(BrepErrorCode.DRAFT_INVALID_ANGLE, "Draft angle must be between -90 and 90 degrees (exclusive)", void 0, void 0, "Typical draft angles are 1-5 degrees for injection molding"));
|
|
842
|
+
}
|
|
843
|
+
if (faces.length === 0) return err(validationError(BrepErrorCode.DRAFT_NO_FACES, "No faces specified for draft", void 0, void 0, "Select at least one face to apply the draft angle to"));
|
|
844
|
+
try {
|
|
845
|
+
const { filteredFaces, kernelAngle } = resolveDraftCallback(faces, angle);
|
|
846
|
+
if (filteredFaces.length === 0) return err(validationError(BrepErrorCode.DRAFT_NO_FACES, "No faces with valid draft angle", void 0, void 0, "Check that the angle callback returns non-zero values between -90 and 90 degrees"));
|
|
847
|
+
const inputFaceHashes = collectInputFaceHashes([shape]);
|
|
848
|
+
const { shape: resultShape, evolution } = getKernel().draftWithHistory(shape.wrapped, filteredFaces.map((f) => f.wrapped), [
|
|
849
|
+
pullDirection[0],
|
|
850
|
+
pullDirection[1],
|
|
851
|
+
pullDirection[2]
|
|
852
|
+
], [
|
|
853
|
+
neutralPlane[0],
|
|
854
|
+
neutralPlane[1],
|
|
855
|
+
neutralPlane[2]
|
|
856
|
+
], kernelAngle, inputFaceHashes, HASH_CODE_MAX);
|
|
857
|
+
return finalizeShape3D(evolution, resultShape, [shape], BrepErrorCode.DRAFT_NOT_3D, "Draft result is not a 3D shape");
|
|
858
|
+
} catch (e) {
|
|
859
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
860
|
+
return err(kernelError(BrepErrorCode.DRAFT_FAILED, `Draft operation failed: ${raw}`, e, {
|
|
861
|
+
operation: "draft",
|
|
862
|
+
faceCount: faces.length,
|
|
863
|
+
angle
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Apply a variable-radius fillet to an edge.
|
|
869
|
+
*
|
|
870
|
+
* The radius varies along the edge according to the provided spec points.
|
|
871
|
+
* Each point specifies a normalized parameter (0 = start, 1 = end) and radius.
|
|
872
|
+
*
|
|
873
|
+
* **Cross-kernel note:** Only brepkit supports variable-radius fillet.
|
|
874
|
+
* Returns UNSUPPORTED_CAPABILITY error on OCCT.
|
|
875
|
+
*/
|
|
876
|
+
function variableFillet(shape, edge, radii) {
|
|
877
|
+
if (radii.length === 0) return err(validationError(BrepErrorCode.VARIABLE_FILLET_FAILED, "radii must contain at least one radius spec"));
|
|
878
|
+
for (const r of radii) if (r.radius <= 0) return err(validationError(BrepErrorCode.VARIABLE_FILLET_FAILED, "All radius values must be positive"));
|
|
879
|
+
const kernel = getKernel();
|
|
880
|
+
try {
|
|
881
|
+
const spec = JSON.stringify({
|
|
882
|
+
edge: kernel.hashCode(edge.wrapped, HASH_CODE_MAX),
|
|
883
|
+
radii: radii.map((r) => ({
|
|
884
|
+
param: r.param,
|
|
885
|
+
radius: r.radius
|
|
886
|
+
}))
|
|
887
|
+
});
|
|
888
|
+
const wrapped = castShape(kernel.filletVariable(shape.wrapped, spec));
|
|
889
|
+
if (!isShape3D(wrapped)) {
|
|
890
|
+
wrapped[Symbol.dispose]();
|
|
891
|
+
return err(kernelError(BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet did not produce a 3D shape"));
|
|
892
|
+
}
|
|
893
|
+
if (!isSolid(wrapped)) {
|
|
894
|
+
wrapped[Symbol.dispose]();
|
|
895
|
+
return err(kernelError(BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet did not produce a solid"));
|
|
896
|
+
}
|
|
897
|
+
return ok(wrapped);
|
|
898
|
+
} catch (e) {
|
|
899
|
+
return err(kernelError(BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet failed", e));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/topology/healingFns.ts
|
|
904
|
+
/**
|
|
905
|
+
* Shape healing and validation functions.
|
|
906
|
+
*
|
|
907
|
+
* Uses ShapeFix_Solid, ShapeFix_Face, ShapeFix_Wire, and BRepCheck_Analyzer
|
|
908
|
+
* to validate and repair shapes.
|
|
909
|
+
*/
|
|
910
|
+
/**
|
|
911
|
+
* Check if a shape is valid according to kernel geometry and topology checks.
|
|
912
|
+
*/
|
|
913
|
+
function isValid(shape) {
|
|
914
|
+
return getKernel().isValid(shape.wrapped);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Attempt to heal/fix a solid shape.
|
|
918
|
+
*
|
|
919
|
+
* Uses ShapeFix_Solid to repair topology issues like gaps, wrong orientation, etc.
|
|
920
|
+
*/
|
|
921
|
+
function healSolid(solid) {
|
|
922
|
+
if (!isSolid(solid)) return err(validationError("NOT_A_SOLID", "Input shape is not a solid"));
|
|
923
|
+
const alreadyValid = isValid(solid);
|
|
924
|
+
try {
|
|
925
|
+
const result = getKernel().healSolid(solid.wrapped);
|
|
926
|
+
if (!result) {
|
|
927
|
+
if (alreadyValid) return ok(solid);
|
|
928
|
+
return err(kernelError(BrepErrorCode.HEAL_NO_EFFECT, "Solid healing had no effect — shape is still invalid"));
|
|
929
|
+
}
|
|
930
|
+
const cast = castShape(result);
|
|
931
|
+
if (!isSolid(cast)) return err(kernelError("HEAL_RESULT_NOT_SOLID", "Healed result is not a solid"));
|
|
932
|
+
if (!isValid(cast)) return err(kernelError("HEAL_SOLID_INCOMPLETE", "Healed result is still invalid after ShapeFix_Solid"));
|
|
933
|
+
return ok(cast);
|
|
934
|
+
} catch (e) {
|
|
935
|
+
return err(kernelError("HEAL_SOLID_FAILED", "Solid healing failed", e));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Attempt to heal/fix a face.
|
|
940
|
+
*
|
|
941
|
+
* Uses ShapeFix_Face to repair wire ordering, orientation, and geometry issues.
|
|
942
|
+
*/
|
|
943
|
+
function healFace(face) {
|
|
944
|
+
if (!isFace(face)) return err(validationError("NOT_A_FACE", "Input shape is not a face"));
|
|
945
|
+
try {
|
|
946
|
+
const cast = castShape(getKernel().healFace(face.wrapped));
|
|
947
|
+
if (!isFace(cast)) return err(kernelError("HEAL_RESULT_NOT_FACE", "Healed result is not a face"));
|
|
948
|
+
return ok(cast);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
return err(kernelError("HEAL_FACE_FAILED", "Face healing failed", e));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Attempt to heal/fix a wire.
|
|
955
|
+
*
|
|
956
|
+
* Uses ShapeFix_Wire to repair edge connectivity, gaps, and self-intersections.
|
|
957
|
+
* Requires a face for surface context; pass `undefined` to use a default planar context.
|
|
958
|
+
*/
|
|
959
|
+
function healWire(wire, face) {
|
|
960
|
+
if (!isWire(wire)) return err(validationError("NOT_A_WIRE", "Input shape is not a wire"));
|
|
961
|
+
try {
|
|
962
|
+
const cast = castShape(getKernel().healWire(wire.wrapped, face?.wrapped));
|
|
963
|
+
if (!isWire(cast)) return err(kernelError("HEAL_RESULT_NOT_WIRE", "Healed result is not a wire"));
|
|
964
|
+
return ok(cast);
|
|
965
|
+
} catch (e) {
|
|
966
|
+
return err(kernelError("HEAL_WIRE_FAILED", "Wire healing failed", e));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Attempt to heal any shape by dispatching to the appropriate fixer.
|
|
971
|
+
*
|
|
972
|
+
* Supports solids, faces, and wires. For other shape types, returns the
|
|
973
|
+
* input unchanged.
|
|
974
|
+
*/
|
|
975
|
+
function heal(shape) {
|
|
976
|
+
if (isSolid(shape)) return healSolid(shape);
|
|
977
|
+
if (isFace(shape)) return healFace(shape);
|
|
978
|
+
if (isWire(shape)) return healWire(shape);
|
|
979
|
+
return ok(shape);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Automatically heal a shape using the appropriate shape-level fixer.
|
|
983
|
+
*
|
|
984
|
+
* If the shape is already valid, returns it unchanged with a no-op report.
|
|
985
|
+
* Uses ShapeFix_Solid/Face/Wire depending on shape type, which internally
|
|
986
|
+
* handles sub-shape healing and reconstruction.
|
|
987
|
+
*/
|
|
988
|
+
function autoHeal(shape, options) {
|
|
989
|
+
const fixWires = options?.fixWires !== false;
|
|
990
|
+
const fixFaces = options?.fixFaces !== false;
|
|
991
|
+
const fixSolids = options?.fixSolids !== false;
|
|
992
|
+
const fixSelfIntersection = options?.fixSelfIntersection === true;
|
|
993
|
+
const sewTolerance = options?.sewTolerance;
|
|
994
|
+
const steps = [];
|
|
995
|
+
const diagnostics = [];
|
|
996
|
+
if (isValid(shape)) return ok({
|
|
997
|
+
shape,
|
|
998
|
+
report: {
|
|
999
|
+
isValid: true,
|
|
1000
|
+
alreadyValid: true,
|
|
1001
|
+
wiresHealed: 0,
|
|
1002
|
+
facesHealed: 0,
|
|
1003
|
+
solidHealed: false,
|
|
1004
|
+
steps: ["Shape already valid"],
|
|
1005
|
+
diagnostics: [{
|
|
1006
|
+
name: "validation",
|
|
1007
|
+
attempted: true,
|
|
1008
|
+
succeeded: true
|
|
1009
|
+
}]
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
steps.push("Shape invalid — applying shape-level healing");
|
|
1013
|
+
const wiresBefore = getWires(shape).length;
|
|
1014
|
+
const facesBefore = getFaces(shape).length;
|
|
1015
|
+
let current = shape;
|
|
1016
|
+
let solidHealed = false;
|
|
1017
|
+
if (sewTolerance !== void 0) try {
|
|
1018
|
+
current = castShape(getKernel().sew([current.wrapped], sewTolerance));
|
|
1019
|
+
steps.push(`Applied sewing with tolerance ${sewTolerance}`);
|
|
1020
|
+
diagnostics.push({
|
|
1021
|
+
name: "sew",
|
|
1022
|
+
attempted: true,
|
|
1023
|
+
succeeded: true,
|
|
1024
|
+
detail: `tolerance=${sewTolerance}`
|
|
1025
|
+
});
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
1028
|
+
steps.push(`Sewing failed: ${detail}`);
|
|
1029
|
+
diagnostics.push({
|
|
1030
|
+
name: "sew",
|
|
1031
|
+
attempted: true,
|
|
1032
|
+
succeeded: false,
|
|
1033
|
+
detail
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (fixSelfIntersection && fixWires) {
|
|
1037
|
+
const wires = getWires(current);
|
|
1038
|
+
let fixCount = 0;
|
|
1039
|
+
for (const wire of wires) try {
|
|
1040
|
+
getKernel().fixSelfIntersection(wire.wrapped);
|
|
1041
|
+
fixCount++;
|
|
1042
|
+
} catch {}
|
|
1043
|
+
steps.push(`Self-intersection fix: ${fixCount}/${wires.length} wires`);
|
|
1044
|
+
diagnostics.push({
|
|
1045
|
+
name: "fixSelfIntersection",
|
|
1046
|
+
attempted: true,
|
|
1047
|
+
succeeded: fixCount > 0,
|
|
1048
|
+
detail: `${fixCount}/${wires.length} wires fixed`
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
if (isSolid(current) && fixSolids || isFace(current) && fixFaces || isWire(current) && fixWires) {
|
|
1052
|
+
const healResult = heal(current);
|
|
1053
|
+
if (isOk(healResult)) {
|
|
1054
|
+
current = healResult.value;
|
|
1055
|
+
if (isSolid(shape)) {
|
|
1056
|
+
solidHealed = true;
|
|
1057
|
+
steps.push("Applied ShapeFix_Solid");
|
|
1058
|
+
diagnostics.push({
|
|
1059
|
+
name: "healSolid",
|
|
1060
|
+
attempted: true,
|
|
1061
|
+
succeeded: true
|
|
1062
|
+
});
|
|
1063
|
+
} else if (isFace(shape)) {
|
|
1064
|
+
steps.push("Applied ShapeFix_Face");
|
|
1065
|
+
diagnostics.push({
|
|
1066
|
+
name: "healFace",
|
|
1067
|
+
attempted: true,
|
|
1068
|
+
succeeded: true
|
|
1069
|
+
});
|
|
1070
|
+
} else {
|
|
1071
|
+
steps.push("Applied ShapeFix_Wire");
|
|
1072
|
+
diagnostics.push({
|
|
1073
|
+
name: "healWire",
|
|
1074
|
+
attempted: true,
|
|
1075
|
+
succeeded: true
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
steps.push("Shape-level healing failed");
|
|
1080
|
+
diagnostics.push({
|
|
1081
|
+
name: "healShape",
|
|
1082
|
+
attempted: true,
|
|
1083
|
+
succeeded: false
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
} else diagnostics.push({
|
|
1087
|
+
name: "healShape",
|
|
1088
|
+
attempted: false,
|
|
1089
|
+
succeeded: false,
|
|
1090
|
+
detail: "skipped by options"
|
|
1091
|
+
});
|
|
1092
|
+
const wiresAfter = getWires(current).length;
|
|
1093
|
+
const facesAfter = getFaces(current).length;
|
|
1094
|
+
const wiresHealed = Math.abs(wiresAfter - wiresBefore);
|
|
1095
|
+
const facesHealed = Math.abs(facesAfter - facesBefore);
|
|
1096
|
+
if (wiresHealed > 0) steps.push(`Wire count changed by ${wiresHealed}`);
|
|
1097
|
+
if (facesHealed > 0) steps.push(`Face count changed by ${facesHealed}`);
|
|
1098
|
+
const valid = isValid(current);
|
|
1099
|
+
steps.push(valid ? "Final validation: valid" : "Final validation: still invalid");
|
|
1100
|
+
diagnostics.push({
|
|
1101
|
+
name: "finalValidation",
|
|
1102
|
+
attempted: true,
|
|
1103
|
+
succeeded: valid
|
|
1104
|
+
});
|
|
1105
|
+
return ok({
|
|
1106
|
+
shape: current,
|
|
1107
|
+
report: {
|
|
1108
|
+
isValid: valid,
|
|
1109
|
+
alreadyValid: false,
|
|
1110
|
+
wiresHealed,
|
|
1111
|
+
facesHealed,
|
|
1112
|
+
solidHealed,
|
|
1113
|
+
steps,
|
|
1114
|
+
diagnostics
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* General-purpose shape repair using ShapeFix_Shape.
|
|
1120
|
+
* Fixes orientations, missing curves, and other common issues.
|
|
1121
|
+
*/
|
|
1122
|
+
function fixShape(shape) {
|
|
1123
|
+
try {
|
|
1124
|
+
return ok(castShape(getKernel().fixShape(shape.wrapped)));
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
return err(kernelError(BrepErrorCode.FIX_SHAPE_FAILED, "ShapeFix_Shape failed", e));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Convert a closed shell into a solid.
|
|
1131
|
+
*
|
|
1132
|
+
* The shell must be closed (all faces share edges) for the conversion to succeed.
|
|
1133
|
+
*/
|
|
1134
|
+
function solidFromShell(shell) {
|
|
1135
|
+
try {
|
|
1136
|
+
const wrapped = castShape(getKernel().solidFromShell(shell.wrapped));
|
|
1137
|
+
if (!isSolid(wrapped)) return err(kernelError(BrepErrorCode.SOLID_FROM_SHELL_FAILED, "solidFromShell did not produce a solid"));
|
|
1138
|
+
if (!isValid(wrapped)) return err(kernelError(BrepErrorCode.SOLID_FROM_SHELL_FAILED, "solidFromShell produced an invalid solid"));
|
|
1139
|
+
return ok(wrapped);
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
return err(kernelError(BrepErrorCode.SOLID_FROM_SHELL_FAILED, "Failed to create solid from shell", e));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Fix self-intersections in a wire.
|
|
1146
|
+
*
|
|
1147
|
+
* Uses ShapeFix_Wire to detect and repair self-intersecting edges.
|
|
1148
|
+
*/
|
|
1149
|
+
function fixSelfIntersection(wire) {
|
|
1150
|
+
try {
|
|
1151
|
+
const wrapped = castShape(getKernel().fixSelfIntersection(wire.wrapped));
|
|
1152
|
+
if (!isWire(wrapped)) return err(kernelError(BrepErrorCode.FIX_SELF_INTERSECTION_FAILED, "Result is not a wire"));
|
|
1153
|
+
return ok(wrapped);
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
return err(kernelError(BrepErrorCode.FIX_SELF_INTERSECTION_FAILED, "Failed to fix wire self-intersection", e));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/topology/primitiveFns.ts
|
|
1160
|
+
/**
|
|
1161
|
+
* Create a box with the given dimensions.
|
|
1162
|
+
*
|
|
1163
|
+
* @param width - Size along X.
|
|
1164
|
+
* @param depth - Size along Y.
|
|
1165
|
+
* @param height - Size along Z.
|
|
1166
|
+
*/
|
|
1167
|
+
function box(width, depth, height, options) {
|
|
1168
|
+
const solid = createSolid(getKernel().makeBox(width, depth, height));
|
|
1169
|
+
const center = options?.at ?? (options?.centered ? [
|
|
1170
|
+
0,
|
|
1171
|
+
0,
|
|
1172
|
+
0
|
|
1173
|
+
] : void 0);
|
|
1174
|
+
if (center) return translate(solid, [
|
|
1175
|
+
center[0] - width / 2,
|
|
1176
|
+
center[1] - depth / 2,
|
|
1177
|
+
center[2] - height / 2
|
|
1178
|
+
]);
|
|
1179
|
+
return solid;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Create a cylinder with the given radius and height.
|
|
1183
|
+
*/
|
|
1184
|
+
function cylinder(radius, height, options) {
|
|
1185
|
+
const at = options?.at ?? [
|
|
1186
|
+
0,
|
|
1187
|
+
0,
|
|
1188
|
+
0
|
|
1189
|
+
];
|
|
1190
|
+
const axis = options?.axis ?? [
|
|
1191
|
+
0,
|
|
1192
|
+
0,
|
|
1193
|
+
1
|
|
1194
|
+
];
|
|
1195
|
+
let solid = makeCylinder(radius, height, at, axis);
|
|
1196
|
+
if (options?.centered) {
|
|
1197
|
+
const halfShift = [
|
|
1198
|
+
-axis[0] * height * .5,
|
|
1199
|
+
-axis[1] * height * .5,
|
|
1200
|
+
-axis[2] * height * .5
|
|
1201
|
+
];
|
|
1202
|
+
solid = translate(solid, halfShift);
|
|
1203
|
+
}
|
|
1204
|
+
return solid;
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Create a sphere with the given radius.
|
|
1208
|
+
*/
|
|
1209
|
+
function sphere(radius, options) {
|
|
1210
|
+
let solid = makeSphere(radius);
|
|
1211
|
+
if (options?.at) solid = translate(solid, options.at);
|
|
1212
|
+
return solid;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Create a cone (or frustum) with the given radii and height.
|
|
1216
|
+
*
|
|
1217
|
+
* @param bottomRadius - Radius at the base.
|
|
1218
|
+
* @param topRadius - Radius at the top (0 for a full cone).
|
|
1219
|
+
* @param height - Height of the cone.
|
|
1220
|
+
*/
|
|
1221
|
+
function cone(bottomRadius, topRadius, height, options) {
|
|
1222
|
+
const at = options?.at ?? [
|
|
1223
|
+
0,
|
|
1224
|
+
0,
|
|
1225
|
+
0
|
|
1226
|
+
];
|
|
1227
|
+
const axis = options?.axis ?? [
|
|
1228
|
+
0,
|
|
1229
|
+
0,
|
|
1230
|
+
1
|
|
1231
|
+
];
|
|
1232
|
+
let solid = makeCone(bottomRadius, topRadius, height, at, axis);
|
|
1233
|
+
if (options?.centered) {
|
|
1234
|
+
const halfShift = [
|
|
1235
|
+
-axis[0] * height * .5,
|
|
1236
|
+
-axis[1] * height * .5,
|
|
1237
|
+
-axis[2] * height * .5
|
|
1238
|
+
];
|
|
1239
|
+
solid = translate(solid, halfShift);
|
|
1240
|
+
}
|
|
1241
|
+
return solid;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Create a torus with the given major and minor radii.
|
|
1245
|
+
*/
|
|
1246
|
+
function torus(majorRadius, minorRadius, options) {
|
|
1247
|
+
return makeTorus(majorRadius, minorRadius, options?.at ?? [
|
|
1248
|
+
0,
|
|
1249
|
+
0,
|
|
1250
|
+
0
|
|
1251
|
+
], options?.axis ?? [
|
|
1252
|
+
0,
|
|
1253
|
+
0,
|
|
1254
|
+
1
|
|
1255
|
+
]);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Create an ellipsoid with the given axis half-lengths.
|
|
1259
|
+
*
|
|
1260
|
+
* @param rx - Half-length along X.
|
|
1261
|
+
* @param ry - Half-length along Y.
|
|
1262
|
+
* @param rz - Half-length along Z.
|
|
1263
|
+
*/
|
|
1264
|
+
function ellipsoid(rx, ry, rz, options) {
|
|
1265
|
+
let solid = makeEllipsoid(rx, ry, rz);
|
|
1266
|
+
if (options?.at) solid = translate(solid, options.at);
|
|
1267
|
+
return solid;
|
|
1268
|
+
}
|
|
1269
|
+
/** Create a straight edge between two 3D points. */
|
|
1270
|
+
function line(from, to) {
|
|
1271
|
+
return makeLine(from, to);
|
|
1272
|
+
}
|
|
1273
|
+
/** Create a circular edge with the given radius. */
|
|
1274
|
+
function circle(radius, options) {
|
|
1275
|
+
const axisDir = options?.axis ?? [
|
|
1276
|
+
0,
|
|
1277
|
+
0,
|
|
1278
|
+
1
|
|
1279
|
+
];
|
|
1280
|
+
return makeCircle(radius, options?.at ?? [
|
|
1281
|
+
0,
|
|
1282
|
+
0,
|
|
1283
|
+
0
|
|
1284
|
+
], axisDir);
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Create an elliptical edge.
|
|
1288
|
+
*
|
|
1289
|
+
* @returns An error if `minorRadius` exceeds `majorRadius`.
|
|
1290
|
+
*/
|
|
1291
|
+
function ellipse(majorRadius, minorRadius, options) {
|
|
1292
|
+
const axisDir = options?.axis ?? [
|
|
1293
|
+
0,
|
|
1294
|
+
0,
|
|
1295
|
+
1
|
|
1296
|
+
];
|
|
1297
|
+
return makeEllipse(majorRadius, minorRadius, options?.at ?? [
|
|
1298
|
+
0,
|
|
1299
|
+
0,
|
|
1300
|
+
0
|
|
1301
|
+
], axisDir, options?.xDir);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Create a helical wire.
|
|
1305
|
+
*
|
|
1306
|
+
* @param pitch - Vertical distance per full turn.
|
|
1307
|
+
* @param height - Total height.
|
|
1308
|
+
* @param radius - Helix radius.
|
|
1309
|
+
*/
|
|
1310
|
+
function helix(pitch, height, radius, options) {
|
|
1311
|
+
return makeHelix(pitch, height, radius, options?.at ?? [
|
|
1312
|
+
0,
|
|
1313
|
+
0,
|
|
1314
|
+
0
|
|
1315
|
+
], options?.axis ?? [
|
|
1316
|
+
0,
|
|
1317
|
+
0,
|
|
1318
|
+
1
|
|
1319
|
+
], options?.lefthand ?? false);
|
|
1320
|
+
}
|
|
1321
|
+
/** Create a circular arc edge passing through three points. */
|
|
1322
|
+
function threePointArc(p1, p2, p3) {
|
|
1323
|
+
return makeThreePointArc(p1, p2, p3);
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Create an elliptical arc edge between two angles.
|
|
1327
|
+
*
|
|
1328
|
+
* All angles are in **degrees** (unlike the legacy `makeEllipseArc` which used radians).
|
|
1329
|
+
*
|
|
1330
|
+
* @param startAngle - Start angle in degrees.
|
|
1331
|
+
* @param endAngle - End angle in degrees.
|
|
1332
|
+
*/
|
|
1333
|
+
function ellipseArc(majorRadius, minorRadius, startAngle, endAngle, options) {
|
|
1334
|
+
const axisDir = options?.axis ?? [
|
|
1335
|
+
0,
|
|
1336
|
+
0,
|
|
1337
|
+
1
|
|
1338
|
+
];
|
|
1339
|
+
return makeEllipseArc(majorRadius, minorRadius, startAngle * DEG2RAD, endAngle * DEG2RAD, options?.at ?? [
|
|
1340
|
+
0,
|
|
1341
|
+
0,
|
|
1342
|
+
0
|
|
1343
|
+
], axisDir, options?.xDir);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Create a B-spline edge that approximates a set of 3D points.
|
|
1347
|
+
*
|
|
1348
|
+
* @returns An error if the approximation algorithm fails.
|
|
1349
|
+
*/
|
|
1350
|
+
function bsplineApprox(points, config) {
|
|
1351
|
+
return makeBSplineApproximation(points, config);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Create a Bezier curve edge from control points.
|
|
1355
|
+
*
|
|
1356
|
+
* @param points - Two or more control points.
|
|
1357
|
+
*/
|
|
1358
|
+
function bezier(points) {
|
|
1359
|
+
return makeBezierCurve(points);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Create a circular arc edge tangent to a direction at the start point.
|
|
1363
|
+
*/
|
|
1364
|
+
function tangentArc(startPoint, startTgt, endPoint) {
|
|
1365
|
+
return makeTangentArc(startPoint, startTgt, endPoint);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Assemble edges and/or wires into a single connected wire.
|
|
1369
|
+
*/
|
|
1370
|
+
function wire(listOfEdges) {
|
|
1371
|
+
return assembleWire(listOfEdges);
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Assemble edges into a wire and verify it forms a closed loop.
|
|
1375
|
+
*
|
|
1376
|
+
* Combines {@link wire} + the `closedWire` smart constructor in a single step.
|
|
1377
|
+
* Returns an error if the edges cannot be assembled or the wire is not closed.
|
|
1378
|
+
*
|
|
1379
|
+
* @example
|
|
1380
|
+
* ```ts
|
|
1381
|
+
* const cw = unwrap(wireLoop([e1, e2, e3, e4]));
|
|
1382
|
+
* const f = unwrap(face(cw)); // ClosedWire accepted directly
|
|
1383
|
+
* ```
|
|
1384
|
+
*/
|
|
1385
|
+
function wireLoop(listOfEdges) {
|
|
1386
|
+
return andThen(assembleWire(listOfEdges), (w) => {
|
|
1387
|
+
if (isClosedWire(w)) return ok(w);
|
|
1388
|
+
return err(validationError("WIRE_NOT_CLOSED", "Assembled wire is not closed: start and end points do not coincide"));
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Create a planar face from a closed wire, optionally with holes.
|
|
1393
|
+
* The resulting face is always oriented (consistent normal direction).
|
|
1394
|
+
*/
|
|
1395
|
+
function face(w, holes) {
|
|
1396
|
+
return makeFace(w, holes);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Create a non-planar face from a wire using surface filling.
|
|
1400
|
+
* The resulting face is always oriented.
|
|
1401
|
+
*/
|
|
1402
|
+
function filledFace(w) {
|
|
1403
|
+
return makeNonPlanarFace(w);
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Create a face bounded by a wire on an existing face's surface.
|
|
1407
|
+
* The resulting face inherits orientation from the origin face.
|
|
1408
|
+
*/
|
|
1409
|
+
function subFace(originFace, w) {
|
|
1410
|
+
return makeNewFaceWithinFace(originFace, w);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Create a polygonal face from three or more coplanar points.
|
|
1414
|
+
* The resulting face is always oriented.
|
|
1415
|
+
*/
|
|
1416
|
+
function polygon(points) {
|
|
1417
|
+
return makePolygon(points);
|
|
1418
|
+
}
|
|
1419
|
+
/** Create a vertex at a 3D point. */
|
|
1420
|
+
function vertex(point) {
|
|
1421
|
+
return makeVertex(point);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Build a compound from multiple shapes.
|
|
1425
|
+
*/
|
|
1426
|
+
function compound(shapeArray) {
|
|
1427
|
+
return makeCompound(shapeArray);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Weld faces and shells into a single solid.
|
|
1431
|
+
* The resulting solid is always validated.
|
|
1432
|
+
*/
|
|
1433
|
+
function solid(facesOrShells) {
|
|
1434
|
+
return makeSolid(facesOrShells);
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Create an offset shape from a face.
|
|
1438
|
+
*/
|
|
1439
|
+
function offsetFace(f, distance, tolerance) {
|
|
1440
|
+
return makeOffset(f, distance, tolerance);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Weld faces and shells into a single shell.
|
|
1444
|
+
*/
|
|
1445
|
+
function sewShells(facesOrShells, ignoreType) {
|
|
1446
|
+
return weldShellsAndFaces(facesOrShells, ignoreType);
|
|
1447
|
+
}
|
|
1448
|
+
function addHoles(f, holes) {
|
|
1449
|
+
return addHolesInFace(f, holes);
|
|
1450
|
+
}
|
|
1451
|
+
//#endregion
|
|
1452
|
+
export { edgesOfFace as $, fixShape as A, offset as B, threePointArc as C, wireLoop as D, wire as E, isValid as F, chamferWithEvolution as G, thicken as H, solidFromShell as I, fuseWithEvolution as J, cutWithEvolution as K, chamfer as L, healFace as M, healSolid as N, autoHeal as O, healWire as P, adjacentFaces as Q, draft as R, tangentArc as S, vertex as T, variableFillet as U, shell as V, positionOnCurve as W, shellWithEvolution as X, intersectWithEvolution as Y, checkBoolean as Z, polygon as _, circle as a, toBufferGeometryData as at, sphere as b, cylinder as c, ellipsoid as d, facesOfEdge as et, face as f, offsetFace as g, line as h, bsplineApprox as i, chamferDistAngle as it, heal as j, fixSelfIntersection as k, ellipse as l, helix as m, bezier as n, verticesOfEdge as nt, compound as o, toGroupedBufferGeometryData as ot, filledFace as p, filletWithEvolution as q, box as r, wiresOfFace as rt, cone as s, toLineGeometryData as st, addHoles as t, sharedEdges as tt, ellipseArc as u, sewShells as v, torus as w, subFace as x, solid as y, fillet as z };
|