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,5 +1,6 @@
1
1
  import { Face } from "../common/face.js";
2
2
  import { Edge } from "../common/edge.js";
3
+ import { Shape } from "../common/shape.js";
3
4
  import { SceneObject } from "../common/scene-object.js";
4
5
  import { Extrudable } from "../helpers/types.js";
5
6
  import { IExtrude } from "../core/interfaces.js";
@@ -8,16 +9,23 @@ import { Point2DLike } from "../math/point.js";
8
9
  import { Plane } from "../math/plane.js";
9
10
  import { FaceFilterBuilder } from "../filters/face/face-filter.js";
10
11
  import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
12
+ import { ShapeHistory } from "../common/shape-history-tracker.js";
11
13
  export declare abstract class ExtrudeBase extends SceneObject implements IExtrude {
12
14
  protected _extrudable: Extrudable | null;
15
+ protected _faceSource: SceneObject | null;
13
16
  protected _draft?: number | [number, number];
14
17
  protected _endOffset?: number;
15
18
  protected _drill?: boolean;
16
19
  protected _picking: boolean;
17
20
  protected _pickPoints: LazyVertex[];
18
21
  protected _thin?: [number] | [number, number];
19
- constructor(extrudable?: Extrudable);
22
+ constructor(source?: Extrudable | SceneObject);
20
23
  get extrudable(): Extrudable;
24
+ get faceSource(): SceneObject | null;
25
+ isFaceSourced(): boolean;
26
+ getSource(): SceneObject | null;
27
+ getSourcePlane(): Plane | null;
28
+ getSourceFaces(): Face[];
21
29
  startFaces(...args: number[] | FaceFilterBuilder[]): SceneObject;
22
30
  endFaces(...args: number[] | FaceFilterBuilder[]): SceneObject;
23
31
  startEdges(...args: number[] | EdgeFilterBuilder[]): SceneObject;
@@ -33,6 +41,40 @@ export declare abstract class ExtrudeBase extends SceneObject implements IExtrud
33
41
  internalEdges(...args: number[] | EdgeFilterBuilder[]): SceneObject;
34
42
  capFaces(...args: number[] | FaceFilterBuilder[]): SceneObject;
35
43
  capEdges(...args: number[] | EdgeFilterBuilder[]): SceneObject;
44
+ /**
45
+ * Read edges for a classification category, preferring the pre-computed
46
+ * state key (set by `classifyExtrudeEdges` during build) and falling back
47
+ * to deriving from the corresponding face-category state (for peer ops that
48
+ * haven't opted into the unified classification step yet).
49
+ */
50
+ private getClassifiedEdges;
51
+ /**
52
+ * Remap the state-stored face category arrays through a fusion's tool-side
53
+ * history so each face reference points at the actual post-fusion face in
54
+ * the final solid. Call after `fuseWithSceneObjects` returns a `toolHistory`.
55
+ */
56
+ protected remapClassifiedFaces(history: ShapeHistory): void;
57
+ /**
58
+ * Record every face/edge of the given shapes as additions on this operation.
59
+ * Used by 3D ops in the "no scene fusion" path — when the tools land
60
+ * unchanged in the scene, every face/edge is brand new from this op's POV.
61
+ */
62
+ protected recordShapeFacesAndEdgesAsAdditions(shapes: Shape[]): void;
63
+ /**
64
+ * One-shot edge classification: derive start/end/side/internal/cap edges
65
+ * from the already-classified face arrays in state and store them as
66
+ * `start-edges`, `end-edges`, `side-edges`, `internal-edges`, `cap-edges`.
67
+ *
68
+ * Call this once after face classification (and after any post-fusion
69
+ * face remapping) so that the selection accessors can just read the
70
+ * pre-computed arrays instead of re-deriving on every access. Matches
71
+ * the classification step from the spec: "Classify the new edges and
72
+ * faces created by the operation".
73
+ *
74
+ * Side edges are the edges of side faces minus any edge that's also on
75
+ * a start/end face (those already belong to start-edges / end-edges).
76
+ */
77
+ protected classifyExtrudeEdges(): void;
36
78
  private buildSuffix;
37
79
  private resolveFaces;
38
80
  private resolveEdges;
@@ -1,3 +1,5 @@
1
+ import { Face } from "../common/face.js";
2
+ import { Edge } from "../common/edge.js";
1
3
  import { SceneObject } from "../common/scene-object.js";
2
4
  import { LazySelectionSceneObject } from "./lazy-scene-object.js";
3
5
  import { normalizePoint2D } from "../helpers/normalize.js";
@@ -7,21 +9,65 @@ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
7
9
  import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
8
10
  import { ShapeFilter } from "../filters/filter.js";
9
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
+ }
10
24
  export class ExtrudeBase extends SceneObject {
11
25
  _extrudable = null;
26
+ _faceSource = null;
12
27
  _draft;
13
28
  _endOffset;
14
29
  _drill = true;
15
30
  _picking = false;
16
31
  _pickPoints = [];
17
32
  _thin;
18
- constructor(extrudable) {
33
+ constructor(source) {
19
34
  super();
20
- this._extrudable = extrudable ?? null;
35
+ if (source) {
36
+ if (source.isExtrudable()) {
37
+ this._extrudable = source;
38
+ }
39
+ else {
40
+ this._faceSource = source;
41
+ }
42
+ }
21
43
  }
22
44
  get extrudable() {
23
45
  return this._extrudable;
24
46
  }
47
+ get faceSource() {
48
+ return this._faceSource;
49
+ }
50
+ isFaceSourced() {
51
+ return this._faceSource !== null;
52
+ }
53
+ getSource() {
54
+ return this._extrudable ?? this._faceSource;
55
+ }
56
+ getSourcePlane() {
57
+ if (this._extrudable) {
58
+ return this._extrudable.getPlane();
59
+ }
60
+ const faces = this.getSourceFaces();
61
+ return faces.length > 0 ? faces[0].getPlane() : null;
62
+ }
63
+ getSourceFaces() {
64
+ if (!this._faceSource) {
65
+ return [];
66
+ }
67
+ return this._faceSource.getShapes()
68
+ .flatMap(s => s.getSubShapes('face'))
69
+ .filter((f) => f instanceof Face);
70
+ }
25
71
  startFaces(...args) {
26
72
  const suffix = this.buildSuffix('start-faces', args);
27
73
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
@@ -47,19 +93,10 @@ export class ExtrudeBase extends SceneObject {
47
93
  startEdges(...args) {
48
94
  const suffix = this.buildSuffix('start-edges', args);
49
95
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
50
- if (this._operationMode === 'remove') {
51
- const edges = parent.getState('start-edges') || [];
52
- const transform = parent.getTransform();
53
- const originalEdges = transform
54
- ? (this.getState('start-edges') || [])
55
- : null;
56
- return this.resolveEdges(edges, args, transform, originalEdges);
57
- }
58
- const faces = parent.getState('start-faces') || [];
59
- const edges = faces.flatMap(f => f.getEdges());
96
+ const edges = this.getClassifiedEdges(parent, 'start-edges', 'start-faces');
60
97
  const transform = parent.getTransform();
61
98
  const originalEdges = transform
62
- ? (this.getState('start-faces') || []).flatMap(f => f.getEdges())
99
+ ? this.getClassifiedEdges(this, 'start-edges', 'start-faces')
63
100
  : null;
64
101
  return this.resolveEdges(edges, args, transform, originalEdges);
65
102
  }, this);
@@ -67,19 +104,10 @@ export class ExtrudeBase extends SceneObject {
67
104
  endEdges(...args) {
68
105
  const suffix = this.buildSuffix('end-edges', args);
69
106
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
70
- if (this._operationMode === 'remove') {
71
- const edges = parent.getState('end-edges') || [];
72
- const transform = parent.getTransform();
73
- const originalEdges = transform
74
- ? (this.getState('end-edges') || [])
75
- : null;
76
- return this.resolveEdges(edges, args, transform, originalEdges);
77
- }
78
- const faces = parent.getState('end-faces') || [];
79
- const edges = faces.flatMap(f => f.getEdges());
107
+ const edges = this.getClassifiedEdges(parent, 'end-edges', 'end-faces');
80
108
  const transform = parent.getTransform();
81
109
  const originalEdges = transform
82
- ? (this.getState('end-faces') || []).flatMap(f => f.getEdges())
110
+ ? this.getClassifiedEdges(this, 'end-edges', 'end-faces')
83
111
  : null;
84
112
  return this.resolveEdges(edges, args, transform, originalEdges);
85
113
  }, this);
@@ -98,13 +126,17 @@ export class ExtrudeBase extends SceneObject {
98
126
  sideEdges(...args) {
99
127
  const suffix = this.buildSuffix('side-edges', args);
100
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.
101
134
  const sideFaces = parent.getState('side-faces') || [];
102
135
  const startFaces = parent.getState('start-faces') || [];
103
136
  const endFaces = parent.getState('end-faces') || [];
104
137
  const excludedEdges = [...startFaces, ...endFaces].flatMap(f => f.getEdges());
105
- const edges = sideFaces.flatMap(f => f.getEdges())
106
- .filter(e => !excludedEdges.some(ex => e.getShape().IsSame(ex.getShape())))
107
- .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()))));
108
140
  return this.resolveEdges(edges, args);
109
141
  }, this);
