fluidcad 0.0.26 → 0.0.28

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.
Files changed (93) hide show
  1. package/lib/dist/common/scene-object.d.ts +45 -0
  2. package/lib/dist/common/scene-object.js +121 -0
  3. package/lib/dist/common/shape-factory.d.ts +1 -1
  4. package/lib/dist/common/shape-history-tracker.d.ts +35 -0
  5. package/lib/dist/common/shape-history-tracker.js +114 -0
  6. package/lib/dist/common/shape.js +7 -1
  7. package/lib/dist/common/shapes.d.ts +0 -1
  8. package/lib/dist/common/shapes.js +0 -1
  9. package/lib/dist/common/solid.js +5 -1
  10. package/lib/dist/core/extrude.d.ts +12 -13
  11. package/lib/dist/core/extrude.js +19 -1
  12. package/lib/dist/core/part.d.ts +2 -1
  13. package/lib/dist/core/part.js +4 -1
  14. package/lib/dist/core/sketch.d.ts +4 -3
  15. package/lib/dist/core/sketch.js +4 -1
  16. package/lib/dist/features/chamfer.js +12 -6
  17. package/lib/dist/features/extrude-base.d.ts +43 -1
  18. package/lib/dist/features/extrude-base.js +141 -36
  19. package/lib/dist/features/extrude-to-face.d.ts +1 -1
  20. package/lib/dist/features/extrude-to-face.js +42 -19
  21. package/lib/dist/features/extrude-two-distances.d.ts +1 -1
  22. package/lib/dist/features/extrude-two-distances.js +41 -15
  23. package/lib/dist/features/extrude.d.ts +1 -1
  24. package/lib/dist/features/extrude.js +75 -20
  25. package/lib/dist/features/fillet.js +3 -4
  26. package/lib/dist/features/fuse.js +14 -0
  27. package/lib/dist/features/infinite-extrude.d.ts +1 -0
  28. package/lib/dist/features/infinite-extrude.js +33 -4
  29. package/lib/dist/features/loft.js +18 -5
  30. package/lib/dist/features/mirror-shape.d.ts +1 -3
  31. package/lib/dist/features/mirror-shape.js +2 -1
  32. package/lib/dist/features/revolve.js +17 -4
  33. package/lib/dist/features/rotate.js +1 -0
  34. package/lib/dist/features/simple-extruder.js +5 -0
  35. package/lib/dist/features/sweep.js +13 -2
  36. package/lib/dist/features/translate.js +3 -1
  37. package/lib/dist/filters/face/face-filter.d.ts +12 -0
  38. package/lib/dist/filters/face/face-filter.js +21 -0
  39. package/lib/dist/filters/face/torus-filter.d.ts +19 -0
  40. package/lib/dist/filters/face/torus-filter.js +38 -0
  41. package/lib/dist/helpers/scene-helpers.d.ts +10 -2
  42. package/lib/dist/helpers/scene-helpers.js +278 -10
  43. package/lib/dist/index.d.ts +1 -0
  44. package/lib/dist/oc/boolean-ops.d.ts +32 -4
  45. package/lib/dist/oc/boolean-ops.js +122 -11
  46. package/lib/dist/oc/color-transfer.d.ts +37 -0
  47. package/lib/dist/oc/color-transfer.js +135 -0
  48. package/lib/dist/oc/extrude-ops.js +25 -3
  49. package/lib/dist/oc/face-ops.d.ts +0 -1
  50. package/lib/dist/oc/face-ops.js +0 -13
  51. package/lib/dist/oc/face-query.d.ts +2 -0
  52. package/lib/dist/oc/face-query.js +30 -0
  53. package/lib/dist/oc/fillet-ops.d.ts +5 -3
  54. package/lib/dist/oc/fillet-ops.js +107 -70
  55. package/lib/dist/oc/intersection.js +6 -3
  56. package/lib/dist/oc/mesh.d.ts +25 -2
  57. package/lib/dist/oc/mesh.js +112 -35
  58. package/lib/dist/oc/shape-ops.d.ts +25 -20
  59. package/lib/dist/oc/shape-ops.js +129 -113
  60. package/lib/dist/rendering/mesh-transform.js +17 -1
  61. package/lib/dist/rendering/render-solid.js +19 -6
  62. package/lib/dist/rendering/render-wire.js +2 -0
  63. package/lib/dist/rendering/render.d.ts +12 -2
  64. package/lib/dist/rendering/render.js +195 -220
  65. package/lib/dist/scene-manager.d.ts +2 -0
  66. package/lib/dist/scene-manager.js +4 -3
  67. package/lib/dist/tests/common/scene-object-history.test.d.ts +1 -0
  68. package/lib/dist/tests/common/scene-object-history.test.js +274 -0
  69. package/lib/dist/tests/common/shape-history-tracker.test.d.ts +1 -0
  70. package/lib/dist/tests/common/shape-history-tracker.test.js +110 -0
  71. package/lib/dist/tests/features/2d/project-regression.test.d.ts +1 -0
  72. package/lib/dist/tests/features/2d/project-regression.test.js +69 -0
  73. package/lib/dist/tests/features/2d/project-user-regression.test.d.ts +1 -0
  74. package/lib/dist/tests/features/2d/project-user-regression.test.js +37 -0
  75. package/lib/dist/tests/features/color-lineage.test.d.ts +1 -0
  76. package/lib/dist/tests/features/color-lineage.test.js +213 -0
  77. package/lib/dist/tests/features/cut-symmetric-through-all.test.d.ts +1 -0
  78. package/lib/dist/tests/features/cut-symmetric-through-all.test.js +32 -0
  79. package/lib/dist/tests/features/extrude-history.test.d.ts +1 -0
  80. package/lib/dist/tests/features/extrude-history.test.js +248 -0
  81. package/lib/dist/tests/features/extrude.test.js +71 -0
  82. package/lib/dist/tests/features/fillet2d.test.js +16 -1
  83. package/lib/dist/tests/features/peer-ops-history.test.d.ts +1 -0
  84. package/lib/dist/tests/features/peer-ops-history.test.js +119 -0
  85. package/lib/dist/tests/features/select.test.js +50 -0
  86. package/lib/dist/tests/features/subtract.test.js +21 -1
  87. package/lib/dist/tests/setup.js +3 -2
  88. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +3 -3
  90. package/ui/dist/assets/{index-BeLxRMCv.js → index-BrW_x4uc.js} +37 -37
  91. package/ui/dist/index.html +1 -1
  92. package/lib/dist/common/solid-face.d.ts +0 -9
  93. package/lib/dist/common/solid-face.js +0 -22
