fluidcad 0.0.15 → 0.0.16

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 (38) hide show
  1. package/bin/fluidcad.js +2 -2
  2. package/lib/dist/common/scene-object.d.ts +4 -1
  3. package/lib/dist/common/scene-object.js +14 -3
  4. package/lib/dist/core/interfaces.d.ts +18 -0
  5. package/lib/dist/core/part.js +1 -1
  6. package/lib/dist/core/repeat.js +2 -1
  7. package/lib/dist/features/2d/aline.d.ts +2 -0
  8. package/lib/dist/features/2d/aline.js +4 -0
  9. package/lib/dist/features/axis.d.ts +2 -0
  10. package/lib/dist/features/axis.js +3 -0
  11. package/lib/dist/features/color.d.ts +1 -0
  12. package/lib/dist/features/color.js +4 -0
  13. package/lib/dist/features/extrude-base.d.ts +13 -0
  14. package/lib/dist/features/extrude-base.js +64 -0
  15. package/lib/dist/features/extrude-to-face.js +28 -7
  16. package/lib/dist/features/extrude-two-distances.js +72 -17
  17. package/lib/dist/features/extrude.js +90 -21
  18. package/lib/dist/features/mirror-shape2d.d.ts +1 -0
  19. package/lib/dist/features/mirror-shape2d.js +7 -0
  20. package/lib/dist/features/remove.js +1 -1
  21. package/lib/dist/filters/filter-builder-base.d.ts +7 -0
  22. package/lib/dist/filters/filter-builder-base.js +16 -0
  23. package/lib/dist/filters/filter.js +6 -0
  24. package/lib/dist/filters/tangent-expander.d.ts +47 -0
  25. package/lib/dist/filters/tangent-expander.js +223 -0
  26. package/lib/dist/index.js +6 -1
  27. package/lib/dist/oc/thin-face-maker.d.ts +12 -1
  28. package/lib/dist/oc/thin-face-maker.js +54 -13
  29. package/lib/dist/rendering/scene.js +5 -4
  30. package/lib/dist/tests/features/extrude-two-distances.test.js +16 -0
  31. package/lib/dist/tests/features/repeat-circular.test.js +12 -0
  32. package/lib/dist/tests/features/repeat-linear.test.js +22 -0
  33. package/lib/dist/tests/features/select.test.js +55 -0
  34. package/lib/dist/tests/features/thin-extrude.test.js +61 -0
  35. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +3 -3
  37. package/ui/dist/assets/{index-BJG141m7.js → index-CLQhpG6A.js} +1 -1
  38. package/ui/dist/index.html +1 -1
package/bin/fluidcad.js CHANGED
@@ -26,14 +26,14 @@ if (positionals[0] === 'init') {
26
26
  process.exit(1);
27
27
  }
28
28
 
29
- writeFileSync(initPath, `import { init } from 'fluidcad'\n\nexport default init()\n`);
29
+ writeFileSync(initPath, `import { init } from 'fluidcad'\n\nexport default await init()\n`);
30
30
 
31
31
  const testPath = resolve(cwd, 'test.fluid.js');