110
142
  }
@@ -136,12 +168,7 @@ export class ExtrudeBase extends SceneObject {
136
168
  internalEdges(...args) {
137
169
  const suffix = this.buildSuffix('internal-edges', args);
138
170
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
139
- if (this._operationMode === 'remove') {
140
- const edges = parent.getState('internal-edges') || [];
141
- return this.resolveEdges(edges, args);
142
- }
143
- const faces = parent.getState('internal-faces') || [];
144
- const edges = faces.flatMap(f => f.getEdges());
171
+ const edges = this.getClassifiedEdges(parent, 'internal-edges', 'internal-faces');
145
172
  return this.resolveEdges(edges, args);
146
173
  }, this);
147
174
  }
@@ -159,11 +186,89 @@ export class ExtrudeBase extends SceneObject {
159
186
  capEdges(...args) {
160
187
  const suffix = this.buildSuffix('cap-edges', args);
161
188
  return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
162
- const faces = parent.getState('cap-faces') || [];
163
- const edges = faces.flatMap(f => f.getEdges());
189
+ const edges = this.getClassifiedEdges(parent, 'cap-edges', 'cap-faces');
164
190
  return this.resolveEdges(edges, args);
165
191
  }, this);
166
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
+ }
167
272
  buildSuffix(prefix, args) {
168
273
  if (args.length === 0) {
169
274
  return prefix;
@@ -230,7 +335,7 @@ export class ExtrudeBase extends SceneObject {
230
335
  return this._thin;
231
336
  }
232
337
  serializePickFields() {
233
- const plane = this._extrudable?.getPlane();
338
+ const plane = this.getSourcePlane();
234
339
  return {
235
340
  picking: this.isPicking() || undefined,
236
341
  pickPoints: this.isPicking()
@@ -4,7 +4,7 @@ import { Extrudable } from "../helpers/types.js";
4
4
  import { Point } from "../math/point.js";
5
5
  export declare class ExtrudeToFace extends ExtrudeBase {
6
6
  face: SceneObject | 'first-face' | 'last-face';
7
- constructor(face: SceneObject | 'first-face' | 'last-face', extrudable?: Extrudable);
7
+ constructor(face: SceneObject | 'first-face' | 'last-face', source?: Extrudable | SceneObject);
8
8
  build(context: BuildSceneObjectContext): void;
9
9
  private createAdvancedExtrude;
10
10
  private resizePlanarFace;
@@ -13,14 +13,14 @@ import { ThinFaceMaker } from "../oc/thin-face-maker.js";
13
13
  import { Point } from "../math/point.js";
14
14
  export class ExtrudeToFace extends ExtrudeBase {
15
15
  face;
16
- constructor(face, extrudable) {
17
- super(extrudable);
16
+ constructor(face, source) {
17
+ super(source);
18
18
  this.face = face;
19
19
  }
20
20
  build(context) {
21
21
  const allSceneObjects = context.getSceneObjects();
22
22
  const sceneObjects = this.resolveFusionScope(allSceneObjects);
23
- const plane = this.extrudable.getPlane();
23
+ const plane = this.getSourcePlane();
24
24
  const pickedFaces = this.resolvePickedFaces(plane);
25
25
  if (pickedFaces !== null && pickedFaces.length === 0) {
26
26
  return;
@@ -35,7 +35,13 @@ export class ExtrudeToFace extends ExtrudeBase {
35
35
  let faces;
36
36
  let inwardEdges;
37
37
  let outwardEdges;
38
- if (this.isThin()) {
38
+ if (this.isFaceSourced()) {
39
+ if (this.isThin()) {
40
+ throw new Error("thin() is not supported with a face-sourced extrude");
41
+ }
42
+ faces = pickedFaces ?? this.getSourceFaces();
43
+ }
44
+ else if (this.isThin()) {
39
45
  const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
40
46
  faces = thinResult.faces;
41
47
  inwardEdges = thinResult.inwardEdges;
@@ -79,26 +85,37 @@ export class ExtrudeToFace extends ExtrudeBase {
79
85
  this.setState('side-faces', allSideFaces);
80
86
  this.setState('internal-faces', allInternalFaces);
81
87
  this.setState('cap-faces', allCapFaces);
82
- this.extrudable.removeShapes(this);
88
+ this.getSource()?.removeShapes(this);
83
89
  if (this.face instanceof SceneObject) {
84
90
  this.face.removeShapes(this);
85
91
  }
86
92
  if (this._operationMode === 'remove') {
87
93
  const scope = this.resolveFusionScope(allSceneObjects);
88
- cutWithSceneObjects(scope, solids, plane, 0, this);
94
+ cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
95
+ this.setFinalShapes(this.getShapes());
89
96
  return;
90
97
  }
91
98
  if (sceneObjects.length === 0) {
92
99
  this.addShapes(solids);
100
+ this.recordShapeFacesAndEdgesAsAdditions(solids);
101
+ this.classifyExtrudeEdges();
102
+ this.setFinalShapes(this.getShapes());
93
103
  return;
94
104
  }
95
- const fusionResult = fuseWithSceneObjects(sceneObjects, solids);
105
+ const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
106
+ recordHistoryFor: this,
107
+ });
96
108
  for (const modifiedShape of fusionResult.modifiedShapes) {
97
109
  if (modifiedShape.object) {
98
110
  modifiedShape.object.removeShape(modifiedShape.shape, this);
99
111
  }
100
112
  }
101
113
  this.addShapes(fusionResult.newShapes);
114
+ if (fusionResult.toolHistory) {
115
+ this.remapClassifiedFaces(fusionResult.toolHistory);
116
+ }
117
+ this.classifyExtrudeEdges();
118
+ this.setFinalShapes(this.getShapes());
102
119
  }
103
120
  createAdvancedExtrude(sourceFace, targetFace, isPlanar, sketchPlane) {
104
121
  const targetDistance = this.computeSignedDistanceToFace(targetFace, sketchPlane);
@@ -128,7 +145,7 @@ export class ExtrudeToFace extends ExtrudeBase {
128
145
  resizePlanarFace(targetFace) {
129
146
  const endOffset = this.getEndOffset();
130
147
  if (endOffset) {
131
- const dir = this.extrudable.getPlane().normal.reverse();
148
+ const dir = this.getSourcePlane().normal.reverse();
132
149
  return FaceQuery.makeInfinitePlanarFace(targetFace, endOffset, dir);
133
150
  }
134
151
  return FaceQuery.makeInfinitePlanarFace(targetFace);
@@ -165,7 +182,7 @@ export class ExtrudeToFace extends ExtrudeBase {
165
182
  }
166
183
  splitShapesByFace(extrusions, targetFace) {
167
184
  const result = [];
168
- const sourcePlane = this.extrudable.getPlane();
185
+ const sourcePlane = this.getSourcePlane();
169
186
  for (const shape of extrusions) {
170
187
  const solids = BooleanOps.splitShape(shape, targetFace);
171
188
  if (solids.length === 1) {
@@ -221,10 +238,11 @@ export class ExtrudeToFace extends ExtrudeBase {
221
238
  }
222
239
  }
223
240
  getFirstOrLastFace(sceneObjects, mode) {
224
- const plane = this.extrudable.getPlane();
241
+ const plane = this.getSourcePlane();
242
+ const source = this.getSource();
225
243
  const allFaces = [];
226
244
  for (const obj of sceneObjects) {
227
- if (obj === this.extrudable) {
245
+ if (obj === source) {
228
246
  continue;
229
247
  }
230
248
  for (const shape of obj.getShapes()) {
@@ -242,8 +260,9 @@ export class ExtrudeToFace extends ExtrudeBase {
242
260
  }
243
261
  getDependencies() {
244
262
  const deps = [];
245
- if (this.extrudable) {
246
- deps.push(this.extrudable);
263
+ const source = this.getSource();
264
+ if (source) {
265
+ deps.push(source);
247
266
  }
248
267
  if (this.face instanceof SceneObject) {
249
268
  deps.push(this.face);
@@ -254,10 +273,9 @@ export class ExtrudeToFace extends ExtrudeBase {
254
273
  const newFace = this.face instanceof SceneObject
255
274
  ? (remap.get(this.face) || this.face)
256
275
  : this.face;
257
- const extrudable = this.extrudable
258
- ? (remap.get(this.extrudable) || this.extrudable)
259
- : undefined;
260
- return new ExtrudeToFace(newFace, extrudable).syncWith(this);
276
+ const source = this.getSource();
277
+ const remapped = source ? (remap.get(source) || source) : undefined;
278
+ return new ExtrudeToFace(newFace, remapped).syncWith(this);
261
279
  }
262
280
  compareTo(other) {
263
281
  if (!(other instanceof ExtrudeToFace)) {
@@ -266,7 +284,12 @@ export class ExtrudeToFace extends ExtrudeBase {
266
284
  if (!super.compareTo(other)) {
267
285
  return false;
268
286
  }
269
- if (!this.extrudable.compareTo(other.extrudable)) {
287
+ const thisSource = this.getSource();
288
+ const otherSource = other.getSource();
289
+ if (!thisSource !== !otherSource) {
290
+ return false;
291
+ }
292
+ if (thisSource && otherSource && !thisSource.compareTo(otherSource)) {
270
293
  return false;
271
294
  }
272
295
  if (typeof (this.face) !== typeof (other.face)) {
@@ -289,7 +312,7 @@ export class ExtrudeToFace extends ExtrudeBase {
289
312
  serialize() {
290
313
  return {
291
314
  sheptType: 'wire',
292
- extrudable: this.extrudable.serialize(),
315
+ extrudable: this.getSource()?.serialize(),
293
316
  draft: this.getDraft(),
294
317
  endOffset: this.getEndOffset(),
295
318
  face: typeof (this.face) === 'string' ? this.face : 'selection',
@@ -4,7 +4,7 @@ import { Extrudable } from "../helpers/types.js";
4
4
  export declare class ExtrudeTwoDistances extends ExtrudeBase {
5
5
  distance1: number;
6
6
  distance2: number;
7
- constructor(distance1: number, distance2: number, extrudable?: Extrudable);
7
+ constructor(distance1: number, distance2: number, source?: Extrudable | SceneObject);
8
8
  build(context: BuildSceneObjectContext): void;
9
9
  getDependencies(): SceneObject[];
10
10
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
@@ -9,14 +9,14 @@ import { ThinFaceMaker } from "../oc/thin-face-maker.js";
9
9
  export class ExtrudeTwoDistances extends ExtrudeBase {
10
10
  distance1;
11
11
  distance2;
12
- constructor(distance1, distance2, extrudable) {
13
- super(extrudable);
12
+ constructor(distance1, distance2, source) {
13
+ super(source);
14
14
  this.distance1 = distance1;
15
15
  this.distance2 = distance2;
16
16
  }
17
17
  build(context) {
18
18
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
19
- const plane = this.extrudable.getPlane();
19
+ const plane = this.getSourcePlane();
20
20
  const pickedFaces = this.resolvePickedFaces(plane);
21
21
  if (pickedFaces !== null && pickedFaces.length === 0) {
22
22
  return;
@@ -24,7 +24,13 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
24
24
  let faces;
25
25
  let inwardEdges;
26
26
  let outwardEdges;
27
- if (this.isThin()) {
27
+ if (this.isFaceSourced()) {
28
+ if (this.isThin()) {
29
+ throw new Error("thin() is not supported with a face-sourced extrude");
30
+ }
31
+ faces = pickedFaces ?? this.getSourceFaces();
32
+ }
33
+ else if (this.isThin()) {
28
34
  const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
29
35
  faces = thinResult.faces;
30
36
  inwardEdges = thinResult.inwardEdges;
@@ -43,7 +49,9 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
43
49
  const extrusions2 = extruder2.extrude();
44
50
  const endFaces = extruder2.getEndFaces();
45
51
  const all = [...extrusions1, ...extrusions2];
46
- const { result: extrusions } = BooleanOps.fuse(all);
52
+ const halvesFuse = BooleanOps.fuse(all);
53
+ const extrusions = halvesFuse.result;
54
+ halvesFuse.dispose();
47
55
  const remainingFaces = [];
48
56
  const fusedStartFaces = [];
49
57
  const fusedEndFaces = [];
@@ -112,17 +120,25 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
112
120
  this.setState('side-faces', sideFaces);
113
121
  this.setState('internal-faces', internalFaces);
114
122
  this.setState('cap-faces', capFaces);
115
- this.extrudable.removeShapes(this);
123
+ this.getSource()?.removeShapes(this);
116
124
  if (this._operationMode === 'remove') {
117
125
  const scope = this.resolveFusionScope(context.getSceneObjects());
118
- 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());
119
130
  return;
120
131
  }
121
132
  if (extrusions.length === 0 || sceneObjects.length === 0) {
122
133
  this.addShapes(extrusions);
134
+ this.recordShapeFacesAndEdgesAsAdditions(extrusions);
135
+ this.classifyExtrudeEdges();
136
+ this.setFinalShapes(this.getShapes());
123
137
  return;
124
138
  }
125
- const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions);
139
+ const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
140
+ recordHistoryFor: this,
141
+ });
126
142
  for (const modifiedShape of fusionResult.modifiedShapes) {
127
143
  if (!modifiedShape.object) {
128
144
  continue;
@@ -130,15 +146,20 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
130
146
  modifiedShape.object.removeShape(modifiedShape.shape, this);
131
147
  }
132
148
  this.addShapes(fusionResult.newShapes);
149
+ if (fusionResult.toolHistory) {
150
+ this.remapClassifiedFaces(fusionResult.toolHistory);
151
+ }
152
+ this.classifyExtrudeEdges();
153
+ this.setFinalShapes(this.getShapes());
133
154
  }
134
155
  getDependencies() {
135
- return this.extrudable ? [this.extrudable] : [];
156
+ const source = this.getSource();
157
+ return source ? [source] : [];
136
158
  }
137
159
  createCopy(remap) {
138
- const extrudable = this.extrudable
139
- ? (remap.get(this.extrudable) || this.extrudable)
140
- : undefined;
141
- return new ExtrudeTwoDistances(this.distance1, this.distance2, extrudable).syncWith(this);
160
+ const source = this.getSource();
161
+ const remapped = source ? (remap.get(source) || source) : undefined;
162
+ return new ExtrudeTwoDistances(this.distance1, this.distance2, remapped).syncWith(this);
142
163
  }
143
164
  compareTo(other) {
144
165
  if (!(other instanceof ExtrudeTwoDistances)) {
@@ -150,7 +171,12 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
150
171
  if (this.distance1 !== other.distance1 || this.distance2 !== other.distance2) {
151
172
  return false;
152
173
  }
153
- if (!this.extrudable.compareTo(other.extrudable)) {
174
+ const thisSource = this.getSource();
175
+ const otherSource = other.getSource();
176
+ if (!thisSource !== !otherSource) {
177
+ return false;
178
+ }
179
+ if (thisSource && otherSource && !thisSource.compareTo(otherSource)) {
154
180
  return false;
155
181
  }
156
182
  return true;
@@ -163,7 +189,7 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
163
189
  }
164
190
  serialize() {
165
191
  return {
166
- extrudable: this.extrudable.serialize(),
192
+ extrudable: this.getSource()?.serialize(),
167
193
  distance1: this.distance1,
168
194
  distance2: this.distance2,
169
195
  operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
@@ -3,7 +3,7 @@ import { Extrudable } from "../helpers/types.js";
3
3
  import { ExtrudeBase } from "./extrude-base.js";
4
4
  export declare class Extrude extends ExtrudeBase {
5
5
  distance: number;
6
- constructor(distance: number, extrudable?: Extrudable);
6
+ constructor(distance: number, source?: Extrudable | SceneObject);
7
7
  build(context: BuildSceneObjectContext): void;
8
8
  private buildAdd;
9
9
  private buildSymmetric;