fluidcad 0.0.28 → 0.0.30

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 (145) hide show
  1. package/lib/dist/common/profiler.d.ts +12 -0
  2. package/lib/dist/common/profiler.js +35 -0
  3. package/lib/dist/common/scene-object.d.ts +3 -0
  4. package/lib/dist/common/scene-object.js +3 -0
  5. package/lib/dist/common/shape-history-tracker.d.ts +9 -1
  6. package/lib/dist/common/shape-history-tracker.js +37 -23
  7. package/lib/dist/core/2d/aline.d.ts +13 -13
  8. package/lib/dist/core/2d/aline.js +20 -11
  9. package/lib/dist/core/2d/arc.d.ts +6 -6
  10. package/lib/dist/core/2d/arc.js +19 -15
  11. package/lib/dist/core/2d/back.d.ts +12 -0
  12. package/lib/dist/core/2d/back.js +11 -0
  13. package/lib/dist/core/2d/circle.d.ts +2 -2
  14. package/lib/dist/core/2d/circle.js +14 -10
  15. package/lib/dist/core/2d/ellipse.d.ts +35 -0
  16. package/lib/dist/core/2d/ellipse.js +65 -0
  17. package/lib/dist/core/2d/hline.d.ts +20 -13
  18. package/lib/dist/core/2d/hline.js +33 -15
  19. package/lib/dist/core/2d/index.d.ts +2 -0
  20. package/lib/dist/core/2d/index.js +2 -0
  21. package/lib/dist/core/2d/intersect.d.ts +2 -2
  22. package/lib/dist/core/2d/intersect.js +7 -3
  23. package/lib/dist/core/2d/line.d.ts +2 -2
  24. package/lib/dist/core/2d/line.js +14 -10
  25. package/lib/dist/core/2d/offset.d.ts +4 -4
  26. package/lib/dist/core/2d/offset.js +9 -5
  27. package/lib/dist/core/2d/polygon.d.ts +4 -4
  28. package/lib/dist/core/2d/polygon.js +24 -20
  29. package/lib/dist/core/2d/project.d.ts +2 -2
  30. package/lib/dist/core/2d/project.js +7 -3
  31. package/lib/dist/core/2d/rect.d.ts +2 -2
  32. package/lib/dist/core/2d/rect.js +22 -21
  33. package/lib/dist/core/2d/slot.d.ts +6 -6
  34. package/lib/dist/core/2d/slot.js +29 -32
  35. package/lib/dist/core/2d/vline.d.ts +20 -13
  36. package/lib/dist/core/2d/vline.js +29 -15
  37. package/lib/dist/core/interfaces.d.ts +62 -0
  38. package/lib/dist/core/mirror.d.ts +7 -7
  39. package/lib/dist/core/mirror.js +17 -11
  40. package/lib/dist/core/part.d.ts +3 -1
  41. package/lib/dist/core/part.js +1 -1
  42. package/lib/dist/core/rotate.d.ts +5 -5
  43. package/lib/dist/core/rotate.js +4 -1
  44. package/lib/dist/core/sketch.d.ts +3 -1
  45. package/lib/dist/core/sketch.js +1 -1
  46. package/lib/dist/core/translate.d.ts +9 -9
  47. package/lib/dist/features/2d/aline.d.ts +8 -5
  48. package/lib/dist/features/2d/aline.js +70 -18
  49. package/lib/dist/features/2d/back.d.ts +14 -0
  50. package/lib/dist/features/2d/back.js +35 -0
  51. package/lib/dist/features/2d/ellipse.d.ts +23 -0
  52. package/lib/dist/features/2d/ellipse.js +75 -0
  53. package/lib/dist/features/2d/hline.d.ts +9 -4
  54. package/lib/dist/features/2d/hline.js +65 -14
  55. package/lib/dist/features/2d/offset.d.ts +3 -0
  56. package/lib/dist/features/2d/offset.js +27 -3
  57. package/lib/dist/features/2d/sketch.d.ts +1 -0
  58. package/lib/dist/features/2d/sketch.js +15 -0
  59. package/lib/dist/features/2d/vline.d.ts +9 -4
  60. package/lib/dist/features/2d/vline.js +67 -15
  61. package/lib/dist/features/common.js +2 -1
  62. package/lib/dist/features/extrude-base.d.ts +19 -1
  63. package/lib/dist/features/extrude-base.js +75 -12
  64. package/lib/dist/features/extrude-two-distances.js +32 -27
  65. package/lib/dist/features/extrude.d.ts +39 -0
  66. package/lib/dist/features/extrude.js +196 -156
  67. package/lib/dist/features/fuse.js +2 -1
  68. package/lib/dist/features/lazy-scene-object.d.ts +1 -0
  69. package/lib/dist/features/lazy-scene-object.js +3 -0
  70. package/lib/dist/features/lazy-vertex.d.ts +1 -0
  71. package/lib/dist/features/lazy-vertex.js +3 -0
  72. package/lib/dist/features/loft.js +11 -8
  73. package/lib/dist/features/mirror-shape.d.ts +2 -0
  74. package/lib/dist/features/mirror-shape.js +16 -0
  75. package/lib/dist/features/mirror-shape2d.d.ts +2 -0
  76. package/lib/dist/features/mirror-shape2d.js +22 -1
  77. package/lib/dist/features/revolve.d.ts +31 -0
  78. package/lib/dist/features/revolve.js +178 -95
  79. package/lib/dist/features/rotate.d.ts +2 -0
  80. package/lib/dist/features/rotate.js +16 -0
  81. package/lib/dist/features/rotate2d.d.ts +2 -0
  82. package/lib/dist/features/rotate2d.js +16 -0
  83. package/lib/dist/features/select.js +2 -1
  84. package/lib/dist/features/simple-extruder.d.ts +3 -1
  85. package/lib/dist/features/simple-extruder.js +13 -9
  86. package/lib/dist/features/subtract.d.ts +2 -2
  87. package/lib/dist/features/subtract.js +3 -3
  88. package/lib/dist/features/sweep.d.ts +14 -0
  89. package/lib/dist/features/sweep.js +93 -80
  90. package/lib/dist/features/translate.d.ts +2 -0
  91. package/lib/dist/features/translate.js +23 -2
  92. package/lib/dist/filters/edge/edge-filter.d.ts +6 -0
  93. package/lib/dist/filters/edge/edge-filter.js +11 -0
  94. package/lib/dist/filters/face/face-filter.d.ts +6 -0
  95. package/lib/dist/filters/face/face-filter.js +11 -0
  96. package/lib/dist/filters/filter-base.d.ts +7 -1
  97. package/lib/dist/filters/filter-base.js +8 -0
  98. package/lib/dist/filters/filter-builder-base.js +11 -0
  99. package/lib/dist/filters/from-object.d.ts +14 -0
  100. package/lib/dist/filters/from-object.js +40 -0
  101. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  102. package/lib/dist/helpers/scene-helpers.js +68 -48
  103. package/lib/dist/oc/color-transfer.js +6 -0
  104. package/lib/dist/oc/edge-ops.d.ts +1 -0
  105. package/lib/dist/oc/edge-ops.js +17 -0
  106. package/lib/dist/oc/extrude-ops.d.ts +18 -1
  107. package/lib/dist/oc/extrude-ops.js +34 -1
  108. package/lib/dist/oc/geometry.d.ts +1 -0
  109. package/lib/dist/oc/geometry.js +27 -0
  110. package/lib/dist/oc/mesh.js +11 -9
  111. package/lib/dist/oc/ray-intersect.d.ts +16 -0
  112. package/lib/dist/oc/ray-intersect.js +91 -0
  113. package/lib/dist/oc/thin-face-maker.d.ts +0 -1
  114. package/lib/dist/oc/thin-face-maker.js +2 -20
  115. package/lib/dist/rendering/render.d.ts +2 -1
  116. package/lib/dist/rendering/render.js +72 -33
  117. package/lib/dist/rendering/scene.d.ts +4 -0
  118. package/lib/dist/tests/features/2d/back.test.d.ts +1 -0
  119. package/lib/dist/tests/features/2d/back.test.js +60 -0
  120. package/lib/dist/tests/features/2d/circle.test.js +1 -1
  121. package/lib/dist/tests/features/2d/constrained.test.js +4 -4
  122. package/lib/dist/tests/features/2d/ellipse.test.d.ts +1 -0
  123. package/lib/dist/tests/features/2d/ellipse.test.js +100 -0
  124. package/lib/dist/tests/features/2d/line.test.js +89 -3
  125. package/lib/dist/tests/features/2d/offset.test.js +1 -1
  126. package/lib/dist/tests/features/2d/polygon.test.js +2 -2
  127. package/lib/dist/tests/features/2d/rect.test.js +1 -1
  128. package/lib/dist/tests/features/2d/slot-from-edge.test.js +1 -1
  129. package/lib/dist/tests/features/2d/slot.test.js +1 -1
  130. package/lib/dist/tests/features/mirror.test.js +58 -0
  131. package/lib/dist/tests/features/mirror2d.test.js +63 -0
  132. package/lib/dist/tests/features/rotate.test.js +62 -0
  133. package/lib/dist/tests/features/rotate2d.test.js +47 -0
  134. package/lib/dist/tests/features/thin-revolve.test.js +37 -1
  135. package/lib/dist/tests/features/translate.test.js +63 -0
  136. package/lib/dist/tests/perf/record-fusion-history.bench.test.d.ts +1 -0
  137. package/lib/dist/tests/perf/record-fusion-history.bench.test.js +77 -0
  138. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  139. package/package.json +1 -1
  140. package/server/dist/index.js +77 -45
  141. package/server/dist/ws-protocol.d.ts +11 -0
  142. package/ui/dist/assets/{index-BrW_x4uc.js → index-6Ep4GPxf.js} +131 -77
  143. package/ui/dist/assets/index-DRKfe6N9.css +2 -0
  144. package/ui/dist/index.html +2 -2
  145. package/ui/dist/assets/index-gPoNOiIs.css +0 -2
