brepjs 18.129.0 → 18.131.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/brepjs.cjs +142 -18
- package/dist/brepjs.js +143 -19
- package/dist/csg/builders.d.ts +7 -1
- package/dist/csg/evaluators/fillet.d.ts +5 -0
- package/dist/csg/index.d.ts +2 -2
- package/dist/csg/serialize.d.ts +1 -1
- package/dist/csg/types.d.ts +11 -1
- package/dist/{healingFns-DTr6cgTb.cjs → healingFns-BqmIvfPm.cjs} +554 -554
- package/dist/{healingFns-BptGY1Th.js → healingFns-BrrTpxjI.js} +555 -555
- package/dist/topology.cjs +1 -1
- package/dist/topology.js +1 -1
- package/package.json +2 -1
|
@@ -107,6 +107,381 @@ function toLODGeometryLevels(lods, options) {
|
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
109
|
//#endregion
|
|
110
|
+
//#region src/topology/modifierFns.ts
|
|
111
|
+
/**
|
|
112
|
+
* Functional modifier operations — fillet, chamfer, shell, thicken, offset, draft.
|
|
113
|
+
*
|
|
114
|
+
* These are standalone functions that operate on branded shape types
|
|
115
|
+
* and return Result values.
|
|
116
|
+
*/
|
|
117
|
+
function validateNotNull$1(shape, label) {
|
|
118
|
+
if (require_shapeTypes.getKernel().isNull(shape.wrapped)) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
119
|
+
return require_errors.ok(void 0);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Validate that a scalar or `[a, b]` pair is positive.
|
|
123
|
+
* Returns an Err Result on failure, `undefined` on success.
|
|
124
|
+
*
|
|
125
|
+
* Function-type values (per-edge callbacks) are intentionally skipped here --
|
|
126
|
+
* they are validated lazily in {@link resolveEdgeCallback} when each edge is processed.
|
|
127
|
+
*/
|
|
128
|
+
function validatePositiveParam(value, msgs) {
|
|
129
|
+
if (typeof value === "number" && value <= 0) return require_errors.err(require_errors.validationError(msgs.code, msgs.scalar, void 0, void 0, msgs.scalarHint));
|
|
130
|
+
if (Array.isArray(value) && (value[0] <= 0 || value[1] <= 0)) return require_errors.err(require_errors.validationError(msgs.code, msgs.pair, void 0, void 0, msgs.pairHint));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* When the user supplies a per-edge callback, pre-filter edges and build a
|
|
134
|
+
* hash-indexed lookup for the kernel. Returns `null` if no edges survive.
|
|
135
|
+
*/
|
|
136
|
+
function resolveEdgeCallback$1(selectedEdges, callbackFn) {
|
|
137
|
+
const filteredEdges = [];
|
|
138
|
+
const hashToValue = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const edge of selectedEdges) {
|
|
140
|
+
const val = callbackFn(edge) ?? 0;
|
|
141
|
+
if (typeof val === "number" && val <= 0) continue;
|
|
142
|
+
if (Array.isArray(val) && (val[0] <= 0 || val[1] <= 0)) continue;
|
|
143
|
+
filteredEdges.push(edge);
|
|
144
|
+
hashToValue.set(require_shapeTypes.getKernel().hashCode(edge.wrapped, require_constants.HASH_CODE_MAX), val);
|
|
145
|
+
}
|
|
146
|
+
if (filteredEdges.length === 0) return null;
|
|
147
|
+
const kernelParam = (ocEdge) => {
|
|
148
|
+
const v = hashToValue.get(require_shapeTypes.getKernel().hashCode(ocEdge, require_constants.HASH_CODE_MAX));
|
|
149
|
+
if (v === void 0) throw new Error("fillet/chamfer: edge hash not found — possible hash collision");
|
|
150
|
+
return v;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
edges: filteredEdges,
|
|
154
|
+
kernelParam
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Cast a kernel result to a Shape3D, propagate metadata, and wrap in `ok()`.
|
|
159
|
+
* Returns an error if the result is not a 3D shape.
|
|
160
|
+
*/
|
|
161
|
+
function finalizeShape3D(evolution, resultShape, inputs, not3dCode, not3dMessage) {
|
|
162
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
163
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
164
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
165
|
+
return require_errors.err(require_errors.kernelError(not3dCode, not3dMessage));
|
|
166
|
+
}
|
|
167
|
+
require_solidBuilders.propagateAllMetadata(evolution, inputs, cast);
|
|
168
|
+
return require_errors.ok(cast);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* When the user supplies a per-face callback for draft angle, pre-filter
|
|
172
|
+
* faces and build a hash-indexed lookup for the kernel.
|
|
173
|
+
*/
|
|
174
|
+
function resolveDraftCallback(faces, angle) {
|
|
175
|
+
if (typeof angle !== "function") return {
|
|
176
|
+
filteredFaces: [...faces],
|
|
177
|
+
kernelAngle: angle
|
|
178
|
+
};
|
|
179
|
+
const filteredFaces = [];
|
|
180
|
+
const hashToAngle = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const face of faces) {
|
|
182
|
+
const a = angle(face);
|
|
183
|
+
if (a === null || a === 0 || Math.abs(a) >= 90) continue;
|
|
184
|
+
filteredFaces.push(face);
|
|
185
|
+
hashToAngle.set(require_shapeTypes.getKernel().hashCode(face.wrapped, require_constants.HASH_CODE_MAX), a);
|
|
186
|
+
}
|
|
187
|
+
const kernelAngle = (ocFace) => {
|
|
188
|
+
const a = hashToAngle.get(require_shapeTypes.getKernel().hashCode(ocFace, require_constants.HASH_CODE_MAX));
|
|
189
|
+
if (a === void 0) throw new Error("draft: face hash not found — possible hash collision");
|
|
190
|
+
return a;
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
filteredFaces,
|
|
194
|
+
kernelAngle
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Thickens a surface (face or shell) into a solid by offsetting it.
|
|
199
|
+
*
|
|
200
|
+
* Takes a planar or non-planar surface shape and creates a solid
|
|
201
|
+
* by offsetting it by the given thickness. Positive thickness offsets
|
|
202
|
+
* along the surface normal; negative thickness offsets against it.
|
|
203
|
+
*/
|
|
204
|
+
function thicken(shape, thickness) {
|
|
205
|
+
const check = validateNotNull$1(shape, "thicken: shape");
|
|
206
|
+
if (require_errors.isErr(check)) return check;
|
|
207
|
+
try {
|
|
208
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
209
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().thickenWithHistory(shape.wrapped, thickness, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
210
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
211
|
+
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
212
|
+
return require_errors.ok(cast);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
215
|
+
return require_errors.err(require_errors.kernelError("THICKEN_FAILED", `Thicken operation failed: ${raw}`, e));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Validate fillet inputs and resolve the user-supplied radius into a
|
|
220
|
+
* kernel-ready value paired with the filtered edge list.
|
|
221
|
+
*/
|
|
222
|
+
function normalizeFilletInputs(shape, edges, radius) {
|
|
223
|
+
const check = validateNotNull$1(shape, "fillet: shape");
|
|
224
|
+
if (require_errors.isErr(check)) return check;
|
|
225
|
+
const paramErr = validatePositiveParam(radius, {
|
|
226
|
+
code: "INVALID_FILLET_RADIUS",
|
|
227
|
+
scalar: "Fillet radius must be positive",
|
|
228
|
+
pair: "Fillet radii must both be positive",
|
|
229
|
+
scalarHint: "Provide a positive radius value greater than 0",
|
|
230
|
+
pairHint: "Both radius values must be greater than 0"
|
|
231
|
+
});
|
|
232
|
+
if (paramErr) return paramErr;
|
|
233
|
+
const selectedEdges = edges ?? require_topologyQueryFns.getEdges(shape);
|
|
234
|
+
if (selectedEdges.length === 0) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
235
|
+
if (typeof radius === "function") {
|
|
236
|
+
const resolved = resolveEdgeCallback$1(selectedEdges, radius);
|
|
237
|
+
if (!resolved) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.FILLET_NO_EDGES, "No edges with positive radius for fillet", void 0, void 0, "Check that the radius callback returns positive values"));
|
|
238
|
+
return require_errors.ok({
|
|
239
|
+
filteredEdges: resolved.edges,
|
|
240
|
+
kernelRadius: resolved.kernelParam,
|
|
241
|
+
selectedCount: selectedEdges.length
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return require_errors.ok({
|
|
245
|
+
filteredEdges: [...selectedEdges],
|
|
246
|
+
kernelRadius: radius,
|
|
247
|
+
selectedCount: selectedEdges.length
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Apply a fillet (rounded edge) to selected edges of a 3D shape.
|
|
252
|
+
*
|
|
253
|
+
* @param shape - The shape to modify.
|
|
254
|
+
* @param edges - Edges to fillet. Pass `undefined` to fillet all edges.
|
|
255
|
+
* @param radius - Constant radius, variable radius `[r1, r2]`, or per-edge callback.
|
|
256
|
+
*/
|
|
257
|
+
function fillet(shape, edges, radius, { trackEvolution = true } = {}) {
|
|
258
|
+
const normalized = normalizeFilletInputs(shape, edges, radius);
|
|
259
|
+
if (require_errors.isErr(normalized)) return normalized;
|
|
260
|
+
const { filteredEdges, kernelRadius, selectedCount } = normalized.value;
|
|
261
|
+
try {
|
|
262
|
+
const edgeShapes = filteredEdges.map((e) => e.wrapped);
|
|
263
|
+
if (!trackEvolution) {
|
|
264
|
+
const resultShape = require_shapeTypes.getKernel().fillet(shape.wrapped, edgeShapes, kernelRadius);
|
|
265
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
266
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
267
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
268
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.FILLET_NOT_3D, "Fillet result is not a 3D shape"));
|
|
269
|
+
}
|
|
270
|
+
return require_errors.ok(cast);
|
|
271
|
+
}
|
|
272
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
273
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().filletWithHistory(shape.wrapped, edgeShapes, kernelRadius, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
274
|
+
return finalizeShape3D(evolution, resultShape, [shape], require_errors.BrepErrorCode.FILLET_NOT_3D, "Fillet result is not a 3D shape");
|
|
275
|
+
} catch (e) {
|
|
276
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
277
|
+
return require_errors.err(require_errors.kernelError("FILLET_FAILED", `Fillet operation failed: ${raw}`, e, {
|
|
278
|
+
operation: "fillet",
|
|
279
|
+
edgeCount: selectedCount,
|
|
280
|
+
radius
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Apply a chamfer (beveled edge) to selected edges of a 3D shape.
|
|
286
|
+
*
|
|
287
|
+
* @param shape - The shape to modify.
|
|
288
|
+
* @param edges - Edges to chamfer. Pass `undefined` to chamfer all edges.
|
|
289
|
+
* @param distance - Symmetric distance, asymmetric `[d1, d2]`, or per-edge callback.
|
|
290
|
+
*/
|
|
291
|
+
function chamfer(shape, edges, distance) {
|
|
292
|
+
const check = validateNotNull$1(shape, "chamfer: shape");
|
|
293
|
+
if (require_errors.isErr(check)) return check;
|
|
294
|
+
const paramErr = validatePositiveParam(distance, {
|
|
295
|
+
code: "INVALID_CHAMFER_DISTANCE",
|
|
296
|
+
scalar: "Chamfer distance must be positive",
|
|
297
|
+
pair: "Chamfer distances must both be positive",
|
|
298
|
+
scalarHint: "Provide a positive distance value greater than 0",
|
|
299
|
+
pairHint: "Both distance values must be greater than 0"
|
|
300
|
+
});
|
|
301
|
+
if (paramErr) return paramErr;
|
|
302
|
+
const selectedEdges = edges ?? require_topologyQueryFns.getEdges(shape);
|
|
303
|
+
if (selectedEdges.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.CHAMFER_NO_EDGES, "No edges found for chamfer"));
|
|
304
|
+
try {
|
|
305
|
+
let filteredEdges;
|
|
306
|
+
let kernelDistance;
|
|
307
|
+
if (typeof distance === "function") {
|
|
308
|
+
const resolved = resolveEdgeCallback$1(selectedEdges, distance);
|
|
309
|
+
if (!resolved) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.CHAMFER_NO_EDGES, "No edges with positive distance for chamfer"));
|
|
310
|
+
filteredEdges = resolved.edges;
|
|
311
|
+
kernelDistance = resolved.kernelParam;
|
|
312
|
+
} else {
|
|
313
|
+
filteredEdges = [...selectedEdges];
|
|
314
|
+
kernelDistance = distance;
|
|
315
|
+
}
|
|
316
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
317
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().chamferWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelDistance, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
318
|
+
return finalizeShape3D(evolution, resultShape, [shape], require_errors.BrepErrorCode.CHAMFER_NOT_3D, "Chamfer result is not a 3D shape");
|
|
319
|
+
} catch (e) {
|
|
320
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
321
|
+
return require_errors.err(require_errors.kernelError("CHAMFER_FAILED", `Chamfer operation failed: ${raw}`, e, {
|
|
322
|
+
operation: "chamfer",
|
|
323
|
+
edgeCount: selectedEdges.length,
|
|
324
|
+
distance
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Create a hollow shell by removing faces and offsetting remaining walls.
|
|
330
|
+
*
|
|
331
|
+
* @param shape - The solid to hollow out.
|
|
332
|
+
* @param faces - Faces to remove.
|
|
333
|
+
* @param thickness - Wall thickness.
|
|
334
|
+
* @param tolerance - Shell operation tolerance (default 1e-3).
|
|
335
|
+
*/
|
|
336
|
+
function shell(shape, faces, thickness, tolerance = .001, { trackEvolution = true } = {}) {
|
|
337
|
+
const check = validateNotNull$1(shape, "shell: shape");
|
|
338
|
+
if (require_errors.isErr(check)) return check;
|
|
339
|
+
if (thickness <= 0) return require_errors.err(require_errors.validationError("INVALID_THICKNESS", "Shell thickness must be positive"));
|
|
340
|
+
if (faces.length === 0) return require_errors.err(require_errors.validationError("NO_FACES", "At least one face must be specified for shell"));
|
|
341
|
+
try {
|
|
342
|
+
if (!trackEvolution) {
|
|
343
|
+
const resultShape = require_shapeTypes.getKernel().shell(shape.wrapped, faces.map((f) => f.wrapped), thickness, tolerance);
|
|
344
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
345
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
346
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
347
|
+
return require_errors.err(require_errors.kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
348
|
+
}
|
|
349
|
+
return require_errors.ok(cast);
|
|
350
|
+
}
|
|
351
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
352
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().shellWithHistory(shape.wrapped, faces.map((f) => f.wrapped), thickness, inputFaceHashes, require_constants.HASH_CODE_MAX, tolerance);
|
|
353
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
354
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
355
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
356
|
+
return require_errors.err(require_errors.kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
357
|
+
}
|
|
358
|
+
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
359
|
+
return require_errors.ok(cast);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
362
|
+
return require_errors.err(require_errors.kernelError("SHELL_FAILED", `Shell operation failed: ${raw}`, e, {
|
|
363
|
+
operation: "shell",
|
|
364
|
+
faceCount: faces.length,
|
|
365
|
+
thickness
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Offset all faces of a shape by a given distance.
|
|
371
|
+
*
|
|
372
|
+
* @param shape - The shape to offset (must be a 3D shape with faces).
|
|
373
|
+
* @param distance - Offset distance (positive = outward, negative = inward).
|
|
374
|
+
* @param tolerance - Offset tolerance (default 1e-6).
|
|
375
|
+
*/
|
|
376
|
+
function offset(shape, distance, tolerance = 1e-6) {
|
|
377
|
+
const check = validateNotNull$1(shape, "offset: shape");
|
|
378
|
+
if (require_errors.isErr(check)) return check;
|
|
379
|
+
if (Math.abs(distance) < 1e-10) return require_errors.err(require_errors.validationError("ZERO_OFFSET", "Offset distance cannot be zero"));
|
|
380
|
+
try {
|
|
381
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
382
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().offsetWithHistory(shape.wrapped, distance, inputFaceHashes, require_constants.HASH_CODE_MAX, tolerance);
|
|
383
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
384
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
385
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
386
|
+
return require_errors.err(require_errors.kernelError("OFFSET_RESULT_NOT_3D", "Offset result is not a 3D shape"));
|
|
387
|
+
}
|
|
388
|
+
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
389
|
+
return require_errors.ok(cast);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
392
|
+
return require_errors.err(require_errors.kernelError("OFFSET_FAILED", `Offset operation failed: ${raw}`, e));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Validate draft inputs (null shape, scalar angle bounds, faces non-empty).
|
|
397
|
+
* Returns an Err Result on failure, `undefined` on success.
|
|
398
|
+
*/
|
|
399
|
+
function validateDraftInputs(shape, faces, angle) {
|
|
400
|
+
const check = validateNotNull$1(shape, "draft: shape");
|
|
401
|
+
if (require_errors.isErr(check)) return check;
|
|
402
|
+
if (typeof angle === "number") {
|
|
403
|
+
if (Math.abs(angle) < 1e-10) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.DRAFT_INVALID_ANGLE, "Draft angle cannot be zero", void 0, void 0, "Provide a non-zero angle in degrees"));
|
|
404
|
+
if (Math.abs(angle) >= 90) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
405
|
+
}
|
|
406
|
+
if (faces.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.DRAFT_NO_FACES, "No faces specified for draft", void 0, void 0, "Select at least one face to apply the draft angle to"));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Apply a draft (taper) to selected faces of a 3D shape.
|
|
410
|
+
*
|
|
411
|
+
* Draft tilts faces by a specified angle relative to a pull direction,
|
|
412
|
+
* pivoting about a neutral plane. This is essential for injection molding
|
|
413
|
+
* and casting workflows where parts must release from a mold.
|
|
414
|
+
*
|
|
415
|
+
* @param shape - The solid to modify.
|
|
416
|
+
* @param faces - Faces to draft.
|
|
417
|
+
* @param pullDirection - Mold opening direction vector.
|
|
418
|
+
* @param neutralPlane - A point on the plane where faces are not displaced.
|
|
419
|
+
* @param angle - Constant angle in degrees, or per-face callback returning degrees (null to skip).
|
|
420
|
+
*/
|
|
421
|
+
function draft(shape, faces, pullDirection, neutralPlane, angle) {
|
|
422
|
+
const inputErr = validateDraftInputs(shape, faces, angle);
|
|
423
|
+
if (inputErr) return inputErr;
|
|
424
|
+
try {
|
|
425
|
+
const { filteredFaces, kernelAngle } = resolveDraftCallback(faces, angle);
|
|
426
|
+
if (filteredFaces.length === 0) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
427
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
428
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().draftWithHistory(shape.wrapped, filteredFaces.map((f) => f.wrapped), [
|
|
429
|
+
pullDirection[0],
|
|
430
|
+
pullDirection[1],
|
|
431
|
+
pullDirection[2]
|
|
432
|
+
], [
|
|
433
|
+
neutralPlane[0],
|
|
434
|
+
neutralPlane[1],
|
|
435
|
+
neutralPlane[2]
|
|
436
|
+
], kernelAngle, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
437
|
+
return finalizeShape3D(evolution, resultShape, [shape], require_errors.BrepErrorCode.DRAFT_NOT_3D, "Draft result is not a 3D shape");
|
|
438
|
+
} catch (e) {
|
|
439
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
440
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.DRAFT_FAILED, `Draft operation failed: ${raw}`, e, {
|
|
441
|
+
operation: "draft",
|
|
442
|
+
faceCount: faces.length,
|
|
443
|
+
angle
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Apply a variable-radius fillet to an edge.
|
|
449
|
+
*
|
|
450
|
+
* The radius varies along the edge according to the provided spec points.
|
|
451
|
+
* Each point specifies a normalized parameter (0 = start, 1 = end) and radius.
|
|
452
|
+
*
|
|
453
|
+
* **Cross-kernel note:** brepkit supports arbitrary multi-point radius profiles.
|
|
454
|
+
* occt-wasm supports a linear (start/end, <=2-point) profile and returns
|
|
455
|
+
* UNSUPPORTED for multi-point; the opencascade.js kernel does not implement it.
|
|
456
|
+
*/
|
|
457
|
+
function variableFillet(shape, edge, radii) {
|
|
458
|
+
if (radii.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "radii must contain at least one radius spec"));
|
|
459
|
+
for (const r of radii) if (r.radius <= 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "All radius values must be positive"));
|
|
460
|
+
const kernel = require_shapeTypes.getKernel();
|
|
461
|
+
try {
|
|
462
|
+
const spec = JSON.stringify({
|
|
463
|
+
edge: kernel.hashCode(edge.wrapped, require_constants.HASH_CODE_MAX),
|
|
464
|
+
radii: radii.map((r) => ({
|
|
465
|
+
param: r.param,
|
|
466
|
+
radius: r.radius
|
|
467
|
+
}))
|
|
468
|
+
});
|
|
469
|
+
const result = kernel.filletVariable(shape.wrapped, spec);
|
|
470
|
+
const wrapped = require_shapeTypes.castResultShape(result);
|
|
471
|
+
if (!require_shapeTypes.isShape3D(wrapped)) {
|
|
472
|
+
require_shapeTypes.disposeResultShape(wrapped);
|
|
473
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet did not produce a 3D shape"));
|
|
474
|
+
}
|
|
475
|
+
if (!require_shapeTypes.isSolid(wrapped)) {
|
|
476
|
+
require_shapeTypes.disposeResultShape(wrapped);
|
|
477
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet did not produce a solid"));
|
|
478
|
+
}
|
|
479
|
+
return require_errors.ok(wrapped);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet failed", e));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
110
485
|
//#region src/topology/chamferAngleFns.ts
|
|
111
486
|
/**
|
|
112
487
|
* Chamfer with distance + angle — functional API.
|
|
@@ -272,356 +647,84 @@ function bisectFuse(ops, shapes, startIdx, options, telemetry) {
|
|
|
272
647
|
return combineFuseHalves(ops, bisectFuse(ops, shapes.slice(0, mid), startIdx, options, telemetry), bisectFuse(ops, shapes.slice(mid), startIdx + mid, options, telemetry), shapes, startIdx, mid, options, telemetry);
|
|
273
648
|
}
|
|
274
649
|
function combineFuseHalves(ops, left, right, shapes, startIdx, mid, options, telemetry) {
|
|
275
|
-
if (left.ok && right.ok) {
|
|
276
|
-
telemetry.singletonFallbacks++;
|
|
277
|
-
const merged = tryBatch(() => ops.fuse(left.value, right.value, options));
|
|
278
|
-
if (merged && merged.ok) return merged;
|
|
279
|
-
for (let i = mid; i < shapes.length; i++) telemetry.failedInputs.add(startIdx + i);
|
|
280
|
-
return left;
|
|
281
|
-
}
|
|
282
|
-
if (left.ok) {
|
|
283
|
-
for (let i = mid; i < shapes.length; i++) telemetry.failedInputs.add(startIdx + i);
|
|
284
|
-
return left;
|
|
285
|
-
}
|
|
286
|
-
if (right.ok) {
|
|
287
|
-
for (let i = 0; i < mid; i++) telemetry.failedInputs.add(startIdx + i);
|
|
288
|
-
return right;
|
|
289
|
-
}
|
|
290
|
-
return left;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Run a batch boolean op, returning null on kernel throw (signal-aborts
|
|
294
|
-
* propagate). The caller checks for null and bisects.
|
|
295
|
-
*/
|
|
296
|
-
function tryBatch(fn) {
|
|
297
|
-
try {
|
|
298
|
-
return fn();
|
|
299
|
-
} catch {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
//#endregion
|
|
304
|
-
//#region src/topology/booleanDiagnosticFns.ts
|
|
305
|
-
/**
|
|
306
|
-
* Boolean pre-validation diagnostics.
|
|
307
|
-
*/
|
|
308
|
-
/**
|
|
309
|
-
* Pre-validate operands before a boolean operation.
|
|
310
|
-
*
|
|
311
|
-
* Checks that both shapes are non-null and topologically valid.
|
|
312
|
-
* Returns a structured report of any issues found.
|
|
313
|
-
*
|
|
314
|
-
* @example
|
|
315
|
-
* ```typescript
|
|
316
|
-
* const check = checkBoolean(base, tool, 'fuse');
|
|
317
|
-
* if (!check.valid) {
|
|
318
|
-
* console.warn('Boolean will likely fail:', check.issues);
|
|
319
|
-
* }
|
|
320
|
-
* ```
|
|
321
|
-
*/
|
|
322
|
-
function checkBoolean(base, tool, op) {
|
|
323
|
-
return require_shapeTypes.getKernel().checkBoolean(base.wrapped, tool.wrapped, op);
|
|
324
|
-
}
|
|
325
|
-
//#endregion
|
|
326
|
-
//#region src/topology/evolutionFns.ts
|
|
327
|
-
/**
|
|
328
|
-
* Evolution-tracking variants of boolean and modifier operations.
|
|
329
|
-
*
|
|
330
|
-
* These functions mirror the standard fuse/cut/intersect/fillet/chamfer/shell
|
|
331
|
-
* operations but additionally return the ShapeEvolution data, enabling
|
|
332
|
-
* persistent face selections, constraint tracking, and custom face-level logic.
|
|
333
|
-
*/
|
|
334
|
-
function validateShape3D(shape, label) {
|
|
335
|
-
if (require_shapeTypes.getKernel().isNull(shape.wrapped)) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
336
|
-
return require_errors.ok(void 0);
|
|
337
|
-
}
|
|
338
|
-
function validateNotNull$1(shape, label) {
|
|
339
|
-
if (require_shapeTypes.getKernel().isNull(shape.wrapped)) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
340
|
-
return require_errors.ok(void 0);
|
|
341
|
-
}
|
|
342
|
-
function castToShape3D(shape, errorCode, errorMsg, suggestion) {
|
|
343
|
-
const wrapped = require_shapeTypes.castShape(shape);
|
|
344
|
-
if (!require_shapeTypes.isShape3D(wrapped)) {
|
|
345
|
-
const typeName = require_shapeTypes.getShapeKind(wrapped).toUpperCase();
|
|
346
|
-
require_shapeTypes.disposeDowncastSource(shape, wrapped);
|
|
347
|
-
require_shapeTypes.disposeResultShape(wrapped);
|
|
348
|
-
return require_errors.err(require_errors.typeCastError(errorCode, `${errorMsg}. Got ${typeName} instead.`, void 0, void 0, suggestion));
|
|
349
|
-
}
|
|
350
|
-
require_shapeTypes.disposeDowncastSource(shape, wrapped);
|
|
351
|
-
return require_errors.ok(wrapped);
|
|
352
|
-
}
|
|
353
|
-
function resolveEdgeCallback$1(selectedEdges, callbackFn) {
|
|
354
|
-
const filteredEdges = [];
|
|
355
|
-
const hashToValue = /* @__PURE__ */ new Map();
|
|
356
|
-
for (const edge of selectedEdges) {
|
|
357
|
-
const val = callbackFn(edge) ?? 0;
|
|
358
|
-
if (typeof val === "number" && val <= 0) continue;
|
|
359
|
-
if (Array.isArray(val) && (val[0] <= 0 || val[1] <= 0)) continue;
|
|
360
|
-
filteredEdges.push(edge);
|
|
361
|
-
hashToValue.set(require_shapeTypes.getKernel().hashCode(edge.wrapped, require_constants.HASH_CODE_MAX), val);
|
|
362
|
-
}
|
|
363
|
-
if (filteredEdges.length === 0) return null;
|
|
364
|
-
const kernelParam = (ocEdge) => {
|
|
365
|
-
return hashToValue.get(require_shapeTypes.getKernel().hashCode(ocEdge, 2147483647)) ?? 1;
|
|
366
|
-
};
|
|
367
|
-
return {
|
|
368
|
-
edges: filteredEdges,
|
|
369
|
-
kernelParam
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function fuseWithEvolution(a, b, { optimisation = "none", simplify = false, signal, fuzzyValue } = {}) {
|
|
373
|
-
if (signal?.aborted) throw signal.reason;
|
|
374
|
-
const checkA = validateShape3D(a, "fuseWithEvolution: first operand");
|
|
375
|
-
if (require_errors.isErr(checkA)) return checkA;
|
|
376
|
-
const checkB = validateShape3D(b, "fuseWithEvolution: second operand");
|
|
377
|
-
if (require_errors.isErr(checkB)) return checkB;
|
|
378
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([a, b]);
|
|
379
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().fuseWithHistory(a.wrapped, b.wrapped, inputFaceHashes, require_constants.HASH_CODE_MAX, {
|
|
380
|
-
optimisation,
|
|
381
|
-
simplify,
|
|
382
|
-
fuzzyValue
|
|
383
|
-
});
|
|
384
|
-
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.");
|
|
385
|
-
if (fuseResult.ok) {
|
|
386
|
-
require_solidBuilders.propagateAllMetadata(evolution, [a, b], fuseResult.value);
|
|
387
|
-
return require_errors.ok({
|
|
388
|
-
shape: fuseResult.value,
|
|
389
|
-
evolution
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
return fuseResult;
|
|
393
|
-
}
|
|
394
|
-
function cutWithEvolution(base, tool, { optimisation = "none", simplify = false, signal, fuzzyValue } = {}) {
|
|
395
|
-
if (signal?.aborted) throw signal.reason;
|
|
396
|
-
const checkBase = validateShape3D(base, "cutWithEvolution: base");
|
|
397
|
-
if (require_errors.isErr(checkBase)) return checkBase;
|
|
398
|
-
const checkTool = validateShape3D(tool, "cutWithEvolution: tool");
|
|
399
|
-
if (require_errors.isErr(checkTool)) return checkTool;
|
|
400
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([base, tool]);
|
|
401
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().cutWithHistory(base.wrapped, tool.wrapped, inputFaceHashes, require_constants.HASH_CODE_MAX, {
|
|
402
|
-
optimisation,
|
|
403
|
-
simplify,
|
|
404
|
-
fuzzyValue
|
|
405
|
-
});
|
|
406
|
-
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.");
|
|
407
|
-
if (cutResult.ok) {
|
|
408
|
-
require_solidBuilders.propagateAllMetadata(evolution, [base, tool], cutResult.value);
|
|
409
|
-
return require_errors.ok({
|
|
410
|
-
shape: cutResult.value,
|
|
411
|
-
evolution
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
return cutResult;
|
|
415
|
-
}
|
|
416
|
-
function intersectWithEvolution(a, b, { simplify = false, signal, fuzzyValue } = {}) {
|
|
417
|
-
if (signal?.aborted) throw signal.reason;
|
|
418
|
-
const checkA = validateShape3D(a, "intersectWithEvolution: first operand");
|
|
419
|
-
if (require_errors.isErr(checkA)) return checkA;
|
|
420
|
-
const checkB = validateShape3D(b, "intersectWithEvolution: second operand");
|
|
421
|
-
if (require_errors.isErr(checkB)) return checkB;
|
|
422
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([a, b]);
|
|
423
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().intersectWithHistory(a.wrapped, b.wrapped, inputFaceHashes, require_constants.HASH_CODE_MAX, {
|
|
424
|
-
simplify,
|
|
425
|
-
fuzzyValue
|
|
426
|
-
});
|
|
427
|
-
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.");
|
|
428
|
-
if (intResult.ok) {
|
|
429
|
-
require_solidBuilders.propagateAllMetadata(evolution, [a, b], intResult.value);
|
|
430
|
-
return require_errors.ok({
|
|
431
|
-
shape: intResult.value,
|
|
432
|
-
evolution
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
return intResult;
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Apply a fillet (rounded edge) to selected edges, returning both
|
|
439
|
-
* the result shape and the face evolution data.
|
|
440
|
-
*
|
|
441
|
-
* @param shape - The shape to modify.
|
|
442
|
-
* @param edges - Edges to fillet. Pass `undefined` to fillet all edges.
|
|
443
|
-
* @param radius - Constant radius, variable radius `[r1, r2]`, or per-edge callback.
|
|
444
|
-
*/
|
|
445
|
-
function filletWithEvolution(shape, edges, radius) {
|
|
446
|
-
const check = validateNotNull$1(shape, "filletWithEvolution: shape");
|
|
447
|
-
if (require_errors.isErr(check)) return check;
|
|
448
|
-
if (typeof radius === "number" && radius <= 0) return require_errors.err(require_errors.validationError("INVALID_FILLET_RADIUS", "Fillet radius must be positive", void 0, void 0, "Provide a positive radius value greater than 0"));
|
|
449
|
-
if (Array.isArray(radius) && (radius[0] <= 0 || radius[1] <= 0)) return require_errors.err(require_errors.validationError("INVALID_FILLET_RADIUS", "Fillet radii must both be positive", void 0, void 0, "Both radius values must be greater than 0"));
|
|
450
|
-
const selectedEdges = edges ?? require_topologyQueryFns.getEdges(shape);
|
|
451
|
-
if (selectedEdges.length === 0) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
452
|
-
try {
|
|
453
|
-
let filteredEdges;
|
|
454
|
-
let kernelRadius;
|
|
455
|
-
if (typeof radius === "function") {
|
|
456
|
-
const resolved = resolveEdgeCallback$1(selectedEdges, radius);
|
|
457
|
-
if (!resolved) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.FILLET_NO_EDGES, "No edges with positive radius for fillet", void 0, void 0, "Check that the radius callback returns positive values"));
|
|
458
|
-
filteredEdges = resolved.edges;
|
|
459
|
-
kernelRadius = resolved.kernelParam;
|
|
460
|
-
} else {
|
|
461
|
-
filteredEdges = [...selectedEdges];
|
|
462
|
-
kernelRadius = radius;
|
|
463
|
-
}
|
|
464
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
465
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().filletWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelRadius, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
466
|
-
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
467
|
-
if (!require_shapeTypes.isShape3D(cast)) {
|
|
468
|
-
require_shapeTypes.disposeResultShape(cast);
|
|
469
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.FILLET_NOT_3D, "Fillet result is not a 3D shape"));
|
|
470
|
-
}
|
|
471
|
-
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
472
|
-
return require_errors.ok({
|
|
473
|
-
shape: cast,
|
|
474
|
-
evolution
|
|
475
|
-
});
|
|
476
|
-
} catch (e) {
|
|
477
|
-
const raw = e instanceof Error ? e.message : String(e);
|
|
478
|
-
return require_errors.err(require_errors.kernelError("FILLET_FAILED", `Fillet operation failed: ${raw}`, e, {
|
|
479
|
-
operation: "fillet",
|
|
480
|
-
edgeCount: selectedEdges.length,
|
|
481
|
-
radius
|
|
482
|
-
}));
|
|
650
|
+
if (left.ok && right.ok) {
|
|
651
|
+
telemetry.singletonFallbacks++;
|
|
652
|
+
const merged = tryBatch(() => ops.fuse(left.value, right.value, options));
|
|
653
|
+
if (merged && merged.ok) return merged;
|
|
654
|
+
for (let i = mid; i < shapes.length; i++) telemetry.failedInputs.add(startIdx + i);
|
|
655
|
+
return left;
|
|
483
656
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
* @param distance - Symmetric distance, asymmetric `[d1, d2]`, or per-edge callback.
|
|
492
|
-
*/
|
|
493
|
-
function chamferWithEvolution(shape, edges, distance) {
|
|
494
|
-
const check = validateNotNull$1(shape, "chamferWithEvolution: shape");
|
|
495
|
-
if (require_errors.isErr(check)) return check;
|
|
496
|
-
if (typeof distance === "number" && distance <= 0) return require_errors.err(require_errors.validationError("INVALID_CHAMFER_DISTANCE", "Chamfer distance must be positive", void 0, void 0, "Provide a positive distance value greater than 0"));
|
|
497
|
-
if (Array.isArray(distance) && (distance[0] <= 0 || distance[1] <= 0)) return require_errors.err(require_errors.validationError("INVALID_CHAMFER_DISTANCE", "Chamfer distances must both be positive", void 0, void 0, "Both distance values must be greater than 0"));
|
|
498
|
-
const selectedEdges = edges ?? require_topologyQueryFns.getEdges(shape);
|
|
499
|
-
if (selectedEdges.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.CHAMFER_NO_EDGES, "No edges found for chamfer"));
|
|
500
|
-
try {
|
|
501
|
-
let filteredEdges;
|
|
502
|
-
let kernelDistance;
|
|
503
|
-
if (typeof distance === "function") {
|
|
504
|
-
const resolved = resolveEdgeCallback$1(selectedEdges, distance);
|
|
505
|
-
if (!resolved) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.CHAMFER_NO_EDGES, "No edges with positive distance for chamfer"));
|
|
506
|
-
filteredEdges = resolved.edges;
|
|
507
|
-
kernelDistance = resolved.kernelParam;
|
|
508
|
-
} else {
|
|
509
|
-
filteredEdges = [...selectedEdges];
|
|
510
|
-
kernelDistance = distance;
|
|
511
|
-
}
|
|
512
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
513
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().chamferWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelDistance, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
514
|
-
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
515
|
-
if (!require_shapeTypes.isShape3D(cast)) {
|
|
516
|
-
require_shapeTypes.disposeResultShape(cast);
|
|
517
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.CHAMFER_NOT_3D, "Chamfer result is not a 3D shape"));
|
|
518
|
-
}
|
|
519
|
-
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
520
|
-
return require_errors.ok({
|
|
521
|
-
shape: cast,
|
|
522
|
-
evolution
|
|
523
|
-
});
|
|
524
|
-
} catch (e) {
|
|
525
|
-
const raw = e instanceof Error ? e.message : String(e);
|
|
526
|
-
return require_errors.err(require_errors.kernelError("CHAMFER_FAILED", `Chamfer operation failed: ${raw}`, e, {
|
|
527
|
-
operation: "chamfer",
|
|
528
|
-
edgeCount: selectedEdges.length,
|
|
529
|
-
distance
|
|
530
|
-
}));
|
|
657
|
+
if (left.ok) {
|
|
658
|
+
for (let i = mid; i < shapes.length; i++) telemetry.failedInputs.add(startIdx + i);
|
|
659
|
+
return left;
|
|
660
|
+
}
|
|
661
|
+
if (right.ok) {
|
|
662
|
+
for (let i = 0; i < mid; i++) telemetry.failedInputs.add(startIdx + i);
|
|
663
|
+
return right;
|
|
531
664
|
}
|
|
665
|
+
return left;
|
|
532
666
|
}
|
|
533
667
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* @param shape - The solid to hollow out.
|
|
538
|
-
* @param faces - Faces to remove.
|
|
539
|
-
* @param thickness - Wall thickness.
|
|
540
|
-
* @param tolerance - Shell operation tolerance (default 1e-3).
|
|
668
|
+
* Run a batch boolean op, returning null on kernel throw (signal-aborts
|
|
669
|
+
* propagate). The caller checks for null and bisects.
|
|
541
670
|
*/
|
|
542
|
-
function
|
|
543
|
-
const check = validateNotNull$1(shape, "shellWithEvolution: shape");
|
|
544
|
-
if (require_errors.isErr(check)) return check;
|
|
545
|
-
if (thickness <= 0) return require_errors.err(require_errors.validationError("INVALID_THICKNESS", "Shell thickness must be positive"));
|
|
546
|
-
if (faces.length === 0) return require_errors.err(require_errors.validationError("NO_FACES", "At least one face must be specified for shell"));
|
|
671
|
+
function tryBatch(fn) {
|
|
547
672
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (!require_shapeTypes.isShape3D(cast)) {
|
|
552
|
-
require_shapeTypes.disposeResultShape(cast);
|
|
553
|
-
return require_errors.err(require_errors.kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
554
|
-
}
|
|
555
|
-
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
556
|
-
return require_errors.ok({
|
|
557
|
-
shape: cast,
|
|
558
|
-
evolution
|
|
559
|
-
});
|
|
560
|
-
} catch (e) {
|
|
561
|
-
const raw = e instanceof Error ? e.message : String(e);
|
|
562
|
-
return require_errors.err(require_errors.kernelError("SHELL_FAILED", `Shell operation failed: ${raw}`, e, {
|
|
563
|
-
operation: "shell",
|
|
564
|
-
faceCount: faces.length,
|
|
565
|
-
thickness
|
|
566
|
-
}));
|
|
673
|
+
return fn();
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
567
676
|
}
|
|
568
677
|
}
|
|
569
678
|
//#endregion
|
|
570
|
-
//#region src/topology/
|
|
679
|
+
//#region src/topology/booleanDiagnosticFns.ts
|
|
571
680
|
/**
|
|
572
|
-
*
|
|
681
|
+
* Boolean pre-validation diagnostics.
|
|
573
682
|
*/
|
|
574
683
|
/**
|
|
575
|
-
*
|
|
684
|
+
* Pre-validate operands before a boolean operation.
|
|
576
685
|
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
686
|
+
* Checks that both shapes are non-null and topologically valid.
|
|
687
|
+
* Returns a structured report of any issues found.
|
|
579
688
|
*
|
|
580
|
-
* @
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
689
|
+
* @example
|
|
690
|
+
* ```typescript
|
|
691
|
+
* const check = checkBoolean(base, tool, 'fuse');
|
|
692
|
+
* if (!check.valid) {
|
|
693
|
+
* console.warn('Boolean will likely fail:', check.issues);
|
|
694
|
+
* }
|
|
695
|
+
* ```
|
|
584
696
|
*/
|
|
585
|
-
function
|
|
586
|
-
|
|
587
|
-
const result = require_shapeTypes.getKernel().positionOnCurve(shape.wrapped, spine.wrapped, param);
|
|
588
|
-
const wrapped = require_shapeTypes.castResultShape(result);
|
|
589
|
-
if (!require_shapeTypes.isShape3D(wrapped)) {
|
|
590
|
-
require_shapeTypes.disposeResultShape(wrapped);
|
|
591
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.POSITION_ON_CURVE_FAILED, "positionOnCurve did not produce a 3D shape"));
|
|
592
|
-
}
|
|
593
|
-
return require_errors.ok(wrapped);
|
|
594
|
-
} catch (e) {
|
|
595
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.POSITION_ON_CURVE_FAILED, `Failed to position shape on curve at param ${param}`, e));
|
|
596
|
-
}
|
|
697
|
+
function checkBoolean(base, tool, op) {
|
|
698
|
+
return require_shapeTypes.getKernel().checkBoolean(base.wrapped, tool.wrapped, op);
|
|
597
699
|
}
|
|
598
700
|
//#endregion
|
|
599
|
-
//#region src/topology/
|
|
701
|
+
//#region src/topology/evolutionFns.ts
|
|
600
702
|
/**
|
|
601
|
-
*
|
|
703
|
+
* Evolution-tracking variants of boolean and modifier operations.
|
|
602
704
|
*
|
|
603
|
-
* These
|
|
604
|
-
*
|
|
705
|
+
* These functions mirror the standard fuse/cut/intersect/fillet/chamfer/shell
|
|
706
|
+
* operations but additionally return the ShapeEvolution data, enabling
|
|
707
|
+
* persistent face selections, constraint tracking, and custom face-level logic.
|
|
605
708
|
*/
|
|
709
|
+
function validateShape3D(shape, label) {
|
|
710
|
+
if (require_shapeTypes.getKernel().isNull(shape.wrapped)) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
711
|
+
return require_errors.ok(void 0);
|
|
712
|
+
}
|
|
606
713
|
function validateNotNull(shape, label) {
|
|
607
714
|
if (require_shapeTypes.getKernel().isNull(shape.wrapped)) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.NULL_SHAPE_INPUT, `${label} is a null shape`));
|
|
608
715
|
return require_errors.ok(void 0);
|
|
609
716
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
717
|
+
function castToShape3D(shape, errorCode, errorMsg, suggestion) {
|
|
718
|
+
const wrapped = require_shapeTypes.castShape(shape);
|
|
719
|
+
if (!require_shapeTypes.isShape3D(wrapped)) {
|
|
720
|
+
const typeName = require_shapeTypes.getShapeKind(wrapped).toUpperCase();
|
|
721
|
+
require_shapeTypes.disposeDowncastSource(shape, wrapped);
|
|
722
|
+
require_shapeTypes.disposeResultShape(wrapped);
|
|
723
|
+
return require_errors.err(require_errors.typeCastError(errorCode, `${errorMsg}. Got ${typeName} instead.`, void 0, void 0, suggestion));
|
|
724
|
+
}
|
|
725
|
+
require_shapeTypes.disposeDowncastSource(shape, wrapped);
|
|
726
|
+
return require_errors.ok(wrapped);
|
|
620
727
|
}
|
|
621
|
-
/**
|
|
622
|
-
* When the user supplies a per-edge callback, pre-filter edges and build a
|
|
623
|
-
* hash-indexed lookup for the kernel. Returns `null` if no edges survive.
|
|
624
|
-
*/
|
|
625
728
|
function resolveEdgeCallback(selectedEdges, callbackFn) {
|
|
626
729
|
const filteredEdges = [];
|
|
627
730
|
const hashToValue = /* @__PURE__ */ new Map();
|
|
@@ -634,160 +737,139 @@ function resolveEdgeCallback(selectedEdges, callbackFn) {
|
|
|
634
737
|
}
|
|
635
738
|
if (filteredEdges.length === 0) return null;
|
|
636
739
|
const kernelParam = (ocEdge) => {
|
|
637
|
-
|
|
638
|
-
if (v === void 0) throw new Error("fillet/chamfer: edge hash not found — possible hash collision");
|
|
639
|
-
return v;
|
|
740
|
+
return hashToValue.get(require_shapeTypes.getKernel().hashCode(ocEdge, 2147483647)) ?? 1;
|
|
640
741
|
};
|
|
641
742
|
return {
|
|
642
743
|
edges: filteredEdges,
|
|
643
744
|
kernelParam
|
|
644
745
|
};
|
|
645
746
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
filteredFaces: [...faces],
|
|
666
|
-
kernelAngle: angle
|
|
667
|
-
};
|
|
668
|
-
const filteredFaces = [];
|
|
669
|
-
const hashToAngle = /* @__PURE__ */ new Map();
|
|
670
|
-
for (const face of faces) {
|
|
671
|
-
const a = angle(face);
|
|
672
|
-
if (a === null || a === 0 || Math.abs(a) >= 90) continue;
|
|
673
|
-
filteredFaces.push(face);
|
|
674
|
-
hashToAngle.set(require_shapeTypes.getKernel().hashCode(face.wrapped, require_constants.HASH_CODE_MAX), a);
|
|
747
|
+
function fuseWithEvolution(a, b, { optimisation = "none", simplify = false, signal, fuzzyValue } = {}) {
|
|
748
|
+
if (signal?.aborted) throw signal.reason;
|
|
749
|
+
const checkA = validateShape3D(a, "fuseWithEvolution: first operand");
|
|
750
|
+
if (require_errors.isErr(checkA)) return checkA;
|
|
751
|
+
const checkB = validateShape3D(b, "fuseWithEvolution: second operand");
|
|
752
|
+
if (require_errors.isErr(checkB)) return checkB;
|
|
753
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([a, b]);
|
|
754
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().fuseWithHistory(a.wrapped, b.wrapped, inputFaceHashes, require_constants.HASH_CODE_MAX, {
|
|
755
|
+
optimisation,
|
|
756
|
+
simplify,
|
|
757
|
+
fuzzyValue
|
|
758
|
+
});
|
|
759
|
+
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.");
|
|
760
|
+
if (fuseResult.ok) {
|
|
761
|
+
require_solidBuilders.propagateAllMetadata(evolution, [a, b], fuseResult.value);
|
|
762
|
+
return require_errors.ok({
|
|
763
|
+
shape: fuseResult.value,
|
|
764
|
+
evolution
|
|
765
|
+
});
|
|
675
766
|
}
|
|
676
|
-
|
|
677
|
-
const a = hashToAngle.get(require_shapeTypes.getKernel().hashCode(ocFace, require_constants.HASH_CODE_MAX));
|
|
678
|
-
if (a === void 0) throw new Error("draft: face hash not found — possible hash collision");
|
|
679
|
-
return a;
|
|
680
|
-
};
|
|
681
|
-
return {
|
|
682
|
-
filteredFaces,
|
|
683
|
-
kernelAngle
|
|
684
|
-
};
|
|
767
|
+
return fuseResult;
|
|
685
768
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
require_solidBuilders.propagateAllMetadata(evolution, [
|
|
701
|
-
return require_errors.ok(
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
769
|
+
function cutWithEvolution(base, tool, { optimisation = "none", simplify = false, signal, fuzzyValue } = {}) {
|
|
770
|
+
if (signal?.aborted) throw signal.reason;
|
|
771
|
+
const checkBase = validateShape3D(base, "cutWithEvolution: base");
|
|
772
|
+
if (require_errors.isErr(checkBase)) return checkBase;
|
|
773
|
+
const checkTool = validateShape3D(tool, "cutWithEvolution: tool");
|
|
774
|
+
if (require_errors.isErr(checkTool)) return checkTool;
|
|
775
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([base, tool]);
|
|
776
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().cutWithHistory(base.wrapped, tool.wrapped, inputFaceHashes, require_constants.HASH_CODE_MAX, {
|
|
777
|
+
optimisation,
|
|
778
|
+
simplify,
|
|
779
|
+
fuzzyValue
|
|
780
|
+
});
|
|
781
|
+
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.");
|
|
782
|
+
if (cutResult.ok) {
|
|
783
|
+
require_solidBuilders.propagateAllMetadata(evolution, [base, tool], cutResult.value);
|
|
784
|
+
return require_errors.ok({
|
|
785
|
+
shape: cutResult.value,
|
|
786
|
+
evolution
|
|
787
|
+
});
|
|
705
788
|
}
|
|
789
|
+
return cutResult;
|
|
706
790
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
pair: "Fillet radii must both be positive",
|
|
718
|
-
scalarHint: "Provide a positive radius value greater than 0",
|
|
719
|
-
pairHint: "Both radius values must be greater than 0"
|
|
791
|
+
function intersectWithEvolution(a, b, { simplify = false, signal, fuzzyValue } = {}) {
|
|
792
|
+
if (signal?.aborted) throw signal.reason;
|
|
793
|
+
const checkA = validateShape3D(a, "intersectWithEvolution: first operand");
|
|
794
|
+
if (require_errors.isErr(checkA)) return checkA;
|
|
795
|
+
const checkB = validateShape3D(b, "intersectWithEvolution: second operand");
|
|
796
|
+
if (require_errors.isErr(checkB)) return checkB;
|
|
797
|
+
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([a, b]);
|
|
798
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().intersectWithHistory(a.wrapped, b.wrapped, inputFaceHashes, require_constants.HASH_CODE_MAX, {
|
|
799
|
+
simplify,
|
|
800
|
+
fuzzyValue
|
|
720
801
|
});
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (typeof radius === "function") {
|
|
725
|
-
const resolved = resolveEdgeCallback(selectedEdges, radius);
|
|
726
|
-
if (!resolved) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.FILLET_NO_EDGES, "No edges with positive radius for fillet", void 0, void 0, "Check that the radius callback returns positive values"));
|
|
802
|
+
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.");
|
|
803
|
+
if (intResult.ok) {
|
|
804
|
+
require_solidBuilders.propagateAllMetadata(evolution, [a, b], intResult.value);
|
|
727
805
|
return require_errors.ok({
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
selectedCount: selectedEdges.length
|
|
806
|
+
shape: intResult.value,
|
|
807
|
+
evolution
|
|
731
808
|
});
|
|
732
809
|
}
|
|
733
|
-
return
|
|
734
|
-
filteredEdges: [...selectedEdges],
|
|
735
|
-
kernelRadius: radius,
|
|
736
|
-
selectedCount: selectedEdges.length
|
|
737
|
-
});
|
|
810
|
+
return intResult;
|
|
738
811
|
}
|
|
739
812
|
/**
|
|
740
|
-
* Apply a fillet (rounded edge) to selected edges
|
|
813
|
+
* Apply a fillet (rounded edge) to selected edges, returning both
|
|
814
|
+
* the result shape and the face evolution data.
|
|
741
815
|
*
|
|
742
816
|
* @param shape - The shape to modify.
|
|
743
817
|
* @param edges - Edges to fillet. Pass `undefined` to fillet all edges.
|
|
744
818
|
* @param radius - Constant radius, variable radius `[r1, r2]`, or per-edge callback.
|
|
745
819
|
*/
|
|
746
|
-
function
|
|
747
|
-
const
|
|
748
|
-
if (require_errors.isErr(
|
|
749
|
-
|
|
820
|
+
function filletWithEvolution(shape, edges, radius) {
|
|
821
|
+
const check = validateNotNull(shape, "filletWithEvolution: shape");
|
|
822
|
+
if (require_errors.isErr(check)) return check;
|
|
823
|
+
if (typeof radius === "number" && radius <= 0) return require_errors.err(require_errors.validationError("INVALID_FILLET_RADIUS", "Fillet radius must be positive", void 0, void 0, "Provide a positive radius value greater than 0"));
|
|
824
|
+
if (Array.isArray(radius) && (radius[0] <= 0 || radius[1] <= 0)) return require_errors.err(require_errors.validationError("INVALID_FILLET_RADIUS", "Fillet radii must both be positive", void 0, void 0, "Both radius values must be greater than 0"));
|
|
825
|
+
const selectedEdges = edges ?? require_topologyQueryFns.getEdges(shape);
|
|
826
|
+
if (selectedEdges.length === 0) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
750
827
|
try {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
if (!
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
828
|
+
let filteredEdges;
|
|
829
|
+
let kernelRadius;
|
|
830
|
+
if (typeof radius === "function") {
|
|
831
|
+
const resolved = resolveEdgeCallback(selectedEdges, radius);
|
|
832
|
+
if (!resolved) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.FILLET_NO_EDGES, "No edges with positive radius for fillet", void 0, void 0, "Check that the radius callback returns positive values"));
|
|
833
|
+
filteredEdges = resolved.edges;
|
|
834
|
+
kernelRadius = resolved.kernelParam;
|
|
835
|
+
} else {
|
|
836
|
+
filteredEdges = [...selectedEdges];
|
|
837
|
+
kernelRadius = radius;
|
|
760
838
|
}
|
|
761
839
|
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
762
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().filletWithHistory(shape.wrapped,
|
|
763
|
-
|
|
840
|
+
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().filletWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelRadius, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
841
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
842
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
843
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
844
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.FILLET_NOT_3D, "Fillet result is not a 3D shape"));
|
|
845
|
+
}
|
|
846
|
+
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
847
|
+
return require_errors.ok({
|
|
848
|
+
shape: cast,
|
|
849
|
+
evolution
|
|
850
|
+
});
|
|
764
851
|
} catch (e) {
|
|
765
852
|
const raw = e instanceof Error ? e.message : String(e);
|
|
766
853
|
return require_errors.err(require_errors.kernelError("FILLET_FAILED", `Fillet operation failed: ${raw}`, e, {
|
|
767
854
|
operation: "fillet",
|
|
768
|
-
edgeCount:
|
|
855
|
+
edgeCount: selectedEdges.length,
|
|
769
856
|
radius
|
|
770
857
|
}));
|
|
771
858
|
}
|
|
772
859
|
}
|
|
773
860
|
/**
|
|
774
|
-
* Apply a chamfer (beveled edge) to selected edges
|
|
861
|
+
* Apply a chamfer (beveled edge) to selected edges, returning both
|
|
862
|
+
* the result shape and the face evolution data.
|
|
775
863
|
*
|
|
776
864
|
* @param shape - The shape to modify.
|
|
777
865
|
* @param edges - Edges to chamfer. Pass `undefined` to chamfer all edges.
|
|
778
866
|
* @param distance - Symmetric distance, asymmetric `[d1, d2]`, or per-edge callback.
|
|
779
867
|
*/
|
|
780
|
-
function
|
|
781
|
-
const check = validateNotNull(shape, "
|
|
868
|
+
function chamferWithEvolution(shape, edges, distance) {
|
|
869
|
+
const check = validateNotNull(shape, "chamferWithEvolution: shape");
|
|
782
870
|
if (require_errors.isErr(check)) return check;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
scalar: "Chamfer distance must be positive",
|
|
786
|
-
pair: "Chamfer distances must both be positive",
|
|
787
|
-
scalarHint: "Provide a positive distance value greater than 0",
|
|
788
|
-
pairHint: "Both distance values must be greater than 0"
|
|
789
|
-
});
|
|
790
|
-
if (paramErr) return paramErr;
|
|
871
|
+
if (typeof distance === "number" && distance <= 0) return require_errors.err(require_errors.validationError("INVALID_CHAMFER_DISTANCE", "Chamfer distance must be positive", void 0, void 0, "Provide a positive distance value greater than 0"));
|
|
872
|
+
if (Array.isArray(distance) && (distance[0] <= 0 || distance[1] <= 0)) return require_errors.err(require_errors.validationError("INVALID_CHAMFER_DISTANCE", "Chamfer distances must both be positive", void 0, void 0, "Both distance values must be greater than 0"));
|
|
791
873
|
const selectedEdges = edges ?? require_topologyQueryFns.getEdges(shape);
|
|
792
874
|
if (selectedEdges.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.CHAMFER_NO_EDGES, "No edges found for chamfer"));
|
|
793
875
|
try {
|
|
@@ -804,7 +886,16 @@ function chamfer(shape, edges, distance) {
|
|
|
804
886
|
}
|
|
805
887
|
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
806
888
|
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().chamferWithHistory(shape.wrapped, filteredEdges.map((e) => e.wrapped), kernelDistance, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
807
|
-
|
|
889
|
+
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
890
|
+
if (!require_shapeTypes.isShape3D(cast)) {
|
|
891
|
+
require_shapeTypes.disposeResultShape(cast);
|
|
892
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.CHAMFER_NOT_3D, "Chamfer result is not a 3D shape"));
|
|
893
|
+
}
|
|
894
|
+
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
895
|
+
return require_errors.ok({
|
|
896
|
+
shape: cast,
|
|
897
|
+
evolution
|
|
898
|
+
});
|
|
808
899
|
} catch (e) {
|
|
809
900
|
const raw = e instanceof Error ? e.message : String(e);
|
|
810
901
|
return require_errors.err(require_errors.kernelError("CHAMFER_FAILED", `Chamfer operation failed: ${raw}`, e, {
|
|
@@ -815,28 +906,20 @@ function chamfer(shape, edges, distance) {
|
|
|
815
906
|
}
|
|
816
907
|
}
|
|
817
908
|
/**
|
|
818
|
-
* Create a hollow shell by removing faces and offsetting remaining walls
|
|
909
|
+
* Create a hollow shell by removing faces and offsetting remaining walls,
|
|
910
|
+
* returning both the result shape and the face evolution data.
|
|
819
911
|
*
|
|
820
912
|
* @param shape - The solid to hollow out.
|
|
821
913
|
* @param faces - Faces to remove.
|
|
822
914
|
* @param thickness - Wall thickness.
|
|
823
915
|
* @param tolerance - Shell operation tolerance (default 1e-3).
|
|
824
916
|
*/
|
|
825
|
-
function
|
|
826
|
-
const check = validateNotNull(shape, "
|
|
917
|
+
function shellWithEvolution(shape, faces, thickness, tolerance = .001) {
|
|
918
|
+
const check = validateNotNull(shape, "shellWithEvolution: shape");
|
|
827
919
|
if (require_errors.isErr(check)) return check;
|
|
828
920
|
if (thickness <= 0) return require_errors.err(require_errors.validationError("INVALID_THICKNESS", "Shell thickness must be positive"));
|
|
829
921
|
if (faces.length === 0) return require_errors.err(require_errors.validationError("NO_FACES", "At least one face must be specified for shell"));
|
|
830
922
|
try {
|
|
831
|
-
if (!trackEvolution) {
|
|
832
|
-
const resultShape = require_shapeTypes.getKernel().shell(shape.wrapped, faces.map((f) => f.wrapped), thickness, tolerance);
|
|
833
|
-
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
834
|
-
if (!require_shapeTypes.isShape3D(cast)) {
|
|
835
|
-
require_shapeTypes.disposeResultShape(cast);
|
|
836
|
-
return require_errors.err(require_errors.kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
837
|
-
}
|
|
838
|
-
return require_errors.ok(cast);
|
|
839
|
-
}
|
|
840
923
|
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
841
924
|
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().shellWithHistory(shape.wrapped, faces.map((f) => f.wrapped), thickness, inputFaceHashes, require_constants.HASH_CODE_MAX, tolerance);
|
|
842
925
|
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
@@ -845,7 +928,10 @@ function shell(shape, faces, thickness, tolerance = .001, { trackEvolution = tru
|
|
|
845
928
|
return require_errors.err(require_errors.kernelError("SHELL_RESULT_NOT_3D", "Shell result is not a 3D shape"));
|
|
846
929
|
}
|
|
847
930
|
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
848
|
-
return require_errors.ok(
|
|
931
|
+
return require_errors.ok({
|
|
932
|
+
shape: cast,
|
|
933
|
+
evolution
|
|
934
|
+
});
|
|
849
935
|
} catch (e) {
|
|
850
936
|
const raw = e instanceof Error ? e.message : String(e);
|
|
851
937
|
return require_errors.err(require_errors.kernelError("SHELL_FAILED", `Shell operation failed: ${raw}`, e, {
|
|
@@ -855,119 +941,33 @@ function shell(shape, faces, thickness, tolerance = .001, { trackEvolution = tru
|
|
|
855
941
|
}));
|
|
856
942
|
}
|
|
857
943
|
}
|
|
944
|
+
//#endregion
|
|
945
|
+
//#region src/topology/positionFns.ts
|
|
858
946
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* @param shape - The shape to offset (must be a 3D shape with faces).
|
|
862
|
-
* @param distance - Offset distance (positive = outward, negative = inward).
|
|
863
|
-
* @param tolerance - Offset tolerance (default 1e-6).
|
|
864
|
-
*/
|
|
865
|
-
function offset(shape, distance, tolerance = 1e-6) {
|
|
866
|
-
const check = validateNotNull(shape, "offset: shape");
|
|
867
|
-
if (require_errors.isErr(check)) return check;
|
|
868
|
-
if (Math.abs(distance) < 1e-10) return require_errors.err(require_errors.validationError("ZERO_OFFSET", "Offset distance cannot be zero"));
|
|
869
|
-
try {
|
|
870
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
871
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().offsetWithHistory(shape.wrapped, distance, inputFaceHashes, require_constants.HASH_CODE_MAX, tolerance);
|
|
872
|
-
const cast = require_shapeTypes.castResultShape(resultShape);
|
|
873
|
-
if (!require_shapeTypes.isShape3D(cast)) {
|
|
874
|
-
require_shapeTypes.disposeResultShape(cast);
|
|
875
|
-
return require_errors.err(require_errors.kernelError("OFFSET_RESULT_NOT_3D", "Offset result is not a 3D shape"));
|
|
876
|
-
}
|
|
877
|
-
require_solidBuilders.propagateAllMetadata(evolution, [shape], cast);
|
|
878
|
-
return require_errors.ok(cast);
|
|
879
|
-
} catch (e) {
|
|
880
|
-
const raw = e instanceof Error ? e.message : String(e);
|
|
881
|
-
return require_errors.err(require_errors.kernelError("OFFSET_FAILED", `Offset operation failed: ${raw}`, e));
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Validate draft inputs (null shape, scalar angle bounds, faces non-empty).
|
|
886
|
-
* Returns an Err Result on failure, `undefined` on success.
|
|
887
|
-
*/
|
|
888
|
-
function validateDraftInputs(shape, faces, angle) {
|
|
889
|
-
const check = validateNotNull(shape, "draft: shape");
|
|
890
|
-
if (require_errors.isErr(check)) return check;
|
|
891
|
-
if (typeof angle === "number") {
|
|
892
|
-
if (Math.abs(angle) < 1e-10) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.DRAFT_INVALID_ANGLE, "Draft angle cannot be zero", void 0, void 0, "Provide a non-zero angle in degrees"));
|
|
893
|
-
if (Math.abs(angle) >= 90) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
894
|
-
}
|
|
895
|
-
if (faces.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.DRAFT_NO_FACES, "No faces specified for draft", void 0, void 0, "Select at least one face to apply the draft angle to"));
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Apply a draft (taper) to selected faces of a 3D shape.
|
|
899
|
-
*
|
|
900
|
-
* Draft tilts faces by a specified angle relative to a pull direction,
|
|
901
|
-
* pivoting about a neutral plane. This is essential for injection molding
|
|
902
|
-
* and casting workflows where parts must release from a mold.
|
|
903
|
-
*
|
|
904
|
-
* @param shape - The solid to modify.
|
|
905
|
-
* @param faces - Faces to draft.
|
|
906
|
-
* @param pullDirection - Mold opening direction vector.
|
|
907
|
-
* @param neutralPlane - A point on the plane where faces are not displaced.
|
|
908
|
-
* @param angle - Constant angle in degrees, or per-face callback returning degrees (null to skip).
|
|
947
|
+
* Curve-based positioning operations.
|
|
909
948
|
*/
|
|
910
|
-
function draft(shape, faces, pullDirection, neutralPlane, angle) {
|
|
911
|
-
const inputErr = validateDraftInputs(shape, faces, angle);
|
|
912
|
-
if (inputErr) return inputErr;
|
|
913
|
-
try {
|
|
914
|
-
const { filteredFaces, kernelAngle } = resolveDraftCallback(faces, angle);
|
|
915
|
-
if (filteredFaces.length === 0) return require_errors.err(require_errors.validationError(require_errors.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"));
|
|
916
|
-
const inputFaceHashes = require_solidBuilders.collectInputFaceHashes([shape]);
|
|
917
|
-
const { shape: resultShape, evolution } = require_shapeTypes.getKernel().draftWithHistory(shape.wrapped, filteredFaces.map((f) => f.wrapped), [
|
|
918
|
-
pullDirection[0],
|
|
919
|
-
pullDirection[1],
|
|
920
|
-
pullDirection[2]
|
|
921
|
-
], [
|
|
922
|
-
neutralPlane[0],
|
|
923
|
-
neutralPlane[1],
|
|
924
|
-
neutralPlane[2]
|
|
925
|
-
], kernelAngle, inputFaceHashes, require_constants.HASH_CODE_MAX);
|
|
926
|
-
return finalizeShape3D(evolution, resultShape, [shape], require_errors.BrepErrorCode.DRAFT_NOT_3D, "Draft result is not a 3D shape");
|
|
927
|
-
} catch (e) {
|
|
928
|
-
const raw = e instanceof Error ? e.message : String(e);
|
|
929
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.DRAFT_FAILED, `Draft operation failed: ${raw}`, e, {
|
|
930
|
-
operation: "draft",
|
|
931
|
-
faceCount: faces.length,
|
|
932
|
-
angle
|
|
933
|
-
}));
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
949
|
/**
|
|
937
|
-
*
|
|
950
|
+
* Position a shape at a point along a spine curve with Frenet frame orientation.
|
|
938
951
|
*
|
|
939
|
-
* The
|
|
940
|
-
*
|
|
952
|
+
* The shape is translated and rotated so its origin aligns with the curve point
|
|
953
|
+
* and its Z axis aligns with the curve tangent at the given parameter.
|
|
941
954
|
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
955
|
+
* @param shape - The shape to position.
|
|
956
|
+
* @param spine - The spine curve (Edge or Wire) to position along.
|
|
957
|
+
* @param param - Normalized parameter (0 = start, 1 = end).
|
|
958
|
+
* @returns The repositioned shape.
|
|
945
959
|
*/
|
|
946
|
-
function
|
|
947
|
-
if (radii.length === 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "radii must contain at least one radius spec"));
|
|
948
|
-
for (const r of radii) if (r.radius <= 0) return require_errors.err(require_errors.validationError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "All radius values must be positive"));
|
|
949
|
-
const kernel = require_shapeTypes.getKernel();
|
|
960
|
+
function positionOnCurve(shape, spine, param) {
|
|
950
961
|
try {
|
|
951
|
-
const
|
|
952
|
-
edge: kernel.hashCode(edge.wrapped, require_constants.HASH_CODE_MAX),
|
|
953
|
-
radii: radii.map((r) => ({
|
|
954
|
-
param: r.param,
|
|
955
|
-
radius: r.radius
|
|
956
|
-
}))
|
|
957
|
-
});
|
|
958
|
-
const result = kernel.filletVariable(shape.wrapped, spec);
|
|
962
|
+
const result = require_shapeTypes.getKernel().positionOnCurve(shape.wrapped, spine.wrapped, param);
|
|
959
963
|
const wrapped = require_shapeTypes.castResultShape(result);
|
|
960
964
|
if (!require_shapeTypes.isShape3D(wrapped)) {
|
|
961
965
|
require_shapeTypes.disposeResultShape(wrapped);
|
|
962
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.
|
|
963
|
-
}
|
|
964
|
-
if (!require_shapeTypes.isSolid(wrapped)) {
|
|
965
|
-
require_shapeTypes.disposeResultShape(wrapped);
|
|
966
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.VARIABLE_FILLET_FAILED, "Variable-radius fillet did not produce a solid"));
|
|
966
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.POSITION_ON_CURVE_FAILED, "positionOnCurve did not produce a 3D shape"));
|
|
967
967
|
}
|
|
968
968
|
return require_errors.ok(wrapped);
|
|
969
969
|
} catch (e) {
|
|
970
|
-
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.
|
|
970
|
+
return require_errors.err(require_errors.kernelError(require_errors.BrepErrorCode.POSITION_ON_CURVE_FAILED, `Failed to position shape on curve at param ${param}`, e));
|
|
971
971
|
}
|
|
972
972
|
}
|
|
973
973
|
//#endregion
|