fluidcad 0.0.27 → 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 (54) 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/solid.js +5 -1
  8. package/lib/dist/features/chamfer.js +12 -6
  9. package/lib/dist/features/extrude-base.d.ts +36 -0
  10. package/lib/dist/features/extrude-base.js +105 -33
  11. package/lib/dist/features/extrude-to-face.js +13 -2
  12. package/lib/dist/features/extrude-two-distances.js +18 -3
  13. package/lib/dist/features/extrude.js +29 -6
  14. package/lib/dist/features/fillet.js +3 -4
  15. package/lib/dist/features/fuse.js +14 -0
  16. package/lib/dist/features/infinite-extrude.d.ts +1 -0
  17. package/lib/dist/features/infinite-extrude.js +33 -4
  18. package/lib/dist/features/loft.js +18 -5
  19. package/lib/dist/features/revolve.js +13 -2
  20. package/lib/dist/features/sweep.js +13 -2
  21. package/lib/dist/helpers/scene-helpers.d.ts +7 -1
  22. package/lib/dist/helpers/scene-helpers.js +274 -9
  23. package/lib/dist/oc/boolean-ops.d.ts +29 -3
  24. package/lib/dist/oc/boolean-ops.js +107 -9
  25. package/lib/dist/oc/color-transfer.d.ts +37 -0
  26. package/lib/dist/oc/color-transfer.js +135 -0
  27. package/lib/dist/oc/extrude-ops.js +25 -3
  28. package/lib/dist/oc/fillet-ops.d.ts +5 -3
  29. package/lib/dist/oc/fillet-ops.js +23 -4
  30. package/lib/dist/oc/intersection.js +6 -3
  31. package/lib/dist/oc/mesh.js +1 -1
  32. package/lib/dist/oc/shape-ops.d.ts +25 -0
  33. package/lib/dist/oc/shape-ops.js +131 -12
  34. package/lib/dist/tests/common/scene-object-history.test.d.ts +1 -0
  35. package/lib/dist/tests/common/scene-object-history.test.js +274 -0
  36. package/lib/dist/tests/common/shape-history-tracker.test.d.ts +1 -0
  37. package/lib/dist/tests/common/shape-history-tracker.test.js +110 -0
  38. package/lib/dist/tests/features/2d/project-regression.test.d.ts +1 -0
  39. package/lib/dist/tests/features/2d/project-regression.test.js +69 -0
  40. package/lib/dist/tests/features/2d/project-user-regression.test.d.ts +1 -0
  41. package/lib/dist/tests/features/2d/project-user-regression.test.js +37 -0
  42. package/lib/dist/tests/features/color-lineage.test.d.ts +1 -0
  43. package/lib/dist/tests/features/color-lineage.test.js +213 -0
  44. package/lib/dist/tests/features/cut-symmetric-through-all.test.d.ts +1 -0
  45. package/lib/dist/tests/features/cut-symmetric-through-all.test.js +32 -0
  46. package/lib/dist/tests/features/extrude-history.test.d.ts +1 -0
  47. package/lib/dist/tests/features/extrude-history.test.js +248 -0
  48. package/lib/dist/tests/features/peer-ops-history.test.d.ts +1 -0
  49. package/lib/dist/tests/features/peer-ops-history.test.js +119 -0
  50. package/lib/dist/tests/features/subtract.test.js +21 -1
  51. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +3 -3
  53. package/ui/dist/assets/{index-55iqIwnj.js → index-BrW_x4uc.js} +1 -1
  54. package/ui/dist/index.html +1 -1
@@ -1,4 +1,5 @@
1
1
  import { Face } from "../common/face.js";
2
+ import { Edge } from "../common/edge.js";
2
3
  import { SceneObject } from "../common/scene-object.js";
3
4
  import { LazySelectionSceneObject } from "./lazy-scene-object.js";
4
5
  import { normalizePoint2D } from "../helpers/normalize.js";
@@ -8,6 +9,18 @@ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
8
9
  import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
9
10
  import { ShapeFilter } from "../filters/filter.js";
10
11
  import { EdgeOps } from "../oc/edge-ops.js";