@@ -1,17 +1,25 @@
1
1
  import { SceneObject } from "../common/scene-object.js";
2
2
  import { Shape } from "../common/shapes.js";
3
3
  import { Plane } from "../math/plane.js";
4
- export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[]): {
4
+ import { ShapeHistory } from "../common/shape-history-tracker.js";
5
+ export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[], opts?: {
6
+ glue?: 'full' | 'shift';
7
+ recordHistoryFor?: SceneObject;
8
+ }): {
5
9
  newShapes: Shape<any>[];
6
10
  modifiedShapes: any[];
11
+ toolHistory?: undefined;
7
12
  } | {
8
13
  newShapes: Shape<import("occjs-wrapper").TopoDS_Shape>[];
9
14
  modifiedShapes: {
10
15
  shape: Shape<any>;
11
16
  object: SceneObject;
12
17
  }[];
18
+ toolHistory: ShapeHistory;
13
19
  };
14
- export declare function cutWithSceneObjects(sceneObjects: SceneObject[], toolShapes: Shape[], plane: Plane, distance: number, caller: SceneObject): {
20
+ export declare function cutWithSceneObjects(sceneObjects: SceneObject[], toolShapes: Shape[], plane: Plane, distance: number, caller: SceneObject, options?: {
21
+ recordHistoryFor?: SceneObject;
22
+ }): {
15
23
  cleanedShapes: Shape[];
16
24
  stockShapes: Shape[];
17
25
  };
@@ -1,8 +1,15 @@
1
1
  import { BooleanOps } from "../oc/boolean-ops.js";
2
2
  import { ShapeOps } from "../oc/shape-ops.js";
3
3
  import { classifyCutResult } from "./cut-helpers.js";
4
- export function fuseWithSceneObjects(sceneObjects, extrusions) {
4
+ import { ShapeHistoryTracker } from "../common/shape-history-tracker.js";
5
+ import { Explorer } from "../oc/explorer.js";
6
+ import { Face } from "../common/face.js";
7
+ import { Edge } from "../common/edge.js";
8
+ import { getOC } from "../oc/init.js";
9
+ import { ColorTransfer } from "../oc/color-transfer.js";
10
+ export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
5
11
  const modified = [];
12
+ const tCollect = performance.now();
6
13
  const objShapeMap = new Map();
7
14
  for (const obj of sceneObjects) {
8
15
  const shapes = obj.getShapes({}, 'solid');
@@ -11,14 +18,19 @@ export function fuseWithSceneObjects(sceneObjects, extrusions) {
11
18
  }
12
19
  }
13
20
  let sceneShapes = Array.from(objShapeMap.keys());
14
- console.log("Fusing extrusions with scene objects. Extrusions:", extrusions.length, "Scene object shapes:", sceneShapes.length);
15
- const all = [...sceneShapes, ...extrusions];
16
- const { result, newShapes, modifiedShapes } = BooleanOps.fuse(all);
21
+ console.log(`[perf] fuseWithSceneObjects.collect (scenes=${sceneShapes.length}, extrusions=${extrusions.length}): ${(performance.now() - tCollect).toFixed(1)} ms`);
22
+ const tFuse = performance.now();
23
+ const { result, newShapes, modifiedShapes, maker, dispose } = BooleanOps.fuseStockAndTools(sceneShapes, extrusions, opts);
24
+ console.log(`[perf] fuseWithSceneObjects.BooleanOps.fuseStockAndTools (glue=${opts?.glue ?? 'off'}): ${(performance.now() - tFuse).toFixed(1)} ms`);
17
25
  if (newShapes.length === 0 && modifiedShapes.length === 0) {
18
26
  console.log("No fusions were made.");
27
+ dispose();
28
+ if (opts?.recordHistoryFor) {
29
+ recordShapesAsAdditions(opts.recordHistoryFor, extrusions);
30
+ }
19
31
  return {
20
32
  newShapes: extrusions,
21
- modifiedShapes: []
33
+ modifiedShapes: [],
22
34
  };
23
35
  }
24
36
  for (const shape of modifiedShapes) {
@@ -30,9 +42,93 @@ export function fuseWithSceneObjects(sceneObjects, extrusions) {
30
42
  // stay on their original owners so we must not duplicate them.
31
43
  const unconsumed = sceneShapes.filter(s => !modifiedShapes.includes(s));
32
44
  const shapesToAdd = result.filter(s => !unconsumed.some(u => u.getShape().IsPartner(s.getShape())));
33
- return { newShapes: shapesToAdd, modifiedShapes: modified };
45
+ let toolHistory;
46
+ if (opts?.recordHistoryFor) {
47
+ recordFusionHistory(opts.recordHistoryFor, sceneShapes, objShapeMap, shapesToAdd, maker);
48
+ // Separately track tool-side (extrusion) lineage so callers can remap
49
+ // pre-fusion categorizations (start/end/side/…) onto the post-fusion
50
+ // faces. We don't store these as modifications on any scene object —
51
+ // from the user's POV they are additions on the caller already.
52
+ toolHistory = ShapeHistoryTracker.collect(maker, extrusions);
53
+ }
54
+ dispose();
55
+ return { newShapes: shapesToAdd, modifiedShapes: modified, toolHistory };
56
+ }
57
+ /**
58
+ * Record faces/edges from each shape as additions on `caller`. Used when a
59
+ * fusion was a no-op and the new geometry is added to the scene unchanged.
60
+ */
61
+ function recordShapesAsAdditions(caller, shapes) {
62
+ const oc = getOC();
63
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
64
+ const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
65
+ for (const shape of shapes) {
66
+ for (const raw of Explorer.findShapes(shape.getShape(), FACE)) {
67
+ caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
68
+ }
69
+ for (const raw of Explorer.findShapes(shape.getShape(), EDGE)) {
70
+ caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Record modifications/removals on each scene-object owner, and additions on
76
+ * the caller. Modifications are per-scene-shape so we can correctly attribute
77
+ * each source face/edge back to its owning SceneObject.
78
+ *
79
+ * Additions: any face/edge in a new result shape that isn't already the target
80
+ * of a scene-shape modification. This captures both extrusion-derived faces
81
+ * (which appear in the result via tool-side Modified()) and truly new faces.
82
+ */
83
+ function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker) {
84
+ const oc = getOC();
85
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
86
+ const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
87
+ const claimedFaces = new oc.TopTools_MapOfShape();
88
+ const claimedEdges = new oc.TopTools_MapOfShape();
89
+ for (const sceneShape of sceneShapes) {
90
+ const owner = owners.get(sceneShape);
91
+ if (!owner) {
92
+ continue;
93
+ }
94
+ const history = ShapeHistoryTracker.collect(maker, [sceneShape]);
95
+ for (const record of history.modifiedFaces) {
96
+ owner.recordModifiedFaces(record.sources, record.results, caller);
97
+ for (const r of record.results) {
98
+ claimedFaces.Add(r.getShape());
99
+ }
100
+ }
101
+ for (const record of history.modifiedEdges) {
102
+ owner.recordModifiedEdges(record.sources, record.results, caller);
103
+ for (const r of record.results) {
104
+ claimedEdges.Add(r.getShape());
105
+ }
106
+ }
107
+ for (const face of history.removedFaces) {
108
+ owner.recordRemovedFace(face, caller);
109
+ }
110
+ for (const edge of history.removedEdges) {
111
+ owner.recordRemovedEdge(edge, caller);
112
+ }
113
+ }
114
+ for (const newShape of newShapes) {
115
+ for (const raw of Explorer.findShapes(newShape.getShape(), FACE)) {
116
+ if (!claimedFaces.Contains(raw)) {
117
+ caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
118
+ }
119
+ }
120
+ for (const raw of Explorer.findShapes(newShape.getShape(), EDGE)) {
121
+ if (!claimedEdges.Contains(raw)) {
122
+ caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
123
+ }
124
+ }
125
+ }
126
+ ColorTransfer.applyThroughMaker(sceneShapes, newShapes, maker);
127
+ ColorTransfer.applyBleeding(sceneShapes, newShapes, maker);
128
+ claimedFaces.delete();
129
+ claimedEdges.delete();
34
130
  }
35
- export function cutWithSceneObjects(sceneObjects, toolShapes, plane, distance, caller) {
131
+ export function cutWithSceneObjects(sceneObjects, toolShapes, plane, distance, caller, options) {
36
132
  const sceneObjectMap = new Map();
37
133
  for (const obj of sceneObjects) {
38
134
  const shapes = obj.getShapes({}, 'solid');
@@ -50,18 +146,190 @@ export function cutWithSceneObjects(sceneObjects, toolShapes, plane, distance, c
50
146
  const stock = Array.from(shapeObjectMap.keys());
51
147
  const cutResult = BooleanOps.cutMultiShape(stock, toolShapes, plane, distance);
52
148
  const cleanedShapes = [];
149
+ const cleanups = [];
53
150
  for (const shape of stock) {
54
151
  const list = cutResult.modified(shape);
55
152
  if (list.length) {
56
153
  for (const newShape of list) {
57
- const s = ShapeOps.cleanShape(newShape);
58
- caller.addShape(s);
59
- cleanedShapes.push(s);
154
+ const cleanup = ShapeOps.cleanShapeWithLineage(newShape);
155
+ caller.addShape(cleanup.shape);
156
+ cleanedShapes.push(cleanup.shape);
157
+ cleanups.push(cleanup);
60
158
  }
61
159
  const obj = shapeObjectMap.get(shape);
62
160
  obj.removeShape(shape, caller);
63
161
  }
64
162
  }
163
+ if (options?.recordHistoryFor) {
164
+ recordCutHistory(options.recordHistoryFor, stock, shapeObjectMap, cleanedShapes, cutResult.maker, cleanups);
165
+ }
166
+ for (const cleanup of cleanups) {
167
+ cleanup.dispose();
168
+ }
169
+ cutResult.dispose();
65
170
  classifyCutResult(caller, stock, cleanedShapes, plane, distance);
66
171
  return { cleanedShapes, stockShapes: stock };
67
172
  }
173
+ /**
174
+ * Record per-owner modifications/removals and caller-side additions for a cut.
175
+ * Mirrors the fusion variant but works with `BRepAlgoAPI_Cut`'s `Modified()` /
176
+ * `IsDeleted()` semantics on stock faces and edges. Additions are the faces
177
+ * and edges on the cleaned result shapes that aren't targets of any stock
178
+ * modification — that covers the section faces/edges created by the cut plus
179
+ * any tool-derived geometry that ended up in the result.
180
+ */
181
+ function recordCutHistory(caller, stock, owners, cleanedShapes, maker, cleanups) {
182
+ const oc = getOC();
183
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
184
+ const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
185
+ const claimedFaces = new oc.TopTools_MapOfShape();
186
+ const claimedEdges = new oc.TopTools_MapOfShape();
187
+ // Remap pre-clean faces through the UnifySameDomain history of whichever
188
+ // cleanup handled them. Returns flattened post-clean faces.
189
+ const remapPreCleanFaces = (preCleanFaces) => {
190
+ const out = [];
191
+ for (const face of preCleanFaces) {
192
+ let matched = false;
193
+ for (const cleanup of cleanups) {
194
+ const remapped = cleanup.remapFace(face);
195
+ if (remapped !== null) {
196
+ out.push(...remapped);
197
+ matched = true;
198
+ break;
199
+ }
200
+ }
201
+ if (!matched) {
202
+ // No cleanup claimed this face — it's somehow outside the cleaned
203
+ // solids. Fall through as-is.
204
+ out.push(face);
205
+ }
206
+ }
207
+ return out;
208
+ };
209
+ const remapPreCleanEdges = (preCleanEdges) => {
210
+ const out = [];
211
+ for (const edge of preCleanEdges) {
212
+ let matched = false;
213
+ for (const cleanup of cleanups) {
214
+ const remapped = cleanup.remapEdge(edge);
215
+ if (remapped !== null) {
216
+ out.push(...remapped);
217
+ matched = true;
218
+ break;
219
+ }
220
+ }
221
+ if (!matched) {
222
+ out.push(edge);
223
+ }
224
+ }
225
+ return out;
226
+ };
227
+ for (const stockShape of stock) {
228
+ const owner = owners.get(stockShape);
229
+ if (!owner) {
230
+ continue;
231
+ }
232
+ const history = ShapeHistoryTracker.collect(maker, [stockShape]);
233
+ for (const record of history.modifiedFaces) {
234
+ const postCleanResults = remapPreCleanFaces(record.results);
235
+ if (postCleanResults.length === 0) {
236
+ // Entire modification was removed by UnifySameDomain — record as removal instead.
237
+ for (const src of record.sources) {
238
+ owner.recordRemovedFace(src, caller);
239
+ }
240
+ continue;
241
+ }
242
+ owner.recordModifiedFaces(record.sources, postCleanResults, caller);
243
+ for (const r of postCleanResults) {
244
+ claimedFaces.Add(r.getShape());
245
+ }
246
+ }
247
+ for (const record of history.modifiedEdges) {
248
+ const postCleanResults = remapPreCleanEdges(record.results);
249
+ if (postCleanResults.length === 0) {
250
+ for (const src of record.sources) {
251
+ owner.recordRemovedEdge(src, caller);
252
+ }
253
+ continue;
254
+ }
255
+ owner.recordModifiedEdges(record.sources, postCleanResults, caller);
256
+ for (const r of postCleanResults) {
257
+ claimedEdges.Add(r.getShape());
258
+ }
259
+ }
260
+ for (const face of history.removedFaces) {
261
+ owner.recordRemovedFace(face, caller);
262
+ }
263
+ for (const edge of history.removedEdges) {
264
+ owner.recordRemovedEdge(edge, caller);
265
+ }
266
+ }
267
+ for (const cleaned of cleanedShapes) {
268
+ for (const raw of Explorer.findShapes(cleaned.getShape(), FACE)) {
269
+ if (!claimedFaces.Contains(raw)) {
270
+ caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
271
+ }
272
+ }
273
+ for (const raw of Explorer.findShapes(cleaned.getShape(), EDGE)) {
274
+ if (!claimedEdges.Contains(raw)) {
275
+ caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
276
+ }
277
+ }
278
+ }
279
+ propagateFaceColorsViaCut(stock, cleanedShapes, maker, cleanups);
280
+ claimedFaces.delete();
281
+ claimedEdges.delete();
282
+ }
283
+ /**
284
+ * Cut-path color propagation. Chains through UnifySameDomain's history via
285
+ * the `cleanups[]` remapFace callbacks so colors land on the actual
286
+ * post-clean faces in `cleanedShapes`.
287
+ */
288
+ function propagateFaceColorsViaCut(stock, cleanedShapes, maker, cleanups) {
289
+ const oc = getOC();
290
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
291
+ for (const stockShape of stock) {
292
+ if (!stockShape.hasColors()) {
293
+ continue;
294
+ }
295
+ for (const entry of stockShape.colorMap) {
296
+ const modifiedRaws = ShapeOps.shapeListToArray(maker.Modified(entry.shape))
297
+ .filter(s => s.ShapeType() === FACE);
298
+ let preCleanFaces;
299
+ if (modifiedRaws.length > 0) {
300
+ preCleanFaces = modifiedRaws.map(r => Face.fromTopoDSFace(Explorer.toFace(r)));
301
+ }
302
+ else if (!maker.IsDeleted(entry.shape)) {
303
+ preCleanFaces = [Face.fromTopoDSFace(Explorer.toFace(entry.shape))];
304
+ }
305
+ else {
306
+ continue;
307
+ }
308
+ // Chain through each cleanup's UnifySameDomain lineage
309
+ const postCleanFaces = [];
310
+ for (const preFace of preCleanFaces) {
311
+ let matched = false;
312
+ for (const cleanup of cleanups) {
313
+ const remapped = cleanup.remapFace(preFace);
314
+ if (remapped !== null) {
315
+ postCleanFaces.push(...remapped);
316
+ matched = true;
317
+ break;
318
+ }
319
+ }
320
+ if (!matched) {
321
+ postCleanFaces.push(preFace);
322
+ }
323
+ }
324
+ for (const postFace of postCleanFaces) {
325
+ for (const cleaned of cleanedShapes) {
326
+ const faces = Explorer.findShapes(cleaned.getShape(), FACE);
327
+ if (faces.some(f => f.IsSame(postFace.getShape()))) {
328
+ cleaned.setColor(postFace.getShape(), entry.color);
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
@@ -20,6 +20,7 @@ export declare function registerBuilder<T extends Function>(builder: (context: S
20
20
  export declare function init(rootPath?: string): Promise<{
21
21
  currentScene: Scene;
22
22
  currentFile: string;
23
+ renderer: import("./rendering/render.js").SceneRenderer;
23
24
  rootPath: string;
24
25
  setCurrentFile(filePath: string): void;
25
26
  startScene(): Scene;
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Shape } from "occjs-wrapper";
1
+ import type { BRepAlgoAPI_Cut, TopoDS_Shape } from "occjs-wrapper";
2
2
  import { Shape } from "../common/shape.js";
3
3
  import { Solid } from "../common/solid.js";
4
4
  import { Edge } from "../common/edge.js";
@@ -8,18 +8,46 @@ export declare class BooleanOps {
8
8
  static cutShapes(shape: Shape, tool: Shape): Shape;
9
9
  static cutShapesRaw(shape: TopoDS_Shape, tool: TopoDS_Shape): TopoDS_Shape;
10
10
  static cutMultiShape(stocks: Shape[], tools: Shape[], plane?: Plane, cutDistance?: number): {
11
- result: import("../common/wire.js").Wire | Edge | Solid | Face;
12
- modified: (shape: Shape) => (import("../common/wire.js").Wire | Edge | Solid | Face)[];
11
+ result: Solid | Face | Edge | import("../common/wire.js").Wire;
12
+ modified: (shape: Shape) => (Solid | Face | Edge | import("../common/wire.js").Wire)[];
13
13
  sectionEdges: Edge[];
14
14
  startEdges: Edge[];
15
15
  endEdges: Edge[];
16
16
  internalEdges: Edge[];
17
17
  internalFaces: Face[];
18
+ maker: BRepAlgoAPI_Cut;
19
+ dispose: () => void;
18
20
  };
19
- static fuse(args: Shape[]): {
21
+ /**
22
+ * Fuse with proper OpenCascade argument-vs-tool separation. Use this for
23
+ * operations that fuse newly built geometry into an existing scene (extrude,
24
+ * revolve, sweep, loft) where we need `maker.Modified(stockFace)` lineage to
25
+ * track which existing face became which result face.
26
+ *
27
+ * Returns the underlying `BRepAlgoAPI_Fuse` so the caller can query
28
+ * `Modified()` / `Generated()` / `IsDeleted()` for history tracking. The
29
+ * caller MUST invoke `dispose()` exactly once to release the maker.
30
+ *
31
+ * Do not use this for the user-facing `Fuse` scene object (keep
32
+ * `BooleanOps.fuse` for that — it treats all inputs as symmetric peers).
33
+ */
34
+ static fuseStockAndTools(stock: Shape[], tools: Shape[], opts?: {
35
+ glue?: 'full' | 'shift';
36
+ }): {
20
37
  result: Shape[];
21
38
  modifiedShapes: Shape[];
22
39
  newShapes: Shape[];
40
+ maker: any;
41
+ dispose: () => void;
42
+ };
43
+ static fuse(args: Shape[], opts?: {
44
+ glue?: 'full' | 'shift';
45
+ }): {
46
+ result: Shape[];
47
+ modifiedShapes: Shape[];
48
+ newShapes: Shape[];
49
+ maker: any;
50
+ dispose: () => void;
23
51
  };
24
52
  static fuseFaces(args: Shape[]): {
25
53
  result: Shape[];
@@ -14,12 +14,19 @@ export class BooleanOps {
14
14
  static cutShapesRaw(shape, tool) {
15
15
  const oc = getOC();
16
16
  const progress = new oc.Message_ProgressRange();
17
- const cutter = new oc.BRepAlgoAPI_Cut(shape, tool, progress);
18
- cutter.Build(progress);
19
- if (!cutter.IsDone()) {
17
+ let cutter;
18
+ try {
19
+ cutter = new oc.BRepAlgoAPI_Cut(shape, tool, progress);
20
+ cutter.Build(progress);
21
+ }
22
+ catch {
23
+ progress.delete();
24
+ throw new Error("Cut failed");
25
+ }
26
+ if (!cutter.IsDone() || cutter.HasErrors()) {
20
27
  cutter.delete();
21
28
  progress.delete();
22
- throw new Error("Cut operation failed");
29
+ throw new Error("Cut failed");
23
30
  }
24
31
  const result = cutter.Shape();
25
32
  cutter.delete();
@@ -113,17 +120,107 @@ export class BooleanOps {
113
120
  .map(rf => Face.fromTopoDSFace(Explorer.toFace(rf)));
114
121
  stockEdgeMap.delete();
115
122
  stockFaceMap.delete();
116
- progress.delete();
117
123
  stockList.delete();
118
124
  toolList.delete();
119
- return { result: wrappedResult, modified, sectionEdges, startEdges, endEdges, internalEdges, internalFaces };
125
+ let disposed = false;
126
+ const dispose = () => {
127
+ if (disposed) {
128
+ return;
129
+ }
130
+ disposed = true;
131
+ cutMaker.delete();
132
+ progress.delete();
133
+ };
134
+ return {
135
+ result: wrappedResult,
136
+ modified,
137
+ sectionEdges,
138
+ startEdges,
139
+ endEdges,
140
+ internalEdges,
141
+ internalFaces,
142
+ maker: cutMaker,
143
+ dispose,
144
+ };
120
145
  }
121
- static fuse(args) {
146
+ /**
147
+ * Fuse with proper OpenCascade argument-vs-tool separation. Use this for
148
+ * operations that fuse newly built geometry into an existing scene (extrude,
149
+ * revolve, sweep, loft) where we need `maker.Modified(stockFace)` lineage to
150
+ * track which existing face became which result face.
151
+ *
152
+ * Returns the underlying `BRepAlgoAPI_Fuse` so the caller can query
153
+ * `Modified()` / `Generated()` / `IsDeleted()` for history tracking. The
154
+ * caller MUST invoke `dispose()` exactly once to release the maker.
155
+ *
156
+ * Do not use this for the user-facing `Fuse` scene object (keep
157
+ * `BooleanOps.fuse` for that — it treats all inputs as symmetric peers).
158
+ */
159
+ static fuseStockAndTools(stock, tools, opts) {
122
160
  const oc = getOC();
123
161
  const builder = new oc.BRepAlgoAPI_Fuse();
124
162
  builder.SetNonDestructive(true);
125
163
  builder.SetCheckInverted(true);
126
164
  builder.SetRunParallel(true);
165
+ if (opts?.glue === 'full') {
166
+ builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueFull);
167
+ }
168
+ else if (opts?.glue === 'shift') {
169
+ builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueShift);
170
+ }
171
+ const stockList = new oc.TopTools_ListOfShape();
172
+ for (const s of stock) {
173
+ stockList.Append(s.getShape());
174
+ }
175
+ const toolList = new oc.TopTools_ListOfShape();
176
+ for (const t of tools) {
177
+ toolList.Append(t.getShape());
178
+ }
179
+ builder.SetArguments(stockList);
180
+ builder.SetTools(toolList);
181
+ const progress = new oc.Message_ProgressRange();
182
+ builder.Build(progress);
183
+ builder.SimplifyResult(false, true, oc.Precision.Angular());
184
+ const resultShape = builder.Shape();
185
+ const rawShapes = Explorer.findAllShapes(resultShape);
186
+ const result = rawShapes.map(s => ShapeFactory.fromShape(s));
187
+ const allInputs = [...stock, ...tools];
188
+ const modifiedShapes = [];
189
+ for (const shape of allInputs) {
190
+ if (builder.IsDeleted(shape.getShape())) {
191
+ modifiedShapes.push(shape);
192
+ }
193
+ }
194
+ const newShapes = [];
195
+ for (const s of result) {
196
+ const existsInArgs = allInputs.some(arg => arg.getShape().IsPartner(s.getShape()));
197
+ if (!existsInArgs) {
198
+ newShapes.push(s);
199
+ }
200
+ }
201
+ let disposed = false;
202
+ const dispose = () => {
203
+ if (disposed) {
204
+ return;
205
+ }
206
+ disposed = true;
207
+ builder.delete();
208
+ progress.delete();
209
+ };
210
+ return { result, newShapes, modifiedShapes, maker: builder, dispose };
211
+ }
212
+ static fuse(args, opts) {
213
+ const oc = getOC();
214
+ const builder = new oc.BRepAlgoAPI_Fuse();
215
+ builder.SetNonDestructive(true);
216
+ builder.SetCheckInverted(true);
217
+ builder.SetRunParallel(true);
218
+ if (opts?.glue === 'full') {
219
+ builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueFull);
220
+ }
221
+ else if (opts?.glue === 'shift') {
222
+ builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueShift);
223
+ }
127
224
  const argsList = new oc.TopTools_ListOfShape();
128
225
  for (const arg of args) {
129
226
  argsList.Append(arg.getShape());
@@ -134,11 +231,16 @@ export class BooleanOps {
134
231
  builder.SetArguments(list);
135
232
  builder.SetTools(argsList);
136
233
  const progress = new oc.Message_ProgressRange();
234
+ const tBuild = performance.now();
137
235
  builder.Build(progress);
236
+ console.log(`[perf] BooleanOps.fuse.Build (args=${args.length}): ${(performance.now() - tBuild).toFixed(1)} ms`);
237
+ const tSimplify = performance.now();
138
238
  builder.SimplifyResult(false, true, oc.Precision.Angular());
239
+ console.log(`[perf] BooleanOps.fuse.SimplifyResult: ${(performance.now() - tSimplify).toFixed(1)} ms`);
139
240
  const resultShape = builder.Shape();
241
+ const tExplore = performance.now();
140
242
  const rawShapes = Explorer.findAllShapes(resultShape);
141
- console.log('FuseMultiShape: Result shapes count:', rawShapes.length);
243
+ console.log(`[perf] BooleanOps.fuse.findAllShapes (count=${rawShapes.length}): ${(performance.now() - tExplore).toFixed(1)} ms`);
142
244
  const result = rawShapes.map(s => ShapeFactory.fromShape(s));
143
245
  const modifiedShapes = [];
144
246
  for (const shape of args) {
@@ -146,16 +248,25 @@ export class BooleanOps {
146
248
  modifiedShapes.push(shape);
147
249
  }
148
250
  }
149
- builder.delete();
150
- progress.delete();
151
251
  const newShapes = [];
252
+ const tPartner = performance.now();
152
253
  for (const s of result) {
153
254
  const existsInArgs = args.some(arg => arg.getShape().IsPartner(s.getShape()));
154
255
  if (!existsInArgs) {
155
256
  newShapes.push(s);
156
257
  }
157
258
  }
158
- return { result, newShapes, modifiedShapes };
259
+ console.log(`[perf] BooleanOps.fuse.IsPartner check (result=${result.length} x args=${args.length}): ${(performance.now() - tPartner).toFixed(1)} ms`);
260
+ let disposed = false;
261
+ const dispose = () => {
262
+ if (disposed) {
263
+ return;
264
+ }
265
+ disposed = true;
266
+ builder.delete();
267
+ progress.delete();
268
+ };
269
+ return { result, newShapes, modifiedShapes, maker: builder, dispose };
159
270
  }
160
271
  static fuseFaces(args) {
161
272
  const oc = getOC();
@@ -0,0 +1,37 @@
1
+ import type { BRepBuilderAPI_MakeShape } from "occjs-wrapper";
2
+ import { Shape } from "../common/shape.js";
3
+ import type { CleanShapeLineage } from "./shape-ops.js";
4
+ /**
5
+ * Walk each source shape's `colorMap`, find where each colored face ended up in
6
+ * the result shapes via `maker.Modified()` (falling back to the unchanged face
7
+ * if `!IsDeleted`), and apply the color to whichever result shape now owns it.
8
+ *
9
+ * Works for any `BRepBuilderAPI_MakeShape`-derived maker — fuse, cut, fillet,
10
+ * chamfer, transform, etc.
11
+ */
12
+ export declare class ColorTransfer {
13
+ static applyThroughMaker(sources: Shape[], results: Shape[], maker: BRepBuilderAPI_MakeShape): void;
14
+ /**
15
+ * Color bleed pass: spreads colors to result faces that came from new
16
+ * geometry (tool inputs, generated faces, or just brand-new) by walking
17
+ * face-edge adjacency in each result solid.
18
+ *
19
+ * Faces that came from `sceneSources` (whether modified or unchanged) are
20
+ * NOT bled — those represent existing geometry whose color state the user
21
+ * explicitly chose. Faces NOT from any sceneSource are eligible: this
22
+ * covers tool extrusions, fillet/chamfer-generated surfaces, and cut
23
+ * section faces.
24
+ *
25
+ * Iterates until stable so newly-bled faces can spread color further.
26
+ * Call AFTER `applyThroughMaker` so the colored seeds are in place.
27
+ */
28
+ static applyBleeding(sceneSources: Shape[], results: Shape[], maker: BRepBuilderAPI_MakeShape): void;
29
+ /**
30
+ * Transfer colors from a pre-clean source shape through a `cleanShapeWithLineage`
31
+ * cleanup's `BRepTools_History` onto the post-clean result. Use this when an
32
+ * op is chained as `maker → cleanShape` — first apply `applyThroughMaker` to
33
+ * move colors from the original source onto the pre-clean result, then call
34
+ * this to chain them through the cleanup's UnifySameDomain history.
35
+ */
36
+ static applyThroughCleanup(source: Shape, cleanup: CleanShapeLineage): void;
37
+ }