@@ -19,14 +19,15 @@ export class Loft extends ExtrudeBase {
19
19
  if (this.profiles.length < 2) {
20
20
  throw new Error("Loft requires at least two profiles.");
21
21
  }
22
+ const p = context.getProfiler();
22
23
  let newShapes;
23
24
  if (this.isThin()) {
24
- newShapes = this.buildThinLoft();
25
+ newShapes = p.record('Build thin loft', () => this.buildThinLoft());
25
26
  }
26
27
  else {
27
28
  const allWires = [];
28
29
  for (const profile of this.profiles) {
29
- const wires = this.getWiresFromSceneObject(profile);
30
+ const wires = p.record('Get profile wires', () => this.getWiresFromSceneObject(profile));
30
31
  if (wires.length === 0) {
31
32
  throw new Error("Could not extract wire from profile.");
32
33
  }
@@ -34,7 +35,7 @@ export class Loft extends ExtrudeBase {
34
35
  allWires.push(wire);
35
36
  }
36
37
  }
37
- newShapes = LoftOps.makeLoft(allWires);
38
+ newShapes = p.record('Make loft', () => LoftOps.makeLoft(allWires));
38
39
  }
39
40
  for (const profile of this.profiles) {
40
41
  profile.removeShapes(this);
@@ -64,13 +65,15 @@ export class Loft extends ExtrudeBase {
64
65
  this.setState('side-faces', sideFaces);
65
66
  // Handle boolean operation based on operation mode
66
67
  if (this._operationMode === 'remove') {
67
- const scope = this.resolveFusionScope(context.getSceneObjects());
68
+ const scope = p.record('Resolve fusion scope', () => this.resolveFusionScope(context.getSceneObjects()));
68
69
  const plane = firstPlane || lastPlane;
69
- cutWithSceneObjects(scope, newShapes, plane, 0, this, { recordHistoryFor: this });
70
+ p.record('Cut with scene objects', () => {
71
+ cutWithSceneObjects(scope, newShapes, plane, 0, this, { recordHistoryFor: this });
72
+ });
70
73
  this.setFinalShapes(this.getShapes());
71
74
  return;
72
75
  }
73
- const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
76
+ const sceneObjects = p.record('Resolve fusion scope', () => this.resolveFusionScope(context.getSceneObjects()));
74
77
  if (sceneObjects.length === 0) {
75
78
  this.addShapes(newShapes);
76
79
  this.recordShapeFacesAndEdgesAsAdditions(newShapes);
@@ -78,9 +81,9 @@ export class Loft extends ExtrudeBase {
78
81
  this.setFinalShapes(this.getShapes());
79
82
  return;
80
83
  }
81
- const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes, {
84
+ const fusionResult = p.record('Fuse with scene objects', () => fuseWithSceneObjects(sceneObjects, newShapes, {
82
85
  recordHistoryFor: this,
83
- });
86
+ }));
84
87
  for (const modifiedShape of fusionResult.modifiedShapes) {
85
88
  if (modifiedShape.object) {
86
89
  modifiedShape.object.removeShape(modifiedShape.shape, this);
@@ -3,7 +3,9 @@ import { PlaneObjectBase } from "./plane-renderable-base.js";
3
3
  export declare class MirrorShape extends SceneObject {
4
4
  private plane;
5
5
  targetObjects: SceneObject[] | null;
6
+ private _excludedObjects;
6
7
  constructor(plane: PlaneObjectBase, targetObjects?: SceneObject[] | null);
8
+ exclude(...objects: SceneObject[]): this;
7
9
  build(context: BuildSceneObjectContext): void;
8
10
  compareTo(other: MirrorShape): boolean;
9
11
  getType(): string;
@@ -5,11 +5,16 @@ import { fuseWithSceneObjects } from "../helpers/scene-helpers.js";
5
5
  export class MirrorShape extends SceneObject {
6
6
  plane;
7
7
  targetObjects;
8
+ _excludedObjects = [];
8
9
  constructor(plane, targetObjects = null) {
9
10
  super();
10
11
  this.plane = plane;
11
12
  this.targetObjects = targetObjects;
12
13
  }
14
+ exclude(...objects) {
15
+ this._excludedObjects.push(...objects);
16
+ return this;
17
+ }
13
18
  build(context) {
14
19
  let objects;
15
20
  let targetObjects = this.targetObjects;
@@ -30,6 +35,9 @@ export class MirrorShape extends SceneObject {
30
35
  else {
31
36
  targetObjects = lastObj ? [lastObj] : objects;
32
37
  }
38
+ if (this._excludedObjects.length > 0) {
39
+ targetObjects = targetObjects.filter(obj => !this._excludedObjects.includes(obj));
40
+ }
33
41
  if (this.plane) {
34
42
  this.plane.removeShapes(this);
35
43
  }
@@ -81,6 +89,14 @@ export class MirrorShape extends SceneObject {
81
89
  return false;
82
90
  }
83
91
  }
92
+ if (this._excludedObjects.length !== other._excludedObjects.length) {
93
+ return false;
94
+ }
95
+ for (let i = 0; i < this._excludedObjects.length; i++) {
96
+ if (!this._excludedObjects[i].compareTo(other._excludedObjects[i])) {
97
+ return false;
98
+ }
99
+ }
84
100
  return true;
85
101
  }
86
102
  getType() {
@@ -5,7 +5,9 @@ import { LazyVertex } from "./lazy-vertex.js";
5
5
  export declare class MirrorShape2D extends GeometrySceneObject {
6
6
  private axis;
7
7
  private targetObjects;
8
+ private _excludedObjects;
8
9
  constructor(axis: AxisObjectBase, targetObjects?: SceneObject[]);
10
+ exclude(...objects: SceneObject[]): this;
9
11
  build(context: BuildSceneObjectContext): void;
10
12
  start(): LazyVertex;
11
13
  end(): LazyVertex;
@@ -6,11 +6,16 @@ import { Vertex } from "../common/vertex.js";
6
6
  export class MirrorShape2D extends GeometrySceneObject {
7
7
  axis;
8
8
  targetObjects;
9
+ _excludedObjects = [];
9
10
  constructor(axis, targetObjects = null) {
10
11
  super();
11
12
  this.axis = axis;
12
13
  this.targetObjects = targetObjects;
13
14
  }
15
+ exclude(...objects) {
16
+ this._excludedObjects.push(...objects);
17
+ return this;
18
+ }
14
19
  build(context) {
15
20
  let targetObjects = this.targetObjects;
16
21
  let sketch = this.sketch;
@@ -23,6 +28,9 @@ export class MirrorShape2D extends GeometrySceneObject {
23
28
  else {
24
29
  targetObjects = objects;
25
30
  }
31
+ if (this._excludedObjects.length > 0) {
32
+ targetObjects = targetObjects.filter(obj => !this._excludedObjects.includes(obj));
33
+ }
26
34
  this.axis.removeShapes(this);
27
35
  axis = this.axis.getAxis();
28
36
  const transformedShapes = [];
@@ -79,7 +87,12 @@ export class MirrorShape2D extends GeometrySceneObject {
79
87
  const targetObjects = this.targetObjects
80
88
  ? this.targetObjects.map(obj => remap.get(obj) || obj)
81
89
  : null;
82
- return new MirrorShape2D(axis, targetObjects);
90
+ const copy = new MirrorShape2D(axis, targetObjects);
91
+ if (this._excludedObjects.length > 0) {
92
+ const remappedExcluded = this._excludedObjects.map(obj => remap.get(obj) || obj);
93
+ copy.exclude(...remappedExcluded);
94
+ }
95
+ return copy;
83
96
  }
84
97
  compareTo(other) {
85
98
  if (!(other instanceof MirrorShape2D)) {
@@ -101,6 +114,14 @@ export class MirrorShape2D extends GeometrySceneObject {
101
114
  return false;
102
115
  }
103
116
  }
117
+ if (this._excludedObjects.length !== other._excludedObjects.length) {
118
+ return false;
119
+ }
120
+ for (let i = 0; i < this._excludedObjects.length; i++) {
121
+ if (!this._excludedObjects[i].compareTo(other._excludedObjects[i])) {
122
+ return false;
123
+ }
124
+ }
104
125
  return true;
105
126
  }
106
127
  getType() {
@@ -8,6 +8,37 @@ export declare class Revolve extends ExtrudeBase implements IRevolve {
8
8
  angle: number;
9
9
  constructor(axis: AxisObjectBase, angle: number, extrudable?: Extrudable);
10
10
  build(context: BuildSceneObjectContext): void;
11
+ /** Plain revolve: classify by inner-wire detection on the source plane. */
12
+ private buildRevolve;
13
+ /** Thin revolve: shell-like profile with inward/outward offsets. */
14
+ private buildRevolveThin;
15
+ /**
16
+ * Classify a thin open-profile revolve via the per-edge history captured
17
+ * during `makeRevol`. Each entry in `revols[i].edgeFaces` pairs an input
18
+ * edge of the thin face with the swept face it produced; we route that
19
+ * face into internal / side / cap based on which edge category the input
20
+ * belongs to (`thinResult.inwardEdges`, `outwardEdges`, or neither →
21
+ * cap-line edge added by `makeOpenFaceWithCaps`).
22
+ */
23
+ private classifyThinByEdgeHistory;
24
+ /**
25
+ * Run the revolutions for each fused profile face. Identifies start/end
26
+ * faces via the maker's `FirstShape` / `LastShape` and tracks which side
27
+ * face each input edge generated via `Generated()` — both survive the
28
+ * downstream `cleanShapeRaw`. Caller refines side → side/internal/cap
29
+ * using the per-edge mapping in `revols[i].edgeFaces`.
30
+ */
31
+ private runRevolutions;
32
+ /**
33
+ * Rotate the revolved solid by `matrix` (used for `.symmetric()`) and
34
+ * remap firstFace / lastFace / edgeFaces through the transformer's
35
+ * `ModifiedShape` so classification keeps pointing at the right TShapes.
36
+ */
37
+ private applySymmetricTransform;
38
+ /** Inner-wire classification used by both regular revolve and closed thin profiles. */
39
+ private classifyRevolveByInnerWires;
40
+ /** Remove source + axis, then dispatch to cut or fuse path. */
41
+ private dispatchFinalize;
11
42
  getDependencies(): SceneObject[];
12
43
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
13
44
  compareTo(other: Revolve): boolean;
@@ -1,15 +1,17 @@
1
1
  import { rad } from "../helpers/math-helpers.js";
2
2
  import { Solid } from "../common/shapes.js";
3
- import { fuseWithSceneObjects, cutWithSceneObjects } from "../helpers/scene-helpers.js";
3
+ import { cutWithSceneObjects } from "../helpers/scene-helpers.js";
4
4
  import { ExtrudeOps } from "../oc/extrude-ops.js";
5
5
  import { Explorer } from "../oc/explorer.js";
6
- import { ShapeOps } from "../oc/shape-ops.js";
7
6
  import { FaceMaker2 } from "../oc/face-maker2.js";
8
7
  import { ExtrudeBase } from "./extrude-base.js";
9
8
  import { BooleanOps } from "../oc/boolean-ops.js";
10
- import { FaceOps } from "../oc/face-ops.js";
9
+ import { Face } from "../common/face.js";
11
10
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
12
11
  import { Matrix4 } from "../math/matrix4.js";
12
+ import { Convert } from "../oc/convert.js";
13
+ import { ShapeFactory } from "../common/shape-factory.js";
14
+ import { getOC } from "../oc/init.js";
13
15
  export class Revolve extends ExtrudeBase {
14
16
  axis;
15
17
  angle;
@@ -19,128 +21,209 @@ export class Revolve extends ExtrudeBase {
19
21
  this.angle = angle;
20
22
  }
21
23
  build(context) {
24
+ const p = context.getProfiler();
22
25
  const plane = this.extrudable.getPlane();
23
- const pickedFaces = this.resolvePickedFaces(plane);
26
+ const pickedFaces = p.record('Resolve picked faces', () => this.resolvePickedFaces(plane));
24
27
  if (pickedFaces !== null && pickedFaces.length === 0) {
25
28
  return;
26
29
  }
27
- const solids = [];
28
- const allStartFaces = [];
29
- const allEndFaces = [];
30
- let allSideFaces = [];
31
- let allInternalFaces = [];
32
- let allCapFaces = [];
33
- let faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane);
34
- let inwardEdges;
35
- let outwardEdges;
36
30
  if (this.isThin()) {
37
- const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
38
- faces = thinResult.faces;
39
- inwardEdges = thinResult.inwardEdges;
40
- outwardEdges = thinResult.outwardEdges;
31
+ const thinResult = p.record('Make thin faces', () => ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]));
32
+ this.buildRevolveThin(thinResult, plane, context);
33
+ }
34
+ else {
35
+ const faces = pickedFaces ?? p.record('Resolve faces', () => FaceMaker2.getRegions(this.extrudable.getGeometries(), plane));
36
+ this.buildRevolve(faces, plane, context);
37
+ }
38
+ this.setFinalShapes(this.getShapes());
39
+ }
40
+ /** Plain revolve: classify by inner-wire detection on the source plane. */
41
+ buildRevolve(faces, plane, context) {
42
+ const revolved = this.runRevolutions(faces, context);
43
+ const classified = this.classifyRevolveByInnerWires(revolved, plane);
44
+ this.dispatchFinalize(revolved.solids, classified, plane, context);
45
+ }
46
+ /** Thin revolve: shell-like profile with inward/outward offsets. */
47
+ buildRevolveThin(thinResult, plane, context) {
48
+ const revolved = this.runRevolutions(thinResult.faces, context);
49
+ let classified;
50
+ if (thinResult.inwardEdges.length > 0) {
51
+ // Open profile: each input edge of the thin face categorizes the side
52
+ // face it generated (inward → internal, outward → side, anything else
53
+ // is a cap edge → cap face).
54
+ classified = this.classifyThinByEdgeHistory(revolved, thinResult);
55
+ }
56
+ else {
57
+ // Closed profile: regular inner-wire detection.
58
+ classified = this.classifyRevolveByInnerWires(revolved, plane);
41
59
  }
42
- const { result: fusedFaces } = BooleanOps.fuseFaces(faces);
60
+ this.dispatchFinalize(revolved.solids, classified, plane, context);
61
+ }
62
+ /**
63
+ * Classify a thin open-profile revolve via the per-edge history captured
64
+ * during `makeRevol`. Each entry in `revols[i].edgeFaces` pairs an input
65
+ * edge of the thin face with the swept face it produced; we route that
66
+ * face into internal / side / cap based on which edge category the input
67
+ * belongs to (`thinResult.inwardEdges`, `outwardEdges`, or neither →
68
+ * cap-line edge added by `makeOpenFaceWithCaps`).
69
+ */
70
+ classifyThinByEdgeHistory(revolved, thinResult) {
71
+ const sideFaces = [];
72
+ const internalFaces = [];
73
+ const capFaces = [];
74
+ const matchesAny = (edge, refs) => refs.some(r => r.getShape().IsSame(edge));
75
+ for (const revol of revolved.revols) {
76
+ for (const { edge, face } of revol.edgeFaces) {
77
+ if (!face) {
78
+ continue;
79
+ }
80
+ if (matchesAny(edge, thinResult.inwardEdges)) {
81
+ internalFaces.push(face);
82
+ }
83
+ else if (matchesAny(edge, thinResult.outwardEdges)) {
84
+ sideFaces.push(face);
85
+ }
86
+ else {
87
+ capFaces.push(face);
88
+ }
89
+ }
90
+ }
91
+ return {
92
+ startFaces: revolved.startFaces,
93
+ endFaces: revolved.endFaces,
94
+ sideFaces,
95
+ internalFaces,
96
+ capFaces,
97
+ };
98
+ }
99
+ /**
100
+ * Run the revolutions for each fused profile face. Identifies start/end
101
+ * faces via the maker's `FirstShape` / `LastShape` and tracks which side
102
+ * face each input edge generated via `Generated()` — both survive the
103
+ * downstream `cleanShapeRaw`. Caller refines side → side/internal/cap
104
+ * using the per-edge mapping in `revols[i].edgeFaces`.
105
+ */
106
+ runRevolutions(faces, context) {
107
+ const p = context.getProfiler();
108
+ const { result: fusedFaces } = p.record('Fuse faces', () => BooleanOps.fuseFaces(faces));
43
109
  const axis = this.axis.getAxis();
44
- const isFullRevolution = Math.abs(this.angle) >= 360;
110
+ const solids = [];
111
+ const startFaces = [];
112
+ const endFaces = [];
113
+ const sideFaces = [];
114
+ const revols = [];
45
115
  for (const face of fusedFaces) {
46
- const solid = ExtrudeOps.makeRevol(face, axis, rad(this.angle));
47
- let resultSolid;
116
+ let revol = p.record('Revolve face', () => ExtrudeOps.makeRevol(face, axis, rad(this.angle)));
48
117
  if (this._symmetric) {
49
118
  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()));
52
- }
53
- else {
54
- resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(solid.getShape()));
119
+ revol = this.applySymmetricTransform(revol, matrix);
55
120
  }
121
+ const resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(revol.solid.getShape()));
56
122
  solids.push(resultSolid);
57
- // Classify faces of the revolved solid
58
- const solidFaces = Explorer.findFacesWrapped(resultSolid);
59
- for (const f of solidFaces) {
60
- const isOnSourcePlane = FaceOps.faceOnPlaneWrapped(f, plane);
61
- if (isOnSourcePlane && !isFullRevolution) {
62
- allStartFaces.push(f);
123
+ revols.push(revol);
124
+ const firstRaw = revol.firstFace?.getShape() ?? null;
125
+ const lastRaw = revol.lastFace?.getShape() ?? null;
126
+ for (const f of Explorer.findFacesWrapped(resultSolid)) {
127
+ const raw = f.getShape();
128
+ if (firstRaw && raw.IsSame(firstRaw)) {
129
+ startFaces.push(f);
130
+ }
131
+ else if (lastRaw && raw.IsSame(lastRaw)) {
132
+ endFaces.push(f);
63
133
  }
64
134
  else {
65
- allSideFaces.push(f);
135
+ sideFaces.push(f);
66
136
  }
67
137
  }
68
138
  }
69
- // For partial revolves with symmetric, classify start/end by plane offset
70
- if (!isFullRevolution && allStartFaces.length > 1) {
71
- const half = Math.floor(allStartFaces.length / 2);
72
- const startSlice = allStartFaces.splice(0, half);
73
- const endSlice = allStartFaces.splice(0);
74
- allStartFaces.length = 0;
75
- allStartFaces.push(...startSlice);
76
- allEndFaces.push(...endSlice);
77
- }
78
- if (inwardEdges && inwardEdges.length > 0) {
79
- const result = this.reclassifyThinFaces(allSideFaces, allStartFaces, plane, inwardEdges, outwardEdges || []);
80
- allSideFaces = result.sideFaces;
81
- allInternalFaces = result.internalFaces;
82
- allCapFaces = result.capFaces;
83
- }
84
- else {
85
- const innerWireEdges = [];
86
- for (const sf of allStartFaces) {
87
- for (const wire of sf.getWires()) {
88
- if (!wire.isCW(plane.normal)) {
89
- for (const edge of wire.getEdges()) {
90
- innerWireEdges.push(edge);
91
- }
139
+ return { solids, startFaces, endFaces, sideFaces, revols };
140
+ }
141
+ /**
142
+ * Rotate the revolved solid by `matrix` (used for `.symmetric()`) and
143
+ * remap firstFace / lastFace / edgeFaces through the transformer's
144
+ * `ModifiedShape` so classification keeps pointing at the right TShapes.
145
+ */
146
+ applySymmetricTransform(revol, matrix) {
147
+ const oc = getOC();
148
+ const [trsf, disposeTrsf] = Convert.toGpTrsf(matrix);
149
+ const transformer = new oc.BRepBuilderAPI_Transform(trsf);
150
+ transformer.Perform(revol.solid.getShape(), true);
151
+ const transformedSolid = ShapeFactory.fromShape(transformer.Shape());
152
+ const remapFace = (f) => {
153
+ if (!f) {
154
+ return null;
155
+ }
156
+ const modified = transformer.ModifiedShape(f.getShape());
157
+ return Face.fromTopoDSFace(Explorer.toFace(modified));
158
+ };
159
+ const result = {
160
+ solid: transformedSolid,
161
+ firstFace: remapFace(revol.firstFace),
162
+ lastFace: remapFace(revol.lastFace),
163
+ edgeFaces: revol.edgeFaces.map(({ edge, face }) => ({
164
+ edge,
165
+ face: remapFace(face),
166
+ })),
167
+ };
168
+ transformer.delete();
169
+ disposeTrsf();
170
+ return result;
171
+ }
172
+ /** Inner-wire classification used by both regular revolve and closed thin profiles. */
173
+ classifyRevolveByInnerWires(revolved, plane) {
174
+ const innerWireEdges = [];
175
+ for (const sf of revolved.startFaces) {
176
+ for (const wire of sf.getWires()) {
177
+ if (!wire.isCW(plane.normal)) {
178
+ for (const edge of wire.getEdges()) {
179
+ innerWireEdges.push(edge);
92
180
  }
93
181
  }
94
182
  }
95
- if (innerWireEdges.length > 0) {
96
- const remaining = [];
97
- for (const f of allSideFaces) {
98
- const isInternal = f.getEdges().some(fe => innerWireEdges.some(iwe => fe.getShape().IsPartner(iwe.getShape())));
99
- if (isInternal) {
100
- allInternalFaces.push(f);
101
- }
102
- else {
103
- remaining.push(f);
104
- }
183
+ }
184
+ const sideFaces = [];
185
+ const internalFaces = [];
186
+ if (innerWireEdges.length === 0) {
187
+ sideFaces.push(...revolved.sideFaces);
188
+ }
189
+ else {
190
+ for (const f of revolved.sideFaces) {
191
+ const isInternal = f.getEdges().some(fe => innerWireEdges.some(iwe => fe.getShape().IsPartner(iwe.getShape())));
192
+ if (isInternal) {
193
+ internalFaces.push(f);
194
+ }
195
+ else {
196
+ sideFaces.push(f);
105
197
  }
106
- allSideFaces = remaining;
107
198
  }
108
199
  }
109
- this.setState('start-faces', allStartFaces);
110
- this.setState('end-faces', allEndFaces);
111
- this.setState('side-faces', allSideFaces);
112
- this.setState('internal-faces', allInternalFaces);
113
- this.setState('cap-faces', allCapFaces);
200
+ return {
201
+ startFaces: revolved.startFaces,
202
+ endFaces: revolved.endFaces,
203
+ sideFaces,
204
+ internalFaces,
205
+ capFaces: [],
206
+ };
207
+ }
208
+ /** Remove source + axis, then dispatch to cut or fuse path. */
209
+ dispatchFinalize(solids, classified, plane, context) {
114
210
  this.extrudable.removeShapes(this);
115
211
  this.axis.removeShapes(this);
116
212
  if (this._operationMode === 'remove') {
117
213
  const scope = this.resolveFusionScope(context.getSceneObjects());
214
+ // Note: stash classification state up front — cutWithSceneObjects /
215
+ // classifyCutResult writes its own state keys, but the pre-classified
216
+ // faces are useful for the remove path's selection accessors when no
217
+ // cut-specific edges exist for that category.
218
+ this.setState('start-faces', classified.startFaces);
219
+ this.setState('end-faces', classified.endFaces);
220
+ this.setState('side-faces', classified.sideFaces);
221
+ this.setState('internal-faces', classified.internalFaces);
222
+ this.setState('cap-faces', classified.capFaces);
118
223
  cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
119
- this.setFinalShapes(this.getShapes());
120
224
  return;
121
225
  }
122
- const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
123
- if (sceneObjects.length === 0) {
124
- this.addShapes(solids);
125
- this.recordShapeFacesAndEdgesAsAdditions(solids);
126
- this.classifyExtrudeEdges();
127
- this.setFinalShapes(this.getShapes());
128
- return;
129
- }
130
- const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
131
- recordHistoryFor: this,
132
- });
133
- for (const modifiedShape of fusionResult.modifiedShapes) {
134
- if (modifiedShape.object) {
135
- modifiedShape.object.removeShape(modifiedShape.shape, this);
136
- }
137
- }
138
- this.addShapes(fusionResult.newShapes);
139
- if (fusionResult.toolHistory) {
140
- this.remapClassifiedFaces(fusionResult.toolHistory);
141
- }
142
- this.classifyExtrudeEdges();
143
- this.setFinalShapes(this.getShapes());
226
+ this.finalizeAndFuse(solids, classified, context);
144
227
  }
145
228
  getDependencies() {
146
229
  return this.extrudable ? [this.extrudable] : [];
@@ -5,8 +5,10 @@ export declare class Rotate extends SceneObject {
5
5
  angle: number;
6
6
  private copy;
7
7
  private _targetObjects;
8
+ private _excludedObjects;
8
9
  constructor(axis: AxisObjectBase, angle: number, copy?: boolean, ...targets: SceneObject[]);
9
10
  get targetObjects(): SceneObject[];
11
+ exclude(...objects: SceneObject[]): this;
10
12
  build(context: BuildSceneObjectContext): void;
11
13
  compareTo(other: Rotate): boolean;
12
14
  getType(): string;
@@ -7,6 +7,7 @@ export class Rotate extends SceneObject {
7
7
  angle;
8
8
  copy;
9
9
  _targetObjects = null;
10
+ _excludedObjects = [];
10
11
  constructor(axis, angle, copy = false, ...targets) {
11
12
  super();
12
13
  this.axis = axis;
@@ -17,6 +18,10 @@ export class Rotate extends SceneObject {
17
18
  get targetObjects() {
18
19
  return this._targetObjects;
19
20
  }
21
+ exclude(...objects) {
22
+ this._excludedObjects.push(...objects);
23
+ return this;
24
+ }
20
25
  build(context) {
21
26
  let objects;
22
27
  let targetObjects = this.targetObjects;
@@ -34,6 +39,9 @@ export class Rotate extends SceneObject {
34
39
  else {
35
40
  targetObjects = objects;
36
41
  }
42
+ if (this._excludedObjects.length > 0) {
43
+ targetObjects = targetObjects.filter(obj => !this._excludedObjects.includes(obj));
44
+ }
37
45
  this.axis.removeShapes(this);
38
46
  const axis = this.axis.getAxis();
39
47
  const matrix = Matrix4.fromRotationAroundAxis(axis.origin, axis.direction, rad(this.angle));
@@ -77,6 +85,14 @@ export class Rotate extends SceneObject {
77
85
  return false;
78
86
  }
79
87
  }
88
+ if (this._excludedObjects.length !== other._excludedObjects.length) {
89
+ return false;
90
+ }
91
+ for (let i = 0; i < this._excludedObjects.length; i++) {
92
+ if (this._excludedObjects[i] !== other._excludedObjects[i]) {
93
+ return false;
94
+ }
95
+ }
80
96
  return true;
81
97
  }
82
98
  getType() {
@@ -4,8 +4,10 @@ export declare class Rotate2D extends GeometrySceneObject {
4
4
  angle: number;
5
5
  private copy;
6
6
  private _targetObjects;
7
+ private _excludedObjects;
7
8
  constructor(angle: number, copy?: boolean, ...targets: SceneObject[]);
8
9
  get targetObjects(): SceneObject[] | null;
10
+ exclude(...objects: SceneObject[]): this;
9
11
  build(context: BuildSceneObjectContext): void;
10
12
  compareTo(other: Rotate2D): boolean;
11
13
  getType(): string;
@@ -7,6 +7,7 @@ export class Rotate2D extends GeometrySceneObject {
7
7
  angle;
8
8
  copy;
9
9
  _targetObjects = null;
10
+ _excludedObjects = [];
10
11
  constructor(angle, copy = false, ...targets) {
11
12
  super();
12
13
  this.angle = angle;
@@ -16,6 +17,10 @@ export class Rotate2D extends GeometrySceneObject {
16
17
  get targetObjects() {
17
18
  return this._targetObjects;
18
19
  }
20
+ exclude(...objects) {
21
+ this._excludedObjects.push(...objects);
22
+ return this;
23
+ }
19
24
  build(context) {
20
25
  let objects;
21
26
  let targetObjects = this.targetObjects;
@@ -27,6 +32,9 @@ export class Rotate2D extends GeometrySceneObject {
27
32
  else {
28
33
  targetObjects = objects;
29
34
  }
35
+ if (this._excludedObjects.length > 0) {
36
+ targetObjects = targetObjects.filter(obj => !this._excludedObjects.includes(obj));
37
+ }
30
38
  const plane = this.sketch.getPlane();
31
39
  const currentPosition = plane.localToWorld(this.sketch.getPositionAt(this));
32
40
  axis = new Axis(currentPosition, plane.zAxis.direction);
@@ -67,6 +75,14 @@ export class Rotate2D extends GeometrySceneObject {
67
75
  return false;
68
76
  }
69
77
  }
78
+ if (this._excludedObjects.length !== other._excludedObjects.length) {
79
+ return false;
80
+ }
81
+ for (let i = 0; i < this._excludedObjects.length; i++) {
82
+ if (!this._excludedObjects[i].compareTo(other._excludedObjects[i])) {
83
+ return false;
84
+ }
85
+ }
70
86
  if (!super.compareTo(other)) {
71
87
  return false;
72
88
  }
@@ -61,7 +61,8 @@ export class SelectSceneObject extends SceneObject {
61
61
  const remappedConstraint = this.constraintObject
62
62
  ? (remap.get(this.constraintObject) || this.constraintObject)
63
63
  : undefined;
64
- return new SelectSceneObject(this.filters, remappedConstraint);
64
+ const remappedFilters = this.filters.map(f => f.remap(remap));
65
+ return new SelectSceneObject(remappedFilters, remappedConstraint);
65
66
  }
66
67
  transform(matrix) {
67
68
  const mirroredFilters = this.filters.map(f => f.transform(matrix));