12
+ import { Explorer } from "../oc/explorer.js";
13
+ import { getOC } from "../oc/init.js";
14
+ import { ShapeHistoryTracker } from "../common/shape-history-tracker.js";
15
+ function dedupEdgesByIsSame(edges) {
16
+ const result = [];
17
+ for (const edge of edges) {
18
+ if (!result.some(r => r.getShape().IsSame(edge.getShape()))) {
19
+ result.push(edge);
20
+ }
21
+ }
22
+ return result;
23
+ }
11
24
  export class ExtrudeBase extends SceneObject {
12
25
  _extrudable = null;
13
26
  _faceSource = null;
@@ -80,19 +93,10 @@ export class ExtrudeBase extends SceneObject {
80
93
  startEdges(...args) {
81
94
  const suffix = this.buildSuffix('start-edges', args);
82
95
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
83
- if (this._operationMode === 'remove') {
84
- const edges = parent.getState('start-edges') || [];
85
- const transform = parent.getTransform();
86
- const originalEdges = transform
87
- ? (this.getState('start-edges') || [])
88
- : null;
89
- return this.resolveEdges(edges, args, transform, originalEdges);
90
- }
91
- const faces = parent.getState('start-faces') || [];
92
- const edges = faces.flatMap(f => f.getEdges());
96
+ const edges = this.getClassifiedEdges(parent, 'start-edges', 'start-faces');
93
97
  const transform = parent.getTransform();
94
98
  const originalEdges = transform
95
- ? (this.getState('start-faces') || []).flatMap(f => f.getEdges())
99
+ ? this.getClassifiedEdges(this, 'start-edges', 'start-faces')
96
100
  : null;
97
101
  return this.resolveEdges(edges, args, transform, originalEdges);
98
102
  }, this);
@@ -100,19 +104,10 @@ export class ExtrudeBase extends SceneObject {
100
104
  endEdges(...args) {
101
105
  const suffix = this.buildSuffix('end-edges', args);
102
106
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
103
- if (this._operationMode === 'remove') {
104
- const edges = parent.getState('end-edges') || [];
105
- const transform = parent.getTransform();
106
- const originalEdges = transform
107
- ? (this.getState('end-edges') || [])
108
- : null;
109
- return this.resolveEdges(edges, args, transform, originalEdges);
110
- }
111
- const faces = parent.getState('end-faces') || [];
112
- const edges = faces.flatMap(f => f.getEdges());
107
+ const edges = this.getClassifiedEdges(parent, 'end-edges', 'end-faces');
113
108
  const transform = parent.getTransform();
114
109
  const originalEdges = transform
115
- ? (this.getState('end-faces') || []).flatMap(f => f.getEdges())
110
+ ? this.getClassifiedEdges(this, 'end-edges', 'end-faces')
116
111
  : null;
117
112
  return this.resolveEdges(edges, args, transform, originalEdges);
118
113
  }, this);
@@ -131,13 +126,17 @@ export class ExtrudeBase extends SceneObject {
131
126
  sideEdges(...args) {
132
127
  const suffix = this.buildSuffix('side-edges', args);
133
128
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
129
+ const classified = parent.getState('side-edges');
130
+ if (classified !== undefined) {
131
+ return this.resolveEdges(classified, args);
132
+ }
133
+ // Fallback for peer ops that haven't called classifyExtrudeEdges: derive on the fly.
134
134
  const sideFaces = parent.getState('side-faces') || [];
135
135
  const startFaces = parent.getState('start-faces') || [];
136
136
  const endFaces = parent.getState('end-faces') || [];
137
137
  const excludedEdges = [...startFaces, ...endFaces].flatMap(f => f.getEdges());
138
- const edges = sideFaces.flatMap(f => f.getEdges())
139
- .filter(e => !excludedEdges.some(ex => e.getShape().IsSame(ex.getShape())))
140
- .filter((e, i, arr) => arr.findIndex(o => o.getShape().IsSame(e.getShape())) === i);
138
+ const edges = dedupEdgesByIsSame(sideFaces.flatMap(f => f.getEdges())
139
+ .filter(e => !excludedEdges.some(ex => e.getShape().IsSame(ex.getShape()))));
141
140
  return this.resolveEdges(edges, args);
142
141
  }, this);
143
142
  }
@@ -169,12 +168,7 @@ export class ExtrudeBase extends SceneObject {
169
168
  internalEdges(...args) {
170
169
  const suffix = this.buildSuffix('internal-edges', args);
171
170
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
172
- if (this._operationMode === 'remove') {
173
- const edges = parent.getState('internal-edges') || [];
174
- return this.resolveEdges(edges, args);
175
- }
176
- const faces = parent.getState('internal-faces') || [];
177
- const edges = faces.flatMap(f => f.getEdges());
171
+ const edges = this.getClassifiedEdges(parent, 'internal-edges', 'internal-faces');
178
172
  return this.resolveEdges(edges, args);
179
173
  }, this);
