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
@@ -8,8 +8,8 @@ import { Edge } from "../common/edge.js";
8
8
  import { getOC } from "../oc/init.js";
9
9
  import { ColorTransfer } from "../oc/color-transfer.js";
10
10
  export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
11
+ const p = opts?.profiler;
11
12
  const modified = [];
12
- const tCollect = performance.now();
13
13
  const objShapeMap = new Map();
14
14
  for (const obj of sceneObjects) {
15
15
  const shapes = obj.getShapes({}, 'solid');
@@ -18,15 +18,15 @@ export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
18
18
  }
19
19
  }
20
20
  let sceneShapes = Array.from(objShapeMap.keys());
21
- console.log(`[perf] fuseWithSceneObjects.collect (scenes=${sceneShapes.length}, extrusions=${extrusions.length}): ${(performance.now() - tCollect).toFixed(1)} ms`);
22
- const tFuse = performance.now();
23
- const { result, newShapes, modifiedShapes, maker, dispose } = BooleanOps.fuseStockAndTools(sceneShapes, extrusions, opts);
24
- console.log(`[perf] fuseWithSceneObjects.BooleanOps.fuseStockAndTools (glue=${opts?.glue ?? 'off'}): ${(performance.now() - tFuse).toFixed(1)} ms`);
21
+ const fuseRun = () => BooleanOps.fuseStockAndTools(sceneShapes, extrusions, opts);
22
+ const { result, newShapes, modifiedShapes, maker, dispose } = p
23
+ ? p.record('Boolean fuse', fuseRun)
24
+ : fuseRun();
25
25
  if (newShapes.length === 0 && modifiedShapes.length === 0) {
26
- console.log("No fusions were made.");
27
26
  dispose();
28
27
  if (opts?.recordHistoryFor) {
29
- recordShapesAsAdditions(opts.recordHistoryFor, extrusions);
28
+ const run = () => recordShapesAsAdditions(opts.recordHistoryFor, extrusions);
29
+ p ? p.record('Record fusion history', run) : run();
30
30
  }
31
31
  return {
32
32
  newShapes: extrusions,
@@ -44,12 +44,18 @@ export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
44
44
  const shapesToAdd = result.filter(s => !unconsumed.some(u => u.getShape().IsPartner(s.getShape())));
45
45
  let toolHistory;
46
46
  if (opts?.recordHistoryFor) {
47
- recordFusionHistory(opts.recordHistoryFor, sceneShapes, objShapeMap, shapesToAdd, maker);
48
- // Separately track tool-side (extrusion) lineage so callers can remap
49
- // pre-fusion categorizations (start/end/side/…) onto the post-fusion
50
- // faces. We don't store these as modifications on any scene object —
51
- // from the user's POV they are additions on the caller already.
52
- toolHistory = ShapeHistoryTracker.collect(maker, extrusions);
47
+ const recordHistory = () => {
48
+ recordFusionHistory(opts.recordHistoryFor, sceneShapes, objShapeMap, shapesToAdd, maker, p);
49
+ // Separately track tool-side (extrusion) lineage so callers can remap
50
+ // pre-fusion categorizations (start/end/side/…) onto the post-fusion
51
+ // faces. We don't store these as modifications on any scene object —
52
+ // from the user's POV they are additions on the caller already.
53
+ // Tool-side history is only consumed by `remapClassifiedFaces`, which
54
+ // touches modifiedFaces only — skip the added* output traversal.
55
+ const collectTools = () => ShapeHistoryTracker.collect(maker, extrusions, { skipAdded: true });
56
+ toolHistory = p ? p.record('Collect tool history', collectTools) : collectTools();
57
+ };
58
+ p ? p.record('Record fusion history', recordHistory) : recordHistory();
53
59
  }
54
60
  dispose();
55
61
  return { newShapes: shapesToAdd, modifiedShapes: modified, toolHistory };
@@ -80,51 +86,62 @@ function recordShapesAsAdditions(caller, shapes) {
80
86
  * of a scene-shape modification. This captures both extrusion-derived faces
81
87
  * (which appear in the result via tool-side Modified()) and truly new faces.
82
88
  */
83
- function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker) {
89
+ function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker, p) {
84
90
  const oc = getOC();
85
91
  const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
86
92
  const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
87
93
  const claimedFaces = new oc.TopTools_MapOfShape();
88
94
  const claimedEdges = new oc.TopTools_MapOfShape();
89
- for (const sceneShape of sceneShapes) {
90
- const owner = owners.get(sceneShape);
91
- if (!owner) {
92
- continue;
93
- }
94
- const history = ShapeHistoryTracker.collect(maker, [sceneShape]);
95
- for (const record of history.modifiedFaces) {
96
- owner.recordModifiedFaces(record.sources, record.results, caller);
97
- for (const r of record.results) {
98
- claimedFaces.Add(r.getShape());
95
+ const collectScene = () => {
96
+ for (const sceneShape of sceneShapes) {
97
+ const owner = owners.get(sceneShape);
98
+ if (!owner) {
99
+ continue;
99
100
  }
100
- }
101
- for (const record of history.modifiedEdges) {
102
- owner.recordModifiedEdges(record.sources, record.results, caller);
103
- for (const r of record.results) {
104
- claimedEdges.Add(r.getShape());
101
+ // recordFusionHistory aggregates additions across the full result via
102
+ // `claimedFaces`/`claimedEdges` below each per-shape collect doesn't
103
+ // need to compute its own added* sets.
104
+ const history = ShapeHistoryTracker.collect(maker, [sceneShape], { skipAdded: true });
105
+ for (const record of history.modifiedFaces) {
106
+ owner.recordModifiedFaces(record.sources, record.results, caller);
107
+ for (const r of record.results) {
108
+ claimedFaces.Add(r.getShape());
109
+ }
105
110
  }
106
- }
107
- for (const face of history.removedFaces) {
108
- owner.recordRemovedFace(face, caller);
109
- }
110
- for (const edge of history.removedEdges) {
111
- owner.recordRemovedEdge(edge, caller);
112
- }
113
- }
114
- for (const newShape of newShapes) {
115
- for (const raw of Explorer.findShapes(newShape.getShape(), FACE)) {
116
- if (!claimedFaces.Contains(raw)) {
117
- caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
111
+ for (const record of history.modifiedEdges) {
112
+ owner.recordModifiedEdges(record.sources, record.results, caller);
113
+ for (const r of record.results) {
114
+ claimedEdges.Add(r.getShape());
115
+ }
116
+ }
117
+ for (const face of history.removedFaces) {
118
+ owner.recordRemovedFace(face, caller);
119
+ }
120
+ for (const edge of history.removedEdges) {
121
+ owner.recordRemovedEdge(edge, caller);
118
122
  }
119
123
  }
120
- for (const raw of Explorer.findShapes(newShape.getShape(), EDGE)) {
121
- if (!claimedEdges.Contains(raw)) {
122
- caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
124
+ };
125
+ p ? p.record('Collect scene history', collectScene) : collectScene();
126
+ const recordAdditions = () => {
127
+ for (const newShape of newShapes) {
128
+ for (const raw of Explorer.findShapes(newShape.getShape(), FACE)) {
129
+ if (!claimedFaces.Contains(raw)) {
130
+ caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
131
+ }
132
+ }
133
+ for (const raw of Explorer.findShapes(newShape.getShape(), EDGE)) {
134
+ if (!claimedEdges.Contains(raw)) {
135
+ caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
136
+ }
123
137
  }
124
138
  }
125
- }
126
- ColorTransfer.applyThroughMaker(sceneShapes, newShapes, maker);
127
- ColorTransfer.applyBleeding(sceneShapes, newShapes, maker);
139
+ };
140
+ p ? p.record('Record additions', recordAdditions) : recordAdditions();
141
+ const colorThrough = () => ColorTransfer.applyThroughMaker(sceneShapes, newShapes, maker);
142
+ p ? p.record('Color through maker', colorThrough) : colorThrough();
143
+ const colorBleed = () => ColorTransfer.applyBleeding(sceneShapes, newShapes, maker);
144
+ p ? p.record('Color bleeding', colorBleed) : colorBleed();
128
145
  claimedFaces.delete();
129
146
  claimedEdges.delete();
130
147
  }
@@ -229,7 +246,10 @@ function recordCutHistory(caller, stock, owners, cleanedShapes, maker, cleanups)
229
246
  if (!owner) {
230
247
  continue;
231
248
  }
232
- const history = ShapeHistoryTracker.collect(maker, [stockShape]);
249
+ // Same as the fuse path: additions are aggregated below across the full
250
+ // cleaned result via `claimedFaces`/`claimedEdges`, so each per-shape
251
+ // collect can skip its own added* output traversal.
252
+ const history = ShapeHistoryTracker.collect(maker, [stockShape], { skipAdded: true });
233
253
  for (const record of history.modifiedFaces) {
234
254
  const postCleanResults = remapPreCleanFaces(record.results);
235
255
  if (postCleanResults.length === 0) {
@@ -58,6 +58,12 @@ export class ColorTransfer {
58
58
  * Call AFTER `applyThroughMaker` so the colored seeds are in place.
59
59
  */
60
60
  static applyBleeding(sceneSources, results, maker) {
61
+ // No colors anywhere on the source side means the bleed loop will iterate
62
+ // every result face only to find nothing to spread. Short-circuit before
63
+ // building maps or running the O(N²·E²) adjacency scan.
64
+ if (!sceneSources.some(s => s.hasColors())) {
65
+ return;
66
+ }
61
67
  const oc = getOC();
62
68
  const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
63
69
  const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
@@ -9,6 +9,7 @@ export declare class EdgeOps {
9
9
  static getLastVertex(edge: Edge): Vertex;
10
10
  static edgeToAxis(edge: Edge): Axis;
11
11
  static axisToEdge(axis: Axis): Edge;
12
+ static makeLineEdge(p1: Point, p2: Point): Edge;
12
13
  static getVertexPoint(vertex: Vertex): Point;
13
14
  static getEdgeMidPoint(edge: Edge): Point;
14
15
  static reverseEdge(edge: Edge): Edge;
@@ -19,6 +19,23 @@ export class EdgeOps {
19
19
  static axisToEdge(axis) {
20
20
  return Edge.fromTopoDSEdge(EdgeOps.axisToEdgeRaw(axis));
21
21
  }
22
+ static makeLineEdge(p1, p2) {
23
+ const oc = getOC();
24
+ const [gp1, dispose1] = Convert.toGpPnt(p1);
25
+ const [gp2, dispose2] = Convert.toGpPnt(p2);
26
+ const edgeMaker = new oc.BRepBuilderAPI_MakeEdge(gp1, gp2);
27
+ if (!edgeMaker.IsDone()) {
28
+ edgeMaker.delete();
29
+ dispose1();
30
+ dispose2();
31
+ throw new Error("Failed to create line edge");
32
+ }
33
+ const edge = edgeMaker.Edge();
34
+ edgeMaker.delete();
35
+ dispose1();
36
+ dispose2();
37
+ return Edge.fromTopoDSEdge(edge);
38
+ }
22
39
  static getVertexPoint(vertex) {
23
40
  return EdgeOps.getVertexPointRaw(vertex.getShape());
24
41
  }
@@ -3,6 +3,23 @@ import { Vector3d } from "../math/vector3d.js";
3
3
  import { Plane } from "../math/plane.js";
4
4
  import { Axis } from "../math/axis.js";
5
5
  import { Shape } from "../common/shape.js";
6
+ import { Face } from "../common/face.js";
7
+ /**
8
+ * History-based result of a `BRepPrimAPI_MakeRevol` operation. `firstFace` /
9
+ * `lastFace` come from the maker's `FirstShape` / `LastShape` (null on full
10
+ * revolution where the source face is absorbed). `edgeFaces` maps each edge
11
+ * of the input face to the swept face it generated, so callers can classify
12
+ * by input-edge category instead of geometric heuristics.
13
+ */
14
+ export interface MakeRevolResult {
15
+ solid: Shape;
16
+ firstFace: Face | null;
17
+ lastFace: Face | null;
18
+ edgeFaces: {
19
+ edge: TopoDS_Shape;
20
+ face: Face | null;
21
+ }[];
22
+ }
6
23
  export declare class ExtrudeOps {
7
24
  static makePrism(shape: Shape, direction: Vector3d, distance: number): Shape;
8
25
  static makePrismFromVec(shape: Shape, vec: Vector3d): {
@@ -12,7 +29,7 @@ export declare class ExtrudeOps {
12
29
  };
13
30
  static makePrismInfinite(shape: Shape, direction: Vector3d): Shape;
14
31
  static makePrismSymmetric(shape: Shape, direction: Vector3d): Shape;
15
- static makeRevol(shape: Shape, axis: Axis, angle: number): Shape;
32
+ static makeRevol(shape: Shape, axis: Axis, angle: number): MakeRevolResult;
16
33
  static applyDraftOnSideFaces(solid: Shape, firstFace: Shape, lastFace: Shape, plane: Plane, angle: number): {
17
34
  solid: Shape;
18
35
  firstFace: Shape;
@@ -1,6 +1,7 @@
1
1
  import { getOC } from "./init.js";
2
2
  import { Convert } from "./convert.js";
3
3
  import { Explorer } from "./explorer.js";
4
+ import { Face } from "../common/face.js";
4
5
  import { ShapeFactory } from "../common/shape-factory.js";
5
6
  import { ShapeOps } from "./shape-ops.js";
6
7
  export class ExtrudeOps {
@@ -73,6 +74,20 @@ export class ExtrudeOps {
73
74
  throw new Error("Revolution failed");
74
75
  }
75
76
  const rawResult = revol.Shape();
77
+ // Capture history while the maker is alive. For full revolution the
78
+ // source face is absorbed (IsDeleted), so first/last are meaningless.
79
+ const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
80
+ const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
81
+ const sourceDeleted = revol.IsDeleted(shape.getShape());
82
+ const firstShapeRaw = sourceDeleted ? null : revol.FirstShape();
83
+ const lastShapeRaw = sourceDeleted ? null : revol.LastShape();
84
+ const inputEdgesRaw = Explorer.findShapes(shape.getShape(), EDGE);
85
+ const edgeFacesRaw = [];
86
+ for (const edge of inputEdgesRaw) {
87
+ const generated = ShapeOps.shapeListToArray(revol.Generated(edge))
88
+ .filter(s => s.ShapeType() === FACE);
89
+ edgeFacesRaw.push({ edge, face: generated[0] ?? null });
90
+ }
76
91
  revol.delete();
77
92
  disposeAx1();
78
93
  // A profile face whose normal points "backwards" relative to the axis
@@ -86,7 +101,25 @@ export class ExtrudeOps {
86
101
  oriented = solid;
87
102
  }
88
103
  const clean = ShapeOps.cleanShapeRaw(oriented);
89
- return ShapeFactory.fromShape(clean);
104
+ const cleanedFaceRaws = Explorer.findShapes(clean, FACE);
105
+ const wrappedFaces = cleanedFaceRaws.map(f => Face.fromTopoDSFace(Explorer.toFace(f)));
106
+ const findFace = (raw) => {
107
+ if (!raw) {
108
+ return null;
109
+ }
110
+ for (let i = 0; i < cleanedFaceRaws.length; i++) {
111
+ if (cleanedFaceRaws[i].IsSame(raw)) {
112
+ return wrappedFaces[i];
113
+ }
114
+ }
115
+ return null;
116
+ };
117
+ return {
118
+ solid: ShapeFactory.fromShape(clean),
119
+ firstFace: findFace(firstShapeRaw),
120
+ lastFace: findFace(lastShapeRaw),
121
+ edgeFaces: edgeFacesRaw.map(({ edge, face }) => ({ edge, face: findFace(face) })),
122
+ };
90
123
  }
91
124
  static applyDraftOnSideFaces(solid, firstFace, lastFace, plane, angle) {
92
125
  const oc = getOC();
@@ -9,6 +9,7 @@ export declare class Geometry {
9
9
  static makeArcFromAngle(center: Point, radius: number, normal: Vector3d, start: Point, angle: number): Geom_TrimmedCurve;
10
10
  static makeArcFromTangent(start: Point, end: Point, tangent: Vector3d): Geom_TrimmedCurve;
11
11
  static makeCircle(center: Point, radius: number, normal: Vector3d): Geom_Circle;
12
+ static makeEllipseEdge(center: Point, majorRadius: number, minorRadius: number, normal: Vector3d, majorAxisDir: Vector3d): Edge;
12
13
  static makeBezierCurve(poles: Point[]): Geom_BezierCurve;
13
14
  static makeEdgeFromBezier(curve: Geom_BezierCurve): Edge;
14
15
  static makeEdge(geometry: Geom_TrimmedCurve): Edge;
@@ -140,6 +140,33 @@ export class Geometry {
140
140
  disposeDir();
141
141
  throw new Error('Failed to create circle edge: ' + status);
142
142
  }
143
+ static makeEllipseEdge(center, majorRadius, minorRadius, normal, majorAxisDir) {
144
+ const oc = getOC();
145
+ const [gpCenter, disposeCenter] = Convert.toGpPnt(center);
146
+ const [gpNormal, disposeNormal] = Convert.toGpDir(normal);
147
+ const [gpAxisDir, disposeAxisDir] = Convert.toGpDir(majorAxisDir);
148
+ const ax2 = new oc.gp_Ax2(gpCenter, gpNormal, gpAxisDir);
149
+ const gpEllipse = new oc.gp_Elips(ax2, majorRadius, minorRadius);
150
+ const edgeMaker = new oc.BRepBuilderAPI_MakeEdge(gpEllipse);
151
+ if (edgeMaker.IsDone()) {
152
+ const edge = edgeMaker.Edge();
153
+ edgeMaker.delete();
154
+ gpEllipse.delete();
155
+ ax2.delete();
156
+ disposeAxisDir();
157
+ disposeNormal();
158
+ disposeCenter();
159
+ return Edge.fromTopoDSEdge(edge);
160
+ }
161
+ const status = edgeMaker.Error();
162
+ edgeMaker.delete();
163
+ gpEllipse.delete();
164
+ ax2.delete();
165
+ disposeAxisDir();
166
+ disposeNormal();
167
+ disposeCenter();
168
+ throw new Error('Failed to create ellipse edge: ' + status);
169
+ }
143
170
  static makeBezierCurve(poles) {
144
171
  const oc = getOC();
145
172
  const polesArray = new oc.TColgp_Array1OfPnt(1, poles.length);
@@ -1,4 +1,5 @@
1
1
  import { getOC } from "./init.js";
2
+ import { Explorer } from "./explorer.js";
2
3
  // Flip to false to benchmark single-threaded meshing.
3
4
  const DEFAULT_LIN_DEFLECTION = 0.1;
4
5
  const DEFAULT_ANG_DEFLECTION = 0.5;
@@ -24,6 +25,7 @@ export class Mesh {
24
25
  if (oc.BRepTools.Triangulation(shape, linDefl, checkFreeEdges)) {
25
26
  return false;
26
27
  }
28
+ console.log('Triangulating shape of type', Explorer.getShapeType(shape));
27
29
  const inc = new oc.BRepMesh_IncrementalMesh(shape, linDefl, relative, angDefl, true);
28
30
  inc.delete();
29
31
  return true;
@@ -45,13 +47,13 @@ export class Mesh {
45
47
  const normals = [];
46
48
  const indices = [];
47
49
  const aLocation = new oc.TopLoc_Location();
48
- const myT = oc.BRep_Tool.Triangulation(face, aLocation, 0);
49
- if (myT.IsNull()) {
50
+ const faceTriangulation = oc.BRep_Tool.Triangulation(face, aLocation, 0);
51
+ if (faceTriangulation.IsNull()) {
50
52
  aLocation.delete();
51
53
  return null;
52
54
  }
53
- const pc = new oc.Poly_Connect(myT);
54
- const triangulation = myT.get();
55
+ const pc = new oc.Poly_Connect(faceTriangulation);
56
+ const triangulation = faceTriangulation.get();
55
57
  const nbNodes = triangulation.NbNodes();
56
58
  for (let i = 1; i <= nbNodes; i++) {
57
59
  const t1 = aLocation.Transformation();
@@ -62,11 +64,11 @@ export class Mesh {
62
64
  p1.delete();
63
65
  t1.delete();
64
66
  }
65
- const myNormal = new oc.TColgp_Array1OfDir(1, nbNodes);
66
- oc.StdPrs_ToolTriangulatedShape.Normal(face, pc, myNormal);
67
+ const faceNormals = new oc.TColgp_Array1OfDir(1, nbNodes);
68
+ oc.StdPrs_ToolTriangulatedShape.Normal(face, pc, faceNormals);
67
69
  for (let i = 1; i <= nbNodes; i++) {
68
70
  const t1 = aLocation.Transformation();
69
- const d1 = myNormal.Value(i);
71
+ const d1 = faceNormals.Value(i);
70
72
  const d = d1.Transformed(t1);
71
73
  normals.push(d.X(), d.Y(), d.Z());
72
74
  d1.delete();
@@ -87,9 +89,9 @@ export class Mesh {
87
89
  t.delete();
88
90
  }
89
91
  pc.delete();
90
- myNormal.delete();
92
+ faceNormals.delete();
91
93
  triangles.delete();
92
- myT.delete();
94
+ faceTriangulation.delete();
93
95
  aLocation.delete();
94
96
  return { vertices, normals, indices, count: nbNodes };
95
97
  }
@@ -0,0 +1,16 @@
1
+ import { Plane } from "../math/plane.js";
2
+ import { Point2D } from "../math/point.js";
3
+ import { SceneObject } from "../common/scene-object.js";
4
+ /**
5
+ * Finds the nearest intersection of an oriented "ray" with a target geometry's
6
+ * edges, returning the result in sketch-local 2D coordinates.
7
+ *
8
+ * The ray is modelled as a long bounded segment centered on `start` along
9
+ * `direction`, so hits on either side of `start` are considered. The nearest
10
+ * hit (by absolute signed distance) is returned. Hits that coincide with
11
+ * `start` (within tolerance) are skipped so a start point already on the
12
+ * target is not picked.
13
+ *
14
+ * Throws if the target produces no usable edge hits.
15
+ */
16
+ export declare function findNearestRayIntersection(plane: Plane, start: Point2D, direction: Point2D, target: SceneObject): Point2D;
@@ -0,0 +1,91 @@
1
+ import { getOC } from "./init.js";
2
+ import { Geometry } from "./geometry.js";
3
+ import { Explorer } from "./explorer.js";
4
+ import { Point, Point2D } from "../math/point.js";
5
+ const PROBE_HALF_LENGTH = 1e6;
6
+ const ON_TARGET_EPSILON = 1e-7;
7
+ /**
8
+ * Finds the nearest intersection of an oriented "ray" with a target geometry's
9
+ * edges, returning the result in sketch-local 2D coordinates.
10
+ *
11
+ * The ray is modelled as a long bounded segment centered on `start` along
12
+ * `direction`, so hits on either side of `start` are considered. The nearest
13
+ * hit (by absolute signed distance) is returned. Hits that coincide with
14
+ * `start` (within tolerance) are skipped so a start point already on the
15
+ * target is not picked.
16
+ *
17
+ * Throws if the target produces no usable edge hits.
18
+ */
19
+ export function findNearestRayIntersection(plane, start, direction, target) {
20
+ const oc = getOC();
21
+ const dirLen = Math.hypot(direction.x, direction.y);
22
+ if (dirLen < 1e-12) {
23
+ throw new Error("findNearestRayIntersection: direction vector is zero");
24
+ }
25
+ const dir = new Point2D(direction.x / dirLen, direction.y / dirLen);
26
+ const probeStart2d = new Point2D(start.x - dir.x * PROBE_HALF_LENGTH, start.y - dir.y * PROBE_HALF_LENGTH);
27
+ const probeEnd2d = new Point2D(start.x + dir.x * PROBE_HALF_LENGTH, start.y + dir.y * PROBE_HALF_LENGTH);
28
+ const probeStartWorld = plane.localToWorld(probeStart2d);
29
+ const probeEndWorld = plane.localToWorld(probeEnd2d);
30
+ const probeCurve = Geometry.makeSegment(probeStartWorld, probeEndWorld);
31
+ const probeEdge = Geometry.makeEdgeRaw(probeCurve);
32
+ const targetEdges = [];
33
+ for (const shape of target.getShapes({ excludeGuide: false })) {
34
+ const inner = shape.getShape();
35
+ if (inner.ShapeType() === oc.TopAbs_ShapeEnum.TopAbs_EDGE) {
36
+ targetEdges.push(inner);
37
+ }
38
+ else {
39
+ const edges = Explorer.findShapes(inner, oc.TopAbs_ShapeEnum.TopAbs_EDGE);
40
+ for (const edge of edges) {
41
+ targetEdges.push(edge);
42
+ }
43
+ }
44
+ }
45
+ if (targetEdges.length === 0) {
46
+ probeEdge.delete();
47
+ throw new Error("Target geometry has no edges to intersect with");
48
+ }
49
+ // Probe edge is parameterized 0..2L by arc-length (GC_MakeSegment), with the
50
+ // start at param 0. Recover the world point at a given param via the curve
51
+ // adaptor, then convert to plane-local 2D.
52
+ const probeAdaptor = new oc.BRepAdaptor_Curve(probeEdge);
53
+ let bestHit = null;
54
+ let bestSignedDist = Infinity;
55
+ for (const targetEdge of targetEdges) {
56
+ const tool = new oc.IntTools_EdgeEdge(probeEdge, targetEdge);
57
+ tool.Perform();
58
+ if (!tool.IsDone()) {
59
+ tool.delete();
60
+ continue;
61
+ }
62
+ const parts = tool.CommonParts();
63
+ const partCount = parts.Length();
64
+ for (let i = 1; i <= partCount; i++) {
65
+ const cp = parts.Value(i);
66
+ if (cp.Type() !== oc.TopAbs_ShapeEnum.TopAbs_VERTEX) {
67
+ continue;
68
+ }
69
+ const probeParam = cp.VertexParameter1();
70
+ const gpHit = probeAdaptor.Value(probeParam);
71
+ const hitWorld = new Point(gpHit.X(), gpHit.Y(), gpHit.Z());
72
+ gpHit.delete();
73
+ const hit2d = plane.worldToLocal(hitWorld);
74
+ const signedDist = (hit2d.x - start.x) * dir.x + (hit2d.y - start.y) * dir.y;
75
+ if (Math.abs(signedDist) < ON_TARGET_EPSILON) {
76
+ continue;
77
+ }
78
+ if (Math.abs(signedDist) < Math.abs(bestSignedDist)) {
79
+ bestSignedDist = signedDist;
80
+ bestHit = hit2d;
81
+ }
82
+ }
83
+ tool.delete();
84
+ }
85
+ probeAdaptor.delete();
86
+ probeEdge.delete();
87
+ if (!bestHit) {
88
+ throw new Error("Line does not intersect target geometry");
89
+ }
90
+ return bestHit;
91
+ }
@@ -30,7 +30,6 @@ export declare class ThinFaceMaker {
30
30
  * breaking IsPartner identity between original wire edges and face edges.
31
31
  */
32
32
  private static matchFaceEdgesByMidpoint;
33
- private static makeLineEdge;
34
33
  /**
35
34
  * Creates a closed face from two open wires by capping the ends with straight lines.
36
35
  * wire1 goes A->B, wire2 goes C->D.
@@ -1,4 +1,3 @@
1
- import { Edge } from "../common/edge.js";
2
1
  import { Face } from "../common/face.js";
3
2
  import { Wire } from "../common/wire.js";
4
3
  import { WireOps } from "./wire-ops.js";
@@ -162,23 +161,6 @@ export class ThinFaceMaker {
162
161
  return wireMidpoints.some(mp => feMid.distanceTo(mp) < 1e-4);
163
162
  });
164
163
  }
165
- static makeLineEdge(p1, p2) {
166
- const oc = getOC();
167
- const [gp1, dispose1] = Convert.toGpPnt(p1);
168
- const [gp2, dispose2] = Convert.toGpPnt(p2);
169
- const edgeMaker = new oc.BRepBuilderAPI_MakeEdge(gp1, gp2);
170
- if (!edgeMaker.IsDone()) {
171
- edgeMaker.delete();
172
- dispose1();
173
- dispose2();
174
- throw new Error("Failed to create cap edge for thin extrude");
175
- }
176
- const edge = edgeMaker.Edge();
177
- edgeMaker.delete();
178
- dispose1();
179
- dispose2();
180
- return Edge.fromTopoDSEdge(edge);
181
- }
182
164
  /**
183
165
  * Creates a closed face from two open wires by capping the ends with straight lines.
184
166
  * wire1 goes A->B, wire2 goes C->D.
@@ -189,8 +171,8 @@ export class ThinFaceMaker {
189
171
  const wire2Start = wire2.getFirstVertex().toPoint();
190
172
  const wire2End = wire2.getLastVertex().toPoint();
191
173
  const wire1Start = wire1.getFirstVertex().toPoint();
192
- const cap1 = this.makeLineEdge(wire1End, wire2End);
193
- const cap2 = this.makeLineEdge(wire2Start, wire1Start);
174
+ const cap1 = EdgeOps.makeLineEdge(wire1End, wire2End);
175
+ const cap2 = EdgeOps.makeLineEdge(wire2Start, wire1Start);
194
176
  const reversedWire2 = WireOps.reverseWire(wire2);
195
177
  const allEdges = [
196
178
  ...wire1.getEdges(),
@@ -3,7 +3,8 @@ export declare class SceneRenderer {
3
3
  private readonly meshBuilder;
4
4
  render(scene: Scene): Scene;
5
5
  renderRollback(scene: Scene, rollbackIndex: number): Scene;
6
- private renderObject;
6
+ private prepareRenderedShapes;
7
+ private emitRenderObject;
7
8
  private buildObject;
8
9
  private getOrBuildMeshes;
9
10
  private toRenderedShape;