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
@@ -9,20 +9,32 @@ import { ExtrudeThroughAll } from "./infinite-extrude.js";
9
9
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
10
10
  export class Extrude extends ExtrudeBase {
11
11
  distance;
12
- constructor(distance, extrudable) {
13
- super(extrudable);
12
+ constructor(distance, source) {
13
+ super(source);
14
14
  this.distance = distance;
15
15
  }
16
16
  build(context) {
17
- const plane = this.extrudable.getPlane();
17
+ const tBuild = performance.now();
18
+ let t = performance.now();
19
+ const plane = this.getSourcePlane();
20
+ console.log(`[perf] Extrude.getSourcePlane: ${(performance.now() - t).toFixed(1)} ms`);
21
+ t = performance.now();
18
22
  const pickedFaces = this.resolvePickedFaces(plane);
23
+ console.log(`[perf] Extrude.resolvePickedFaces: ${(performance.now() - t).toFixed(1)} ms`);
19
24
  if (pickedFaces !== null && pickedFaces.length === 0) {
20
25
  return;
21
26
  }
22
27
  let faces;
23
28
  let inwardEdges;
24
29
  let outwardEdges;
25
- if (this.isThin()) {
30
+ t = performance.now();
31
+ if (this.isFaceSourced()) {
32
+ if (this.isThin()) {
33
+ throw new Error("thin() is not supported with a face-sourced extrude");
34
+ }
35
+ faces = pickedFaces ?? this.getSourceFaces();
36
+ }
37
+ else if (this.isThin()) {
26
38
  const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
27
39
  faces = thinResult.faces;
28
40
  inwardEdges = thinResult.inwardEdges;
@@ -31,6 +43,7 @@ export class Extrude extends ExtrudeBase {
31
43
  else {
32
44
  faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
33
45
  }
46
+ console.log(`[perf] Extrude.resolveFaces (faces=${faces.length}, faceSourced=${this.isFaceSourced()}): ${(performance.now() - t).toFixed(1)} ms`);
34
47
  if (this._operationMode === 'remove') {
35
48
  this.buildRemove(faces, plane, context);
36
49
  }
@@ -40,11 +53,17 @@ export class Extrude extends ExtrudeBase {
40
53
  else {
41
54
  this.buildAdd(faces, plane, context, inwardEdges, outwardEdges);
42
55
  }
56
+ this.setFinalShapes(this.getShapes());
57
+ console.log(`[perf] Extrude.build TOTAL: ${(performance.now() - tBuild).toFixed(1)} ms`);
43
58
  }
44
59
  buildAdd(faces, plane, context, inwardEdges, outwardEdges) {
60
+ let t = performance.now();
45
61
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
62
+ console.log(`[perf] Extrude.buildAdd.resolveFusionScope (n=${sceneObjects.length}): ${(performance.now() - t).toFixed(1)} ms`);
63
+ t = performance.now();
46
64
  const extruder = new Extruder(faces, plane, this.distance, this.getDraft(), this.getEndOffset());
47
65
  let extrusions = extruder.extrude();
66
+ console.log(`[perf] Extrude.buildAdd.extruder.extrude (extrusions=${extrusions.length}): ${(performance.now() - t).toFixed(1)} ms`);
48
67
  let sideFaces = extruder.getSideFaces();
49
68
  let internalFaces = extruder.getInternalFaces();
50
69
  let capFaces = [];
@@ -59,12 +78,20 @@ export class Extrude extends ExtrudeBase {
59
78
  this.setState('side-faces', sideFaces);
60
79
  this.setState('internal-faces', internalFaces);
61
80
  this.setState('cap-faces', capFaces);
62
- this.extrudable.removeShapes(this);
81
+ this.getSource()?.removeShapes(this);
82
+ console.log("Extrusions before fusion:", extrusions.length);
63
83
  if (extrusions.length === 0 || sceneObjects.length === 0) {
64
84
  this.addShapes(extrusions);
85
+ this.recordShapeFacesAndEdgesAsAdditions(extrusions);
86
+ this.classifyExtrudeEdges();
65
87
  return;
66
88
  }
67
- const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions);
89
+ const tFuse = performance.now();
90
+ const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
91
+ glue: this.isFaceSourced() ? 'full' : undefined,
92
+ recordHistoryFor: this,
93
+ });
94
+ console.log(`[perf] Extrude.buildAdd.fuseWithSceneObjects: ${(performance.now() - tFuse).toFixed(1)} ms`);
68
95
  for (const modifiedShape of fusionResult.modifiedShapes) {
69
96
  if (!modifiedShape.object) {
70
97
  continue;
@@ -72,6 +99,10 @@ export class Extrude extends ExtrudeBase {
72
99
  modifiedShape.object.removeShape(modifiedShape.shape, this);
73
100
  }
74
101
  this.addShapes(fusionResult.newShapes);
102
+ if (fusionResult.toolHistory) {
103
+ this.remapClassifiedFaces(fusionResult.toolHistory);
104
+ }
105
+ this.classifyExtrudeEdges();
75
106
  }
76
107
  buildSymmetric(faces, plane, context, inwardEdges, outwardEdges) {
77
108
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
@@ -82,7 +113,9 @@ export class Extrude extends ExtrudeBase {
82
113
  const extrusions2 = extruder2.extrude();
83
114
  const endFaces = extruder2.getEndFaces();
84
115
  const all = [...extrusions1, ...extrusions2];
85
- const { result: extrusions } = BooleanOps.fuse(all);
116
+ const halvesFuse = BooleanOps.fuse(all);
117
+ const extrusions = halvesFuse.result;
118
+ halvesFuse.dispose();
86
119
  // Collect remaining faces and fused start/end faces from the fused solid.
87
120
  // We need the fused face objects (not pre-fusion) for IsPartner matching.
88
121
  const remainingFaces = [];
@@ -158,12 +191,16 @@ export class Extrude extends ExtrudeBase {
158
191
  this.setState('side-faces', sideFaces);
159
192
  this.setState('internal-faces', internalFaces);
160
193
  this.setState('cap-faces', capFaces);
161
- this.extrudable.removeShapes(this);
194
+ this.getSource()?.removeShapes(this);
162
195
  if (extrusions.length === 0 || sceneObjects.length === 0) {
163
196
  this.addShapes(extrusions);
197
+ this.recordShapeFacesAndEdgesAsAdditions(extrusions);
198
+ this.classifyExtrudeEdges();
164
199
  return;
165
200
  }
166
- const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions);
201
+ const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
202
+ recordHistoryFor: this,
203
+ });
167
204
  for (const modifiedShape of fusionResult.modifiedShapes) {
168
205
  if (!modifiedShape.object) {
169
206
  continue;
@@ -171,6 +208,10 @@ export class Extrude extends ExtrudeBase {
171
208
  modifiedShape.object.removeShape(modifiedShape.shape, this);
172
209
  }
173
210
  this.addShapes(fusionResult.newShapes);
211
+ if (fusionResult.toolHistory) {
212
+ this.remapClassifiedFaces(fusionResult.toolHistory);
213
+ }
214
+ this.classifyExtrudeEdges();
174
215
  }
175
216
  buildRemove(faces, plane, context) {
176
217
  const scope = this.resolveFusionScope(context.getSceneObjects());
@@ -179,6 +220,9 @@ export class Extrude extends ExtrudeBase {
179
220
  if (this._symmetric) {
180
221
  // Symmetric cut: create tool centered on sketch plane
181
222
  if (isThroughAll) {
223
+ if (this.isFaceSourced()) {
224
+ throw new Error("through-all is not supported with a face-sourced extrude");
225
+ }
182
226
  const extrudeThroughAll = new ExtrudeThroughAll(this.extrudable, true, true, faces);
183
227
  toolShapes = extrudeThroughAll.build();
184
228
  }
@@ -188,11 +232,15 @@ export class Extrude extends ExtrudeBase {
188
232
  const extruder2 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset());
189
233
  const extrusions2 = extruder2.extrude();
190
234
  const all = [...extrusions1, ...extrusions2];
191
- const { result } = BooleanOps.fuse(all);
192
- toolShapes = result;
235
+ const halvesFuse = BooleanOps.fuse(all);
236
+ toolShapes = halvesFuse.result;
237
+ halvesFuse.dispose();
193
238
  }
194
239
  }
195
240
  else if (isThroughAll) {
241
+ if (this.isFaceSourced()) {
242
+ throw new Error("through-all is not supported with a face-sourced extrude");
243
+ }
196
244
  const extrudeThroughAll = new ExtrudeThroughAll(this.extrudable, false, true, faces);
197
245
  toolShapes = extrudeThroughAll.build();
198
246
  }
@@ -201,17 +249,19 @@ export class Extrude extends ExtrudeBase {
201
249
  const extruder = new Extruder(faces, plane, distance, this.getDraft(), this.getEndOffset());
202
250
  toolShapes = extruder.extrude();
203
251
  }
204
- this.extrudable.removeShapes(this);
205
- cutWithSceneObjects(scope, toolShapes, plane, this.distance, this);
252
+ this.getSource()?.removeShapes(this);
253
+ cutWithSceneObjects(scope, toolShapes, plane, this.distance, this, {
254
+ recordHistoryFor: this,
255
+ });
206
256
  }
207
257
  getDependencies() {
208
- return this.extrudable ? [this.extrudable] : [];
258
+ const source = this.getSource();
259
+ return source ? [source] : [];
209
260
  }
210
261
  createCopy(remap) {
211
- const extrudable = this.extrudable
212
- ? (remap.get(this.extrudable) || this.extrudable)
213
- : undefined;
214
- return new Extrude(this.distance, extrudable).syncWith(this);
262
+ const source = this.getSource();
263
+ const remapped = source ? (remap.get(source) || source) : undefined;
264
+ return new Extrude(this.distance, remapped).syncWith(this);
215
265
  }
216
266
  compareTo(other) {
217
267
  if (!(other instanceof Extrude)) {
@@ -220,7 +270,12 @@ export class Extrude extends ExtrudeBase {
220
270
  if (!super.compareTo(other)) {
221
271
  return false;
222
272
  }
223
- if (!this.extrudable.compareTo(other.extrudable)) {
273
+ const thisSource = this.getSource();
274
+ const otherSource = other.getSource();
275
+ if (!thisSource !== !otherSource) {
276
+ return false;
277
+ }
278
+ if (thisSource && otherSource && !thisSource.compareTo(otherSource)) {
224
279
  return false;
225
280
  }
226
281
  if (this.distance !== other.distance) {
@@ -242,7 +297,7 @@ export class Extrude extends ExtrudeBase {
242
297
  }
243
298
  serialize() {
244
299
  return {
245
- extrudable: this.extrudable.serialize(),
300
+ extrudable: this.getSource()?.serialize(),
246
301
  distance: this.distance,
247
302
  operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
248
303
  symmetric: this._symmetric || undefined,
@@ -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;
@@ -8,7 +8,5 @@ export declare class MirrorShape extends SceneObject {
8
8
  compareTo(other: MirrorShape): boolean;
9
9
  getType(): string;
10
10
  getUniqueType(): string;
11
- serialize(): {
12
- plane: PlaneObjectBase;
13
- };
11
+ serialize(): {};
14
12
  }
@@ -47,6 +47,7 @@ export class MirrorShape extends SceneObject {
47
47
  for (const shape of shapes) {
48
48
  const matrix = Matrix4.mirrorPlane(plane.normal, plane.origin);
49
49
  const transformed = ShapeOps.transform(shape, matrix);
50
+ transformed.setMeshSource(shape, matrix);
50
51
  transformedShapes.push(transformed);
51
52
  }
52
53
  }
@@ -90,7 +91,7 @@ export class MirrorShape extends SceneObject {
90
91
  }
91
92
  serialize() {
92
93
  return {
93
- plane: this.plane,
94
+ // plane: this.plane,
94
95
  };
95
96
  }
96
97
  }
@@ -9,6 +9,7 @@ import { ExtrudeBase } from "./extrude-base.js";
9
9
  import { BooleanOps } from "../oc/boolean-ops.js";
10
10
  import { FaceOps } from "../oc/face-ops.js";
11
11
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
12
+ import { Matrix4 } from "../math/matrix4.js";
12
13
  export class Revolve extends ExtrudeBase {
13
14
  axis;
14
15
  angle;
@@ -45,8 +46,9 @@ export class Revolve extends ExtrudeBase {
45
46
  const solid = ExtrudeOps.makeRevol(face, axis, rad(this.angle));
46
47
  let resultSolid;
47
48
  if (this._symmetric) {
48
- const rotated = ShapeOps.rotateShape(solid.getShape(), axis, -rad(this.angle) / 2);
49
- resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(rotated));
49
+ const matrix = Matrix4.fromRotationAroundAxis(axis.origin, axis.direction, -rad(this.angle) / 2);
50
+ const rotated = ShapeOps.transform(solid, matrix);
51
+ resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(rotated.getShape()));
50
52
  }
51
53
  else {
52
54
  resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(solid.getShape()));
@@ -113,21 +115,32 @@ export class Revolve extends ExtrudeBase {
113
115
  this.axis.removeShapes(this);
114
116
  if (this._operationMode === 'remove') {
115
117
  const scope = this.resolveFusionScope(context.getSceneObjects());
116
- cutWithSceneObjects(scope, solids, plane, 0, this);
118
+ cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
119
+ this.setFinalShapes(this.getShapes());
117
120
  return;
118
121
  }
119
122
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
120
123
  if (sceneObjects.length === 0) {
121
124
  this.addShapes(solids);
125
+ this.recordShapeFacesAndEdgesAsAdditions(solids);
126
+ this.classifyExtrudeEdges();
127
+ this.setFinalShapes(this.getShapes());
122
128
  return;
123
129
  }
124
- const fusionResult = fuseWithSceneObjects(sceneObjects, solids);
130
+ const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
131
+ recordHistoryFor: this,
132
+ });
125
133
  for (const modifiedShape of fusionResult.modifiedShapes) {
126
134
  if (modifiedShape.object) {
127
135
  modifiedShape.object.removeShape(modifiedShape.shape, this);
128
136
  }
129
137
  }
130
138
  this.addShapes(fusionResult.newShapes);
139
+ if (fusionResult.toolHistory) {
140
+ this.remapClassifiedFaces(fusionResult.toolHistory);
141
+ }
142
+ this.classifyExtrudeEdges();
143
+ this.setFinalShapes(this.getShapes());
131
144
  }
132
145
  getDependencies() {
133
146
  return this.extrudable ? [this.extrudable] : [];
@@ -41,6 +41,7 @@ export class Rotate extends SceneObject {
41
41
  const shapes = obj.getShapes();
42
42
  for (const shape of shapes) {
43
43
  const transformed = ShapeOps.transform(shape, matrix);
44
+ transformed.setMeshSource(shape, matrix);
44
45
  this.addShape(transformed);
45
46
  if (!this.copy) {
46
47
  obj.removeShape(shape, this);
@@ -42,9 +42,14 @@ export class Extruder {
42
42
  let lastFaces = [];
43
43
  let sideFaces = [];
44
44
  let internalFaces = [];
45
+ console.log("Fusing faces before extrusion...", this.faces.length);
46
+ const tFuseFaces = performance.now();
45
47
  const fusedFaces = BooleanOps.fuseFaces(this.faces);
48
+ console.log(`[perf] Extruder.fuseFaces (in=${this.faces.length}, out=${fusedFaces.result.length}): ${(performance.now() - tFuseFaces).toFixed(1)} ms`);
46
49
  for (const face of fusedFaces.result) {
50
+ const time = performance.now();
47
51
  let { solid, firstFace, lastFace } = ExtrudeOps.makePrismFromVec(face, vec);
52
+ console.log(`[perf] Extruder.makePrismFromVec: ${(performance.now() - time).toFixed(1)} ms`);
48
53
  if (this.draft) {
49
54
  const draftResult = this.applyDraft(solid, firstFace, lastFace, this.plane);
50
55
  solid = draftResult.solid;
@@ -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 });
@@ -23,7 +23,9 @@ export class Translate extends SceneObject {
23
23
  continue;
24
24
  }
25
25
  const amount = this.amount.asPoint();
26
- let transformed = ShapeOps.transform(shape, Matrix4.fromTranslation(amount.x, amount.y, amount.z));
26
+ const matrix = Matrix4.fromTranslation(amount.x, amount.y, amount.z);
27
+ const transformed = ShapeOps.transform(shape, matrix);
28
+ transformed.setMeshSource(shape, matrix);
27
29
  this.addShape(transformed);
28
30
  if (!this.copy) {
29
31
  obj.removeShape(shape, this);
@@ -58,6 +58,18 @@ export declare class FaceFilterBuilder extends FilterBuilderBase<Face> {
58
58
  * @param plane - The reference plane.
59
59
  */
60
60
  notParallelTo(plane: PlaneLike | PlaneObjectBase): this;
61
+ /**
62
+ * Selects toroidal faces, optionally matching major and/or minor radius.
63
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
64
+ * @param minorRadius - Optional radius of the tube itself.
65
+ */
66
+ torus(majorRadius?: number, minorRadius?: number): this;
67
+ /**
68
+ * Excludes toroidal faces, optionally matching major and/or minor radius.
69
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
70
+ * @param minorRadius - Optional radius of the tube itself.
71
+ */
72
+ notTorus(majorRadius?: number, minorRadius?: number): this;
61
73
  /**
62
74
  * Selects conical faces.
63
75
  */
@@ -4,6 +4,7 @@ import { CircleFilter, NotCircleFilter } from "./circle-filter.js";
4
4
  import { ConeFilter, NotConeFilter } from "./cone-filter.js";
5
5
  import { CylinderCurveFilter, NotCylinderCurveFilter } from "./cylinder-curve.js";
6
6
  import { CylinderFilter, NotCylinderFilter } from "./cylinder.js";
7
+ import { TorusFilter, NotTorusFilter } from "./torus-filter.js";
7
8
  import { NotOnPlaneFilter, OnPlaneFilter } from "./on-plane.js";
8
9
  import { NotParallelFilter, ParallelFilter } from "./parallel.js";
9
10
  import { PlaneObject } from "../../features/plane.js";
@@ -182,6 +183,26 @@ export class FaceFilterBuilder extends FilterBuilderBase {
182
183
  this.filters.push(filter);
183
184
  return this;
184
185
  }
186
+ /**
187
+ * Selects toroidal faces, optionally matching major and/or minor radius.
188
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
189
+ * @param minorRadius - Optional radius of the tube itself.
190
+ */
191
+ torus(majorRadius, minorRadius) {
192
+ const filter = new TorusFilter(majorRadius, minorRadius);
193
+ this.filters.push(filter);
194
+ return this;
195
+ }
196
+ /**
197
+ * Excludes toroidal faces, optionally matching major and/or minor radius.
198
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
199
+ * @param minorRadius - Optional radius of the tube itself.
200
+ */
201
+ notTorus(majorRadius, minorRadius) {
202
+ const filter = new NotTorusFilter(majorRadius, minorRadius);
203
+ this.filters.push(filter);
204
+ return this;
205
+ }
185
206
  /**
186
207
  * Selects conical faces.
187
208
  */
@@ -0,0 +1,19 @@
1
+ import { Matrix4 } from "../../math/matrix4.js";
2
+ import { Face } from "../../common/shapes.js";
3
+ import { FilterBase } from "../filter-base.js";
4
+ export declare class TorusFilter extends FilterBase<Face> {
5
+ private majorRadius?;
6
+ private minorRadius?;
7
+ constructor(majorRadius?: number, minorRadius?: number);
8
+ match(shape: Face): boolean;
9
+ compareTo(other: TorusFilter): boolean;
10
+ transform(matrix: Matrix4): TorusFilter;
11
+ }
12
+ export declare class NotTorusFilter extends FilterBase<Face> {
13
+ private majorRadius?;
14
+ private minorRadius?;
15
+ constructor(majorRadius?: number, minorRadius?: number);
16
+ match(shape: Face): boolean;
17
+ compareTo(other: NotTorusFilter): boolean;
18
+ transform(matrix: Matrix4): NotTorusFilter;
19
+ }
@@ -0,0 +1,38 @@
1
+ import { FilterBase } from "../filter-base.js";
2
+ import { FaceQuery } from "../../oc/face-query.js";
3
+ export class TorusFilter extends FilterBase {
4
+ majorRadius;
5
+ minorRadius;
6
+ constructor(majorRadius, minorRadius) {
7
+ super();
8
+ this.majorRadius = majorRadius;
9
+ this.minorRadius = minorRadius;
10
+ }
11
+ match(shape) {
12
+ return FaceQuery.isTorusFace(shape, this.majorRadius, this.minorRadius);
13
+ }
14
+ compareTo(other) {
15
+ return this.majorRadius === other.majorRadius && this.minorRadius === other.minorRadius;
16
+ }
17
+ transform(matrix) {
18
+ return new TorusFilter(this.majorRadius, this.minorRadius);
19
+ }
20
+ }
21
+ export class NotTorusFilter extends FilterBase {
22
+ majorRadius;
23
+ minorRadius;
24
+ constructor(majorRadius, minorRadius) {
25
+ super();
26
+ this.majorRadius = majorRadius;
27
+ this.minorRadius = minorRadius;
28
+ }
29
+ match(shape) {
30
+ return !FaceQuery.isTorusFace(shape, this.majorRadius, this.minorRadius);
31
+ }
32
+ compareTo(other) {
33
+ return this.majorRadius === other.majorRadius && this.minorRadius === other.minorRadius;
34
+ }
35
+ transform(matrix) {
36
+ return new NotTorusFilter(this.majorRadius, this.minorRadius);
37
+ }
38
+ }