180
174
  }
@@ -192,11 +186,89 @@ export class ExtrudeBase extends SceneObject {
192
186
  capEdges(...args) {
193
187
  const suffix = this.buildSuffix('cap-edges', args);
194
188
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
195
- const faces = parent.getState('cap-faces') || [];
196
- const edges = faces.flatMap(f => f.getEdges());
189
+ const edges = this.getClassifiedEdges(parent, 'cap-edges', 'cap-faces');
197
190
  return this.resolveEdges(edges, args);
198
191
  }, this);
199
192
  }
193
+ /**
194
+ * Read edges for a classification category, preferring the pre-computed
195
+ * state key (set by `classifyExtrudeEdges` during build) and falling back
196
+ * to deriving from the corresponding face-category state (for peer ops that
197
+ * haven't opted into the unified classification step yet).
198
+ */
199
+ getClassifiedEdges(source, edgeKey, faceKey) {
200
+ const classified = source.getState(edgeKey);
201
+ if (classified !== undefined) {
202
+ return classified;
203
+ }
204
+ const faces = source.getState(faceKey) || [];
205
+ return dedupEdgesByIsSame(faces.flatMap(f => f.getEdges()));
206
+ }
207
+ /**
208
+ * Remap the state-stored face category arrays through a fusion's tool-side
209
+ * history so each face reference points at the actual post-fusion face in
210
+ * the final solid. Call after `fuseWithSceneObjects` returns a `toolHistory`.
211
+ */
212
+ remapClassifiedFaces(history) {
213
+ const keys = ['start-faces', 'end-faces', 'side-faces', 'internal-faces', 'cap-faces'];
214
+ for (const key of keys) {
215
+ const faces = this.getState(key);
216
+ if (faces && faces.length > 0) {
217
+ this.setState(key, ShapeHistoryTracker.remapFaces(faces, history));
218
+ }
219
+ }
220
+ }
221
+ /**
222
+ * Record every face/edge of the given shapes as additions on this operation.
223
+ * Used by 3D ops in the "no scene fusion" path — when the tools land
224
+ * unchanged in the scene, every face/edge is brand new from this op's POV.
225
+ */
226
+ recordShapeFacesAndEdgesAsAdditions(shapes) {
227
+ const oc = getOC();
228
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
229
+ const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
230
+ for (const shape of shapes) {
231
+ for (const raw of Explorer.findShapes(shape.getShape(), FACE)) {
232
+ this.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), this);
233
+ }
234
+ for (const raw of Explorer.findShapes(shape.getShape(), EDGE)) {
235
+ this.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), this);
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * One-shot edge classification: derive start/end/side/internal/cap edges
241
+ * from the already-classified face arrays in state and store them as
242
+ * `start-edges`, `end-edges`, `side-edges`, `internal-edges`, `cap-edges`.
243
+ *
244
+ * Call this once after face classification (and after any post-fusion
245
+ * face remapping) so that the selection accessors can just read the
246
+ * pre-computed arrays instead of re-deriving on every access. Matches
247
+ * the classification step from the spec: "Classify the new edges and
248
+ * faces created by the operation".
249
+ *
250
+ * Side edges are the edges of side faces minus any edge that's also on
251
+ * a start/end face (those already belong to start-edges / end-edges).
252
+ */
253
+ classifyExtrudeEdges() {
254
+ const startFaces = this.getState('start-faces') || [];
255
+ const endFaces = this.getState('end-faces') || [];
256
+ const sideFaces = this.getState('side-faces') || [];
257
+ const internalFaces = this.getState('internal-faces') || [];
258
+ const capFaces = this.getState('cap-faces') || [];
259
+ const startEdges = dedupEdgesByIsSame(startFaces.flatMap(f => f.getEdges()));
260
+ const endEdges = dedupEdgesByIsSame(endFaces.flatMap(f => f.getEdges()));
261
+ const excludedForSide = [...startEdges, ...endEdges];
262
+ const sideEdges = dedupEdgesByIsSame(sideFaces.flatMap(f => f.getEdges())
263
+ .filter(e => !excludedForSide.some(ex => ex.getShape().IsSame(e.getShape()))));
264
+ const internalEdges = dedupEdgesByIsSame(internalFaces.flatMap(f => f.getEdges()));
265
+ const capEdges = dedupEdgesByIsSame(capFaces.flatMap(f => f.getEdges()));
266
+ this.setState('start-edges', startEdges);
267
+ this.setState('end-edges', endEdges);
268
+ this.setState('side-edges', sideEdges);
269
+ this.setState('internal-edges', internalEdges);
270
+ this.setState('cap-edges', capEdges);
271
+ }
200
272
  buildSuffix(prefix, args) {
201
273
  if (args.length === 0) {
202
274
  return prefix;
@@ -91,20 +91,31 @@ export class ExtrudeToFace extends ExtrudeBase {
91
91
  }
92
92
  if (this._operationMode === 'remove') {
93
93
  const scope = this.resolveFusionScope(allSceneObjects);
94
- cutWithSceneObjects(scope, solids, plane, 0, this);
94
+ cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
95
+ this.setFinalShapes(this.getShapes());
95
96
  return;
96
97
  }
97
98
  if (sceneObjects.length === 0) {
98
99
  this.addShapes(solids);
100
+ this.recordShapeFacesAndEdgesAsAdditions(solids);
101
+ this.classifyExtrudeEdges();
102
+ this.setFinalShapes(this.getShapes());
99
103
  return;
100
104
  }
101
- const fusionResult = fuseWithSceneObjects(sceneObjects, solids);
105
+ const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
106
+ recordHistoryFor: this,
107
+ });
102
108
  for (const modifiedShape of fusionResult.modifiedShapes) {
103
109
  if (modifiedShape.object) {
104
110
  modifiedShape.object.removeShape(modifiedShape.shape, this);
105
111
  }
106
112
  }