32
32
  if (!existsSync(testPath)) {
33
33
  writeFileSync(testPath, `import { extrude, fillet, rect, shell, sketch } from "fluidcad/core";
34
34
 
35
35
  sketch("xy", () => {
36
- rect(100, 50).radius(10).center();
36
+ rect(100, 50).radius(10).centered();
37
37
  });
38
38
 
39
39
  const e = extrude(30);
@@ -32,6 +32,7 @@ export declare abstract class SceneObject implements Comparable<SceneObject>, Se
32
32
  private _alwaysVisible;
33
33
  private _name;
34
34
  private _guide;
35
+ private _reusable;
35
36
  private _sourceLocation;
36
37
  private _error;
37
38
  protected _fusionScope?: FusionScope;
@@ -74,7 +75,7 @@ export declare abstract class SceneObject implements Comparable<SceneObject>, Se
74
75
  addShape(shape: Shape): void;
75
76
  addShapes(shapes: Shape[]): void;
76
77
  removeShape(shape: Shape, removedBy: SceneObject): void;
77
- removeShapes(removedBy: SceneObject): void;
78
+ removeShapes(removedBy: SceneObject, force?: boolean): void;
78
79
  getOwnShapes(filter?: ShapeFilter, scope?: Set<SceneObject>): Shape[];
79
80
  getChildShapes(filter?: ShapeFilter, type?: ShapeType): Shape[];
80
81
  getShapes(filter?: ShapeFilter, type?: ShapeType): Shape[];
@@ -92,6 +93,8 @@ export declare abstract class SceneObject implements Comparable<SceneObject>, Se
92
93
  getName(): string;
93
94
  name(value: string): this;
94
95
  guide(): this;
96
+ reusable(): this;
97
+ isReusable(): boolean;
95
98
  setSourceLocation(loc: SourceLocation): void;
96
99
  getSourceLocation(): SourceLocation | null;
97
100
  setError(message: string): void;
@@ -10,6 +10,7 @@ export class SceneObject {
10
10
  _alwaysVisible = false;
11
11
  _name = null;
12
12
  _guide = false;
13
+ _reusable = false;
13
14
  _sourceLocation = null;
14
15
  _error = null;
15
16
  _fusionScope = 'all';
@@ -89,7 +90,7 @@ export class SceneObject {
89
90
  return this.getState('snapshot') || [];
90
91
  }
91
92
  compareTo(other) {
92
- const match = this._guide === other._guide;
93
+ const match = this._guide === other._guide && this._reusable === other._reusable;
93
94
  if (!match) {
94
95
  return false;
95
96
  }
@@ -217,10 +218,13 @@ export class SceneObject {
217
218
  removedBy
218
219
  });
219
220
  }
220
- removeShapes(removedBy) {
221
+ removeShapes(removedBy, force) {
222
+ if (this._reusable && !force) {
223
+ return;
224
+ }
221
225
  if (this.isContainer()) {
222
226
  for (const child of this.children) {
223
- child.removeShapes(removedBy);
227
+ child.removeShapes(removedBy, force);
224
228
  }
225
229
  return;
226
230
  }
@@ -303,6 +307,13 @@ export class SceneObject {
303
307
  this._guide = true;
304
308
  return this;
305
309
  }
310
+ reusable() {
311
+ this._reusable = true;
312
+ return this;
313
+ }
314
+ isReusable() {
315
+ return this._reusable;
316
+ }
306
317
  setSourceLocation(loc) {
307
318
  this._sourceLocation = loc;
308
319
  }
@@ -13,6 +13,13 @@ export interface ISceneObject {
13
13
  * final geometry output unless explicitly included.
14
14
  */
15
15
  guide(): this;
16
+ /**
17
+ * Marks this object as reusable. Reusable objects retain their shapes when
18
+ * consumed by features (e.g., extrude, revolve), allowing multiple features
19
+ * to reference the same source geometry. Use `remove(obj)` to force-remove
20
+ * shapes from a reusable object.
21
+ */
22
+ reusable(): this;
16
23
  }
17
24
  export interface IFuseable extends ISceneObject {
18
25
  /**
@@ -243,6 +250,17 @@ export interface IExtrude extends IFuseable {
243
250
  * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
244
251
  */
245
252
  internalEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
253
+ /**
254
+ * Selects the cap faces at the open ends of a thin-walled extrusion from an open profile.
255
+ * These are the small faces connecting the inner and outer walls at the profile endpoints.
256
+ * @param args - Numeric indices or {@link FaceFilterBuilder} instances to filter the selection.
257
+ */
258
+ capFaces(...args: (number | FaceFilterBuilder)[]): ISceneObject;
259
+ /**
260
+ * Selects edges on the cap faces of a thin-walled extrusion from an open profile.
261
+ * @param args - Numeric indices or {@link EdgeFilterBuilder} instances to filter the selection.
262
+ */
263
+ capEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
246
264
  /**
247
265
  * Applies a draft (taper) angle to the extrusion walls.
248
266
  * @param value - A single angle for uniform draft, or a `[start, end]` tuple for asymmetric draft.
@@ -11,7 +11,7 @@ function part(name, callback) {
11
11
  const currentFile = getCurrentFile();
12
12
  const isDirectEdit = sourceLocation
13
13
  && currentFile
14
- && sourceLocation.filePath.replace('virtual:live-render:', '') === currentFile;
14
+ && sourceLocation.filePath === currentFile;
15
15
  if (isDirectEdit) {
16
16
  const scene = getCurrentScene();
17
17
  if (scene) {
@@ -117,7 +117,8 @@ function build(context) {
117
117
  offset = circularOptions.offset;
118
118
  }
119
119
  else {
120
- offset = circularOptions.angle / (count - 1);
120
+ const angle = circularOptions.angle;
121
+ offset = angle % 360 === 0 ? angle / count : angle / (count - 1);
121
122
  }
122
123
  const startOffset = centered ? -(count * offset) / 2 : 0;
123
124
  const transformedObjects = [];
@@ -1,3 +1,4 @@
1
+ import { SceneObject } from "../../common/scene-object.js";
1
2
  import { PlaneObjectBase } from "../plane-renderable-base.js";
2
3
  import { GeometrySceneObject } from "./geometry.js";
3
4
  export declare class AngledLine extends GeometrySceneObject {
@@ -7,6 +8,7 @@ export declare class AngledLine extends GeometrySceneObject {
7
8
  private targetPlane;
8
9
  constructor(length: number, angle: number, centered?: boolean, targetPlane?: PlaneObjectBase);
9
10
  build(): void;
11
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
10
12
  compareTo(other: AngledLine): boolean;
11
13
  getType(): string;
12
14
  getUniqueType(): string;
@@ -47,6 +47,10 @@ export class AngledLine extends GeometrySceneObject {
47
47
  if (this.targetPlane)
48
48
  this.targetPlane.removeShapes(this);
49
49
  }
50
+ createCopy(remap) {
51
+ const targetPlane = this.targetPlane ? (remap.get(this.targetPlane) || this.targetPlane) : null;
52
+ return new AngledLine(this.length, this.angle, this.centered, targetPlane);
53
+ }
50
54
  compareTo(other) {
51
55
  if (!(other instanceof AngledLine)) {
52
56
  return false;
@@ -1,3 +1,4 @@
1
+ import { SceneObject } from "../common/scene-object.js";
1
2
  import { Axis, AxisTransformOptions } from "../math/axis.js";
2
3
  import { AxisObjectBase } from "./axis-renderable-base.js";
3
4
  export declare class AxisObject extends AxisObjectBase {
@@ -5,6 +6,7 @@ export declare class AxisObject extends AxisObjectBase {
5
6
  private options?;
6
7
  constructor(axis: Axis, options?: AxisTransformOptions);
7
8
  build(): void;
9
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
8
10
  compareTo(other: AxisObject): boolean;
9
11
  serialize(): {
10
12
  origin: import("../math/point.js").Point;
@@ -18,6 +18,9 @@ export class AxisObject extends AxisObjectBase {
18
18
  edge.markAsMetaShape();
19
19
  this.addShape(edge);
20
20
  }
21
+ createCopy(remap) {
22
+ return new AxisObject(this.axis, this.options);
23
+ }
21
24
  compareTo(other) {
22
25
  if (!(other instanceof AxisObject)) {
23
26
  return false;
@@ -5,6 +5,7 @@ export declare class Color extends SceneObject {
5
5
  constructor(color: string, selection?: SceneObject);
6
6
  get selection(): SceneObject;
7
7
  build(context: BuildSceneObjectContext): void;
8
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
8
9
  compareTo(other: Color): boolean;
9
10
  getType(): string;
10
11
  serialize(): {};
@@ -69,6 +69,10 @@ export class Color extends SceneObject {
69
69
  this.addShape(newSolid);
70
70
  }
71
71
  }
72
+ createCopy(remap) {
73
+ const selection = this._selection ? (remap.get(this._selection) || this._selection) : undefined;
74
+ return new Color(this.color, selection);
75
+ }
72
76
  compareTo(other) {
73
77
  if (!(other instanceof Color)) {
74
78
  return false;
@@ -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 { Extrudable } from "../helpers/types.js";
4
5
  import { IExtrude } from "../core/interfaces.js";
@@ -30,6 +31,8 @@ export declare abstract class ExtrudeBase extends SceneObject implements IExtrud
30
31
  edges(...indices: number[]): SceneObject;
31
32
  internalFaces(...args: number[] | FaceFilterBuilder[]): SceneObject;
32
33
  internalEdges(...args: number[] | EdgeFilterBuilder[]): SceneObject;
34
+ capFaces(...args: number[] | FaceFilterBuilder[]): SceneObject;
35
+ capEdges(...args: number[] | EdgeFilterBuilder[]): SceneObject;
33
36
  private buildSuffix;
34
37
  private resolveFaces;
35
38
  private resolveEdges;
@@ -66,6 +69,16 @@ export declare abstract class ExtrudeBase extends SceneObject implements IExtrud
66
69
  getEndOffset(): number | undefined;
67
70
  getDrill(): boolean;
68
71
  protected syncWith(other: ExtrudeBase): this;
72
+ /**
73
+ * Reclassifies faces for thin open-profile extrusions into side, internal, and cap faces.
74
+ * Uses 2D midpoint projection (onto sketch plane) to match reference face edges to
75
+ * the inward/outward wire edges, then IsPartner within the solid to classify faces.
76
+ */
77
+ protected reclassifyThinFaces(remainingFaces: Face[], referenceFaces: Face[], plane: Plane, inwardEdges: Edge[], outwardEdges: Edge[]): {
78
+ sideFaces: Face[];
79
+ internalFaces: Face[];
80
+ capFaces: Face[];
81
+ };
69
82
  compareTo(other: ExtrudeBase): boolean;
70
83
  getType(): string;
71
84
  }
@@ -6,6 +6,7 @@ import { FaceMaker2 } from "../oc/face-maker2.js";
6
6
  import { FaceFilterBuilder } from "../filters/face/face-filter.js";
7
7
  import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
8
8
  import { ShapeFilter } from "../filters/filter.js";
9
+ import { EdgeOps } from "../oc/edge-ops.js";
9
10
  export class ExtrudeBase extends SceneObject {
10
11
  _extrudable = null;
11
12
  _draft;
@@ -144,6 +145,25 @@ export class ExtrudeBase extends SceneObject {
144
145
  return this.resolveEdges(edges, args);
145
146
  }, this);
146
147
  }
148
+ capFaces(...args) {
149
+ const suffix = this.buildSuffix('cap-faces', args);
150
+ return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
151
+ const faces = parent.getState('cap-faces') || [];
152
+ const transform = parent.getTransform();
153
+ const originalFaces = transform
154
+ ? (this.getState('cap-faces') || [])
155
+ : null;
156
+ return this.resolveFaces(faces, args, transform, originalFaces);
157
+ }, this);
158
+ }
159
+ capEdges(...args) {
160
+ const suffix = this.buildSuffix('cap-edges', args);
161
+ return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
162
+ const faces = parent.getState('cap-faces') || [];
163
+ const edges = faces.flatMap(f => f.getEdges());
164
+ return this.resolveEdges(edges, args);
165
+ }, this);
166
+ }
147
167
  buildSuffix(prefix, args) {
148
168
  if (args.length === 0) {
149
169
  return prefix;
@@ -321,6 +341,50 @@ export class ExtrudeBase extends SceneObject {
321
341
  this._thin = other._thin;
322
342
  return this;
323
343
  }
344
+ /**
345
+ * Reclassifies faces for thin open-profile extrusions into side, internal, and cap faces.
346
+ * Uses 2D midpoint projection (onto sketch plane) to match reference face edges to
347
+ * the inward/outward wire edges, then IsPartner within the solid to classify faces.
348
+ */
349
+ reclassifyThinFaces(remainingFaces, referenceFaces, plane, inwardEdges, outwardEdges) {
350
+ // Project edge midpoints to 2D on the sketch plane for height-independent matching
351
+ const to2D = (edge) => plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(edge.getShape()));
352
+ const inwardMids = inwardEdges.map(to2D);
353
+ const outwardMids = outwardEdges.map(to2D);
354
+ // Find reference face edges matching inward/outward in 2D
355
+ const solidInwardEdges = [];
356
+ const solidOutwardEdges = [];
357
+ for (const rf of referenceFaces) {
358
+ for (const rfe of rf.getEdges()) {
359
+ const rfeMid = to2D(rfe);
360
+ if (inwardMids.some(mp => rfeMid.distanceTo(mp) < 1e-4)) {
361
+ solidInwardEdges.push(rfe);
362
+ }
363
+ else if (outwardMids.some(mp => rfeMid.distanceTo(mp) < 1e-4)) {
364
+ solidOutwardEdges.push(rfe);
365
+ }
366
+ }
367
+ }
368
+ // Classify remaining faces using IsPartner within the solid
369
+ const sideFaces = [];
370
+ const internalFaces = [];
371
+ const capFaces = [];
372
+ for (const f of remainingFaces) {
373
+ const faceEdges = f.getEdges();
374
+ const isInward = solidInwardEdges.length > 0 && faceEdges.some(fe => solidInwardEdges.some(ie => fe.getShape().IsPartner(ie.getShape())));
375
+ const isOutward = solidOutwardEdges.length > 0 && faceEdges.some(fe => solidOutwardEdges.some(oe => fe.getShape().IsPartner(oe.getShape())));
376
+ if (isInward) {
377
+ internalFaces.push(f);
378
+ }
379
+ else if (isOutward) {
380
+ sideFaces.push(f);
381
+ }
382
+ else {
383
+ capFaces.push(f);
384
+ }
385
+ }
386
+ return { sideFaces, internalFaces, capFaces };
387
+ }
324
388
  compareTo(other) {
325
389
  if (!super.compareTo(other)) {
326
390
  return false;
@@ -32,19 +32,39 @@ export class ExtrudeToFace extends ExtrudeBase {
32
32
  const allEndFaces = [];
33
33
  const allSideFaces = [];
34
34
  const allInternalFaces = [];
35
- const faces = this.isThin()
36
- ? ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1])
37
- : pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane);
35
+ let faces;
36
+ let inwardEdges;
37
+ let outwardEdges;
38
+ if (this.isThin()) {
39
+ const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
40
+ faces = thinResult.faces;
41
+ inwardEdges = thinResult.inwardEdges;
42
+ outwardEdges = thinResult.outwardEdges;
43
+ }
44
+ else {
45
+ faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane);
46
+ }
47
+ const allCapFaces = [];
38
48
  for (const startFace of faces) {
39
49
  if (isPlanar && FaceQuery.areFacePlanesParallel(startFace, targetFace)) {
40
50
  const { shapes, extruder } = this.createSimpleExtrude(startFace, targetFace, plane);
41
51
  for (const s of shapes) {
42
52
  solids.push(s);
43
53
  }
44
- allStartFaces.push(...extruder.getStartFaces());
45
- allEndFaces.push(...extruder.getEndFaces());
46
- allSideFaces.push(...extruder.getSideFaces());
47
- allInternalFaces.push(...extruder.getInternalFaces());
54
+ if (inwardEdges && inwardEdges.length > 0) {
55
+ const result = this.reclassifyThinFaces([...extruder.getSideFaces(), ...extruder.getInternalFaces()], extruder.getStartFaces(), plane, inwardEdges, outwardEdges || []);
56
+ allStartFaces.push(...extruder.getStartFaces());
57
+ allEndFaces.push(...extruder.getEndFaces());
58
+ allSideFaces.push(...result.sideFaces);
59
+ allInternalFaces.push(...result.internalFaces);
60
+ allCapFaces.push(...result.capFaces);
61
+ }
62
+ else {
63
+ allStartFaces.push(...extruder.getStartFaces());
64
+ allEndFaces.push(...extruder.getEndFaces());
65
+ allSideFaces.push(...extruder.getSideFaces());
66
+ allInternalFaces.push(...extruder.getInternalFaces());
67
+ }
48
68
  }
49
69
  else {
50
70
  console.log("Creating advanced extrude for face:");
@@ -58,6 +78,7 @@ export class ExtrudeToFace extends ExtrudeBase {
58
78
  this.setState('end-faces', allEndFaces);
59
79
  this.setState('side-faces', allSideFaces);
60
80
  this.setState('internal-faces', allInternalFaces);
81
+ this.setState('cap-faces', allCapFaces);
61
82
  this.extrudable.removeShapes(this);
62
83
  if (this.face instanceof SceneObject) {
63
84
  this.face.removeShapes(this);
@@ -3,6 +3,7 @@ import { fuseWithSceneObjects, cutWithSceneObjects } from "../helpers/scene-help
3
3
  import { BooleanOps } from "../oc/boolean-ops.js";
4
4
  import { Explorer } from "../oc/explorer.js";
5
5
  import { FaceMaker2 } from "../oc/face-maker2.js";
6
+ import { EdgeOps } from "../oc/edge-ops.js";
6
7
  import { Extruder } from "./simple-extruder.js";
7
8
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
8
9
  export class ExtrudeTwoDistances extends ExtrudeBase {
@@ -20,43 +21,97 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
20
21
  if (pickedFaces !== null && pickedFaces.length === 0) {
21
22
  return;
22
23
  }
23
- const faces = this.isThin()
24
- ? ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1])
25
- : pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
26
- const extruder1 = new Extruder(faces, plane, this.distance1, this.getDraft(), this.getEndOffset());
24
+ let faces;
25
+ let inwardEdges;
26
+ let outwardEdges;
27
+ if (this.isThin()) {
28
+ const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
29
+ faces = thinResult.faces;
30
+ inwardEdges = thinResult.inwardEdges;
31
+ outwardEdges = thinResult.outwardEdges;
32
+ }
33
+ else {
34
+ faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
35
+ }
36
+ const draft = this.getDraft();
37
+ const draft1 = draft ? [draft[0], draft[0]] : undefined;
38
+ const draft2 = draft ? [draft[1], draft[1]] : undefined;
39
+ const extruder1 = new Extruder(faces, plane, this.distance1, draft1, this.getEndOffset());
27
40
  const extrusions1 = extruder1.extrude();
28
41
  const startFaces = extruder1.getEndFaces();
29
- const extruder2 = new Extruder(faces, plane, -this.distance2, this.getDraft(), this.getEndOffset());
42
+ const extruder2 = new Extruder(faces, plane, -this.distance2, draft2, this.getEndOffset());
30
43
  const extrusions2 = extruder2.extrude();
31
44
  const endFaces = extruder2.getEndFaces();
32
- const preFusionInternalFaces = [
33
- ...extruder1.getInternalFaces(),
34
- ...extruder2.getInternalFaces(),
35
- ];
36
45
  const all = [...extrusions1, ...extrusions2];
37
46
  const { result: extrusions } = BooleanOps.fuse(all);
38
- const sideFaces = [];
39
- const internalFaces = [];
47
+ const remainingFaces = [];
48
+ const fusedStartFaces = [];
49
+ const fusedEndFaces = [];
40
50
  for (const solid of extrusions) {
41
51
  const allFaces = Explorer.findFacesWrapped(solid);
42
52
  for (const f of allFaces) {
43
53
  const isStart = startFaces.some(sf => f.getShape().IsSame(sf.getShape()));
44
54
  const isEnd = endFaces.some(ef => f.getShape().IsSame(ef.getShape()));
45
- if (!isStart && !isEnd) {
46
- const isInternal = preFusionInternalFaces.some(pf => f.getShape().IsSame(pf.getShape()));
47
- if (isInternal) {
48
- internalFaces.push(f);
55
+ if (isStart) {
56
+ fusedStartFaces.push(f);
57
+ }
58
+ else if (isEnd) {
59
+ fusedEndFaces.push(f);
60
+ }
61
+ else {
62
+ remainingFaces.push(f);
63
+ }
64
+ }
65
+ }
66
+ let sideFaces;
67
+ let internalFaces;
68
+ let capFaces = [];
69
+ if (inwardEdges && inwardEdges.length > 0) {
70
+ const result = this.reclassifyThinFaces(remainingFaces, [...fusedStartFaces, ...fusedEndFaces], plane, inwardEdges, outwardEdges || []);
71
+ sideFaces = result.sideFaces;
72
+ internalFaces = result.internalFaces;
73
+ capFaces = result.capFaces;
74
+ }
75
+ else {
76
+ const preInnerEdges = [];
77
+ for (const sf of extruder1.getStartFaces()) {
78
+ for (const wire of sf.getWires()) {
79
+ if (!wire.isCW(plane.normal)) {
80
+ for (const edge of wire.getEdges()) {
81
+ preInnerEdges.push(edge);
82
+ }
49
83
  }
50
- else {
51
- sideFaces.push(f);
84
+ }
85
+ }
86
+ const fusedInnerEdges = [];
87
+ if (preInnerEdges.length > 0) {
88
+ const innerMids = preInnerEdges.map(e => plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(e.getShape())));
89
+ for (const sf of fusedStartFaces) {
90
+ for (const sfe of sf.getEdges()) {
91
+ const mid = plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(sfe.getShape()));
92
+ if (innerMids.some(im => mid.distanceTo(im) < 1e-4)) {
93
+ fusedInnerEdges.push(sfe);
94
+ }
52
95
  }
53
96
  }
54
97
  }
98
+ sideFaces = [];
99
+ internalFaces = [];
100
+ for (const f of remainingFaces) {
101
+ const isInternal = fusedInnerEdges.length > 0 && f.getEdges().some(fe => fusedInnerEdges.some(ie => fe.getShape().IsPartner(ie.getShape())));
102
+ if (isInternal) {
103
+ internalFaces.push(f);
104
+ }
105
+ else {
106
+ sideFaces.push(f);
107
+ }
108
+ }
55
109
  }
56
110
  this.setState('start-faces', startFaces);
57
111
  this.setState('end-faces', endFaces);
58
112
  this.setState('side-faces', sideFaces);
59
113
  this.setState('internal-faces', internalFaces);
114
+ this.setState('cap-faces', capFaces);
60
115
  this.extrudable.removeShapes(this);
61
116
  if (this._operationMode === 'remove') {
62
117
  const scope = this.resolveFusionScope(context.getSceneObjects());