107
113
  this.addShapes(fusionResult.newShapes);
114
+ if (fusionResult.toolHistory) {
115
+ this.remapClassifiedFaces(fusionResult.toolHistory);
116
+ }
117
+ this.classifyExtrudeEdges();
118
+ this.setFinalShapes(this.getShapes());
108
119
  }
109
120
  createAdvancedExtrude(sourceFace, targetFace, isPlanar, sketchPlane) {
110
121
  const targetDistance = this.computeSignedDistanceToFace(targetFace, sketchPlane);
@@ -49,7 +49,9 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
49
49
  const extrusions2 = extruder2.extrude();
50
50
  const endFaces = extruder2.getEndFaces();
51
51
  const all = [...extrusions1, ...extrusions2];
52
- const { result: extrusions } = BooleanOps.fuse(all);
52
+ const halvesFuse = BooleanOps.fuse(all);
53
+ const extrusions = halvesFuse.result;
54
+ halvesFuse.dispose();
53
55
  const remainingFaces = [];
54
56
  const fusedStartFaces = [];
55
57
  const fusedEndFaces = [];
@@ -121,14 +123,22 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
121
123
  this.getSource()?.removeShapes(this);
122
124
  if (this._operationMode === 'remove') {
123
125
  const scope = this.resolveFusionScope(context.getSceneObjects());
124
- cutWithSceneObjects(scope, extrusions, plane, this.distance1 + this.distance2, this);
126
+ cutWithSceneObjects(scope, extrusions, plane, this.distance1 + this.distance2, this, {
127
+ recordHistoryFor: this,
128
+ });
129
+ this.setFinalShapes(this.getShapes());
125
130
  return;
126
131
  }
127
132
  if (extrusions.length === 0 || sceneObjects.length === 0) {
128
133
  this.addShapes(extrusions);
134
+ this.recordShapeFacesAndEdgesAsAdditions(extrusions);
135
+ this.classifyExtrudeEdges();
136
+ this.setFinalShapes(this.getShapes());
129
137
  return;
130
138
  }
131
- const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions);
139
+ const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
140
+ recordHistoryFor: this,
141
+ });
132
142
  for (const modifiedShape of fusionResult.modifiedShapes) {
133
143
  if (!modifiedShape.object) {
134
144
  continue;
@@ -136,6 +146,11 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
136
146
  modifiedShape.object.removeShape(modifiedShape.shape, this);
137
147
  }
138
148
  this.addShapes(fusionResult.newShapes);
149
+ if (fusionResult.toolHistory) {
150
+ this.remapClassifiedFaces(fusionResult.toolHistory);
151
+ }
152
+ this.classifyExtrudeEdges();
153
+ this.setFinalShapes(this.getShapes());
139
154
  }
140
155
  getDependencies() {
141
156
  const source = this.getSource();
@@ -53,6 +53,7 @@ export class Extrude extends ExtrudeBase {
53
53
  else {
54
54
  this.buildAdd(faces, plane, context, inwardEdges, outwardEdges);
55
55
  }
56
+ this.setFinalShapes(this.getShapes());
56
57
  console.log(`[perf] Extrude.build TOTAL: ${(performance.now() - tBuild).toFixed(1)} ms`);
57
58
  }
58
59
  buildAdd(faces, plane, context, inwardEdges, outwardEdges) {
@@ -81,10 +82,15 @@ export class Extrude extends ExtrudeBase {
81
82
  console.log("Extrusions before fusion:", extrusions.length);
82
83
  if (extrusions.length === 0 || sceneObjects.length === 0) {
83
84
  this.addShapes(extrusions);
85
+ this.recordShapeFacesAndEdgesAsAdditions(extrusions);
86
+ this.classifyExtrudeEdges();
84
87
  return;
85
88
  }
86
89
  const tFuse = performance.now();
87
- const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, this.isFaceSourced() ? { glue: 'full' } : undefined);
90
+ const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
91
+ glue: this.isFaceSourced() ? 'full' : undefined,
92
+ recordHistoryFor: this,
93
+ });
88
94
  console.log(`[perf] Extrude.buildAdd.fuseWithSceneObjects: ${(performance.now() - tFuse).toFixed(1)} ms`);
89
95
  for (const modifiedShape of fusionResult.modifiedShapes) {
90
96
  if (!modifiedShape.object) {
@@ -93,6 +99,10 @@ export class Extrude extends ExtrudeBase {
93
99
  modifiedShape.object.removeShape(modifiedShape.shape, this);
94
100
  }
95
101
  this.addShapes(fusionResult.newShapes);
102
+ if (fusionResult.toolHistory) {
103
+ this.remapClassifiedFaces(fusionResult.toolHistory);
104
+ }
105
+ this.classifyExtrudeEdges();
96
106
  }
97
107
  buildSymmetric(faces, plane, context, inwardEdges, outwardEdges) {
98
108
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
@@ -103,7 +113,9 @@ export class Extrude extends ExtrudeBase {
103
113
  const extrusions2 = extruder2.extrude();
104
114
  const endFaces = extruder2.getEndFaces();
105
115
  const all = [...extrusions1, ...extrusions2];
106
- const { result: extrusions } = BooleanOps.fuse(all);
116
+ const halvesFuse = BooleanOps.fuse(all);
117
+ const extrusions = halvesFuse.result;
118
+ halvesFuse.dispose();
107
119
  // Collect remaining faces and fused start/end faces from the fused solid.
108
120
  // We need the fused face objects (not pre-fusion) for IsPartner matching.
109
121
  const remainingFaces = [];
@@ -182,9 +194,13 @@ export class Extrude extends ExtrudeBase {
182
194
  this.getSource()?.removeShapes(this);
183
195
  if (extrusions.length === 0 || sceneObjects.length === 0) {
184
196
  this.addShapes(extrusions);
197
+ this.recordShapeFacesAndEdgesAsAdditions(extrusions);
198
+ this.classifyExtrudeEdges();
185
199
  return;
186
200
  }
187
- const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions);
201
+ const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
202
+ recordHistoryFor: this,
203
+ });
188
204
  for (const modifiedShape of fusionResult.modifiedShapes) {
189
205
  if (!modifiedShape.object) {
190
206
  continue;
@@ -192,6 +208,10 @@ export class Extrude extends ExtrudeBase {
192
208
  modifiedShape.object.removeShape(modifiedShape.shape, this);
193
209
  }
194
210
  this.addShapes(fusionResult.newShapes);
211
+ if (fusionResult.toolHistory) {
212
+ this.remapClassifiedFaces(fusionResult.toolHistory);
213
+ }
214
+ this.classifyExtrudeEdges();
195
215
  }
196
216
  buildRemove(faces, plane, context) {
197
217
  const scope = this.resolveFusionScope(context.getSceneObjects());
@@ -212,8 +232,9 @@ export class Extrude extends ExtrudeBase {
212
232
  const extruder2 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset());
213
233
  const extrusions2 = extruder2.extrude();
214
234
  const all = [...extrusions1, ...extrusions2];
215
- const { result } = BooleanOps.fuse(all);
216
- toolShapes = result;
235
+ const halvesFuse = BooleanOps.fuse(all);
236
+ toolShapes = halvesFuse.result;
237
+ halvesFuse.dispose();
217
238
  }
218
239
  }
219
240
  else if (isThroughAll) {
@@ -229,7 +250,9 @@ export class Extrude extends ExtrudeBase {
229
250
  toolShapes = extruder.extrude();
230
251
  }
231
252
  this.getSource()?.removeShapes(this);
232
- cutWithSceneObjects(scope, toolShapes, plane, this.distance, this);
253
+ cutWithSceneObjects(scope, toolShapes, plane, this.distance, this, {
254
+ recordHistoryFor: this,
255
+ });
233
256
  }
234
257
  getDependencies() {
235
258
  const source = this.getSource();
@@ -65,12 +65,11 @@ export class Fillet extends SceneObject {
65
65
  }
66
66
  edges = edges.filter(e => !targetEdges.includes(e));
67
67
  try {
68
- const newShape = FilletOps.makeFillet(solid, targetEdges, this.radius);
68
+ const newSolids = FilletOps.makeFillet(solid, targetEdges, this.radius);
69
69
  const obj = sceneShapeObjectMap.get(shape);
70
70
  removedShapes.push({ shape: solid, owner: obj });
71
- const subShapes = Explorer.findSolidsWrapped(newShape);
72
- for (const subShape of subShapes) {
73
- addedShapes.push(subShape);
71
+ for (const newSolid of newSolids) {
72
+ addedShapes.push(newSolid);
74
73
  }
75
74
  }
76
75
  catch {
@@ -1,5 +1,6 @@
1
1
  import { SceneObject } from "../common/scene-object.js";
2
2
  import { BooleanOps } from "../oc/boolean-ops.js";
3
+ import { ColorTransfer } from "../oc/color-transfer.js";
3
4
  export class Fuse extends SceneObject {
4
5
  _sceneObjects = [];
5
6
  constructor(...objects) {
@@ -26,16 +27,29 @@ export class Fuse extends SceneObject {
26
27
  }
27
28
  const fuseResult = BooleanOps.fuse(allShapes);
28
29
  if (fuseResult.result.length === allShapes.length) {
30
+ fuseResult.dispose();
29
31
  return;
30
32
  }
31
33
  if (!fuseResult.modifiedShapes.length) {
34
+ fuseResult.dispose();
32
35
  return;
33
36
  }
37
+ // Color rule for the user-facing Fuse op: the FIRST input is dominant.
38
+ // If it has any colors, propagate those to the result (and bleed across
39
+ // adjacent faces from any uncolored input). If the first input has no
40
+ // colors, the fused result stays uncolored — even if other inputs are
41
+ // colored, those colors are intentionally dropped.
42
+ const firstShape = allShapes[0];
43
+ if (firstShape && firstShape.hasColors()) {
44
+ ColorTransfer.applyThroughMaker([firstShape], fuseResult.newShapes, fuseResult.maker);
45
+ ColorTransfer.applyBleeding([firstShape], fuseResult.newShapes, fuseResult.maker);
46
+ }
34
47
  for (const shape of fuseResult.modifiedShapes) {
35
48
  const obj = objShapeMap.get(shape);
36
49
  obj.removeShape(shape, this);
37
50
  }
38
51
  this.addShapes(fuseResult.newShapes);
52
+ fuseResult.dispose();
39
53
  }
40
54
  compareTo(other) {
41
55
  if (!(other instanceof Fuse)) {
@@ -9,4 +9,5 @@ export declare class ExtrudeThroughAll {
9
9
  private shapes;
10
10
  constructor(extrudable: Extrudable, symmetric: boolean, reversed: boolean, pickedFaces?: Face[]);
11
11
  build(): Solid[];
12
+ private firstSolidOf;
12
13
  }
@@ -1,5 +1,12 @@
1
+ import { Solid } from "../common/shapes.js";
1
2
  import { ExtrudeOps } from "../oc/extrude-ops.js";
2
3
  import { FaceMaker2 } from "../oc/face-maker2.js";
4
+ import { BooleanOps } from "../oc/boolean-ops.js";
5
+ import { Explorer } from "../oc/explorer.js";
6
+ /** A large finite magnitude that stands in for "infinity" in through-all ops.
7
+ * True infinite prisms (via OC's `Inf=true` flag) silently fail inside
8
+ * `BRepAlgoAPI_Cut` — use a large finite extrusion instead. */
9
+ const THROUGH_ALL_LENGTH = 100000;
3
10
  export class ExtrudeThroughAll {
4
11
  extrudable;
5
12
  symmetric;
@@ -25,19 +32,29 @@ export class ExtrudeThroughAll {
25
32
  dir = dir.multiply(-1);
26
33
  }
27
34
  const shouldDispose = !this.pickedFaces;
35
+ const bigDir = dir.multiply(THROUGH_ALL_LENGTH);
28
36
  if (this.symmetric) {
29
37
  for (const face of faces) {
30
- const solid = ExtrudeOps.makePrismSymmetric(face, dir);
31
- solids.push(solid);
38
+ // Fuse two finite large prisms — one in each direction — to approximate
39
+ // a symmetric through-all tool. Cannot use makePrismSymmetric here
40
+ // because `BRepAlgoAPI_Cut` silently drops infinite shapes.
41
+ const positive = ExtrudeOps.makePrism(face, bigDir, 1);
42
+ const negative = ExtrudeOps.makePrism(face, bigDir, -1);
43
+ const fuseResult = BooleanOps.fuse([positive, negative]);
44
+ const fusedSolid = fuseResult.result.find(s => s.getType() === 'solid')
45
+ ?? this.firstSolidOf(fuseResult.result);
46
+ if (fusedSolid) {
47
+ solids.push(fusedSolid);
48
+ }
49
+ fuseResult.dispose();
32
50
  if (shouldDispose) {
33
51
  face.dispose();
34
52
  }
35
53
  }
36
54
  }
37
55
  else {
38
- dir = dir.multiply(100000);
39
56
  for (const face of faces) {
40
- const solid = ExtrudeOps.makePrism(face, dir, 1);
57
+ const solid = ExtrudeOps.makePrism(face, bigDir, 1);
41
58
  solids.push(solid);
42
59
  if (shouldDispose) {
43
60
  face.dispose();
@@ -47,4 +64,16 @@ export class ExtrudeThroughAll {
47
64
  this.shapes = solids;
48
65
  return solids;
49
66
  }
67
+ firstSolidOf(shapes) {
68
+ for (const shape of shapes) {
69
+ if (shape.getType() === 'solid') {
70
+ return shape;
71
+ }
72
+ const subSolids = Explorer.findShapes(shape.getShape(), Explorer.getOcShapeType('solid'));
73
+ if (subSolids.length > 0) {
74
+ return Solid.fromTopoDSSolid(Explorer.toSolid(subSolids[0]));
75
+ }
76
+ }
77
+ return null;
78
+ }
50
79
  }
@@ -66,21 +66,32 @@ export class Loft extends ExtrudeBase {
66
66
  if (this._operationMode === 'remove') {
67
67
  const scope = this.resolveFusionScope(context.getSceneObjects());
68
68
  const plane = firstPlane || lastPlane;
69
- cutWithSceneObjects(scope, newShapes, plane, 0, this);
69
+ cutWithSceneObjects(scope, newShapes, plane, 0, this, { recordHistoryFor: this });
70
+ this.setFinalShapes(this.getShapes());
70
71
  return;
71
72
  }
72
73
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
73
74
  if (sceneObjects.length === 0) {
74
75
  this.addShapes(newShapes);
76
+ this.recordShapeFacesAndEdgesAsAdditions(newShapes);
77
+ this.classifyExtrudeEdges();
78
+ this.setFinalShapes(this.getShapes());
75
79
  return;
76
80
  }
77
- const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes);
81
+ const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes, {
82
+ recordHistoryFor: this,
83
+ });
78
84
  for (const modifiedShape of fusionResult.modifiedShapes) {
79
85
  if (modifiedShape.object) {
80
86
  modifiedShape.object.removeShape(modifiedShape.shape, this);
81
87
  }
82
88
  }
83
89
  this.addShapes(fusionResult.newShapes);
90
+ if (fusionResult.toolHistory) {
91
+ this.remapClassifiedFaces(fusionResult.toolHistory);
92
+ }
93
+ this.classifyExtrudeEdges();
94
+ this.setFinalShapes(this.getShapes());
84
95
  }
85
96
  buildThinLoft() {
86
97
  const outerWires = [];
@@ -103,9 +114,11 @@ export class Loft extends ExtrudeBase {
103
114
  const outerSolids = LoftOps.makeLoft(outerWires);
104
115
  if (innerWires.length > 0 && innerWires.length === outerWires.length) {
105
116
  const innerSolids = LoftOps.makeLoft(innerWires);
106
- const { result: outerFused } = BooleanOps.fuse(outerSolids);
107
- const { result: innerFused } = BooleanOps.fuse(innerSolids);
108
- const cutResult = BooleanOps.cutShapes(outerFused[0], innerFused[0]);
117
+ const outerFuse = BooleanOps.fuse(outerSolids);
118
+ const innerFuse = BooleanOps.fuse(innerSolids);
119
+ const cutResult = BooleanOps.cutShapes(outerFuse.result[0], innerFuse.result[0]);
120
+ outerFuse.dispose();
121
+ innerFuse.dispose();
109
122
  return [cutResult];
110
123
  }
111
124
  return outerSolids;
@@ -115,21 +115,32 @@ export class Revolve extends ExtrudeBase {
115
115
  this.axis.removeShapes(this);
116
116
  if (this._operationMode === 'remove') {
117
117
  const scope = this.resolveFusionScope(context.getSceneObjects());
118
- cutWithSceneObjects(scope, solids, plane, 0, this);
118
+ cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
119
+ this.setFinalShapes(this.getShapes());
119
120
  return;
120
121
  }
121
122
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
122
123
  if (sceneObjects.length === 0) {
123
124
  this.addShapes(solids);
125
+ this.recordShapeFacesAndEdgesAsAdditions(solids);
126
+ this.classifyExtrudeEdges();
127
+ this.setFinalShapes(this.getShapes());
124
128
  return;
125
129
  }
126
- const fusionResult = fuseWithSceneObjects(sceneObjects, solids);
130
+ const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
131
+ recordHistoryFor: this,
132
+ });
127
133
  for (const modifiedShape of fusionResult.modifiedShapes) {
128
134
  if (modifiedShape.object) {
129
135
  modifiedShape.object.removeShape(modifiedShape.shape, this);
130
136
  }
131
137
  }
132
138
  this.addShapes(fusionResult.newShapes);
139
+ if (fusionResult.toolHistory) {
140
+ this.remapClassifiedFaces(fusionResult.toolHistory);
141
+ }
142
+ this.classifyExtrudeEdges();
143
+ this.setFinalShapes(this.getShapes());
133
144
  }
134
145
  getDependencies() {
135
146
  return this.extrudable ? [this.extrudable] : [];
@@ -102,21 +102,32 @@ export class Sweep extends ExtrudeBase {
102
102
  // Handle boolean operation based on operation mode
103
103
  if (this._operationMode === 'remove') {
104
104
  const scope = this.resolveFusionScope(context.getSceneObjects());
105
- cutWithSceneObjects(scope, newShapes, plane, 0, this);
105
+ cutWithSceneObjects(scope, newShapes, plane, 0, this, { recordHistoryFor: this });
106
+ this.setFinalShapes(this.getShapes());
106
107
  return;
107
108
  }
108
109
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
109
110
  if (sceneObjects.length === 0) {
110
111
  this.addShapes(newShapes);
112
+ this.recordShapeFacesAndEdgesAsAdditions(newShapes);
113
+ this.classifyExtrudeEdges();
114
+ this.setFinalShapes(this.getShapes());
111
115
  return;
112
116
  }
113
- const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes);
117
+ const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes, {
118
+ recordHistoryFor: this,
119
+ });
114
120
  for (const modifiedShape of fusionResult.modifiedShapes) {
115
121
  if (modifiedShape.object) {
116
122
  modifiedShape.object.removeShape(modifiedShape.shape, this);
117
123
  }
118
124
  }
119
125
  this.addShapes(fusionResult.newShapes);
126
+ if (fusionResult.toolHistory) {
127
+ this.remapClassifiedFaces(fusionResult.toolHistory);
128
+ }
129
+ this.classifyExtrudeEdges();
130
+ this.setFinalShapes(this.getShapes());
120
131
  }
121
132
  getSpineWire(pathObj) {
122
133
  const shapes = pathObj.getShapes({ excludeMeta: false });
@@ -1,19 +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
+ import { ShapeHistory } from "../common/shape-history-tracker.js";
4
5
  export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[], opts?: {
5
6
  glue?: 'full' | 'shift';
7
+ recordHistoryFor?: SceneObject;
6
8
  }): {
7
9
  newShapes: Shape<any>[];
8
10
  modifiedShapes: any[];
11
+ toolHistory?: undefined;
9
12
  } | {
10
13
  newShapes: Shape<import("occjs-wrapper").TopoDS_Shape>[];
11
14
  modifiedShapes: {
12
15
  shape: Shape<any>;
13
16
  object: SceneObject;
14
17
  }[];
18
+ toolHistory: ShapeHistory;
15
19
  };
16
- 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
+ }): {
17
23
  cleanedShapes: Shape[];
18
24
  stockShapes: Shape[];
19
25
  };