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
@@ -3,6 +3,7 @@ import { fuseWithSceneObjects, cutWithSceneObjects } from "../helpers/scene-help
3
3
  import { ExtrudeBase } from "./extrude-base.js";
4
4
  import { FaceMaker2 } from "../oc/face-maker2.js";
5
5
  import { BooleanOps } from "../oc/boolean-ops.js";
6
+ import { EdgeOps } from "../oc/edge-ops.js";
6
7
  import { Explorer } from "../oc/explorer.js";
7
8
  import { ExtrudeThroughAll } from "./infinite-extrude.js";
8
9
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
@@ -18,27 +19,46 @@ export class Extrude extends ExtrudeBase {
18
19
  if (pickedFaces !== null && pickedFaces.length === 0) {
19
20
  return;
20
21
  }
21
- const faces = this.isThin()
22
- ? ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1])
23
- : pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
22
+ let faces;
23
+ let inwardEdges;
24
+ let outwardEdges;
25
+ if (this.isThin()) {
26
+ const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
27
+ faces = thinResult.faces;
28
+ inwardEdges = thinResult.inwardEdges;
29
+ outwardEdges = thinResult.outwardEdges;
30
+ }
31
+ else {
32
+ faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
33
+ }
24
34
  if (this._operationMode === 'remove') {
25
35
  this.buildRemove(faces, plane, context);
26
36
  }
27
37
  else if (this._symmetric) {
28
- this.buildSymmetric(faces, plane, context);
38
+ this.buildSymmetric(faces, plane, context, inwardEdges, outwardEdges);
29
39
  }
30
40
  else {
31
- this.buildAdd(faces, plane, context);
41
+ this.buildAdd(faces, plane, context, inwardEdges, outwardEdges);
32
42
  }
33
43
  }
34
- buildAdd(faces, plane, context) {
44
+ buildAdd(faces, plane, context, inwardEdges, outwardEdges) {
35
45
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
36
46
  const extruder = new Extruder(faces, plane, this.distance, this.getDraft(), this.getEndOffset());
37
47
  let extrusions = extruder.extrude();
48
+ let sideFaces = extruder.getSideFaces();
49
+ let internalFaces = extruder.getInternalFaces();
50
+ let capFaces = [];
51
+ if (inwardEdges && inwardEdges.length > 0) {
52
+ const result = this.reclassifyThinFaces([...sideFaces, ...internalFaces], extruder.getStartFaces(), plane, inwardEdges, outwardEdges || []);
53
+ sideFaces = result.sideFaces;
54
+ internalFaces = result.internalFaces;
55
+ capFaces = result.capFaces;
56
+ }
38
57
  this.setState('start-faces', extruder.getStartFaces());
39
58
  this.setState('end-faces', extruder.getEndFaces());
40
- this.setState('side-faces', extruder.getSideFaces());
41
- this.setState('internal-faces', extruder.getInternalFaces());
59
+ this.setState('side-faces', sideFaces);
60
+ this.setState('internal-faces', internalFaces);
61
+ this.setState('cap-faces', capFaces);
42
62
  this.extrudable.removeShapes(this);
43
63
  if (extrusions.length === 0 || sceneObjects.length === 0) {
44
64
  this.addShapes(extrusions);
@@ -53,7 +73,7 @@ export class Extrude extends ExtrudeBase {
53
73
  }
54
74
  this.addShapes(fusionResult.newShapes);
55
75
  }
56
- buildSymmetric(faces, plane, context) {
76
+ buildSymmetric(faces, plane, context, inwardEdges, outwardEdges) {
57
77
  const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
58
78
  const extruder1 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset());
59
79
  const extrusions1 = extruder1.extrude();
@@ -61,34 +81,83 @@ export class Extrude extends ExtrudeBase {
61
81
  const extruder2 = new Extruder(faces, plane, -this.distance / 2, this.getDraft(), this.getEndOffset());
62
82
  const extrusions2 = extruder2.extrude();
63
83
  const endFaces = extruder2.getEndFaces();
64
- const preFusionInternalFaces = [
65
- ...extruder1.getInternalFaces(),
66
- ...extruder2.getInternalFaces(),
67
- ];
68
84
  const all = [...extrusions1, ...extrusions2];
69
85
  const { result: extrusions } = BooleanOps.fuse(all);
70
- const sideFaces = [];
71
- const internalFaces = [];
86
+ // Collect remaining faces and fused start/end faces from the fused solid.
87
+ // We need the fused face objects (not pre-fusion) for IsPartner matching.
88
+ const remainingFaces = [];
89
+ const fusedStartFaces = [];
90
+ const fusedEndFaces = [];
72
91
  for (const solid of extrusions) {
73
92
  const allFaces = Explorer.findFacesWrapped(solid);
74
93
  for (const f of allFaces) {
75
94
  const isStart = startFaces.some(sf => f.getShape().IsSame(sf.getShape()));
76
95
  const isEnd = endFaces.some(ef => f.getShape().IsSame(ef.getShape()));
77
- if (!isStart && !isEnd) {
78
- const isInternal = preFusionInternalFaces.some(pf => f.getShape().IsSame(pf.getShape()));
79
- if (isInternal) {
80
- internalFaces.push(f);
96
+ if (isStart) {
97
+ fusedStartFaces.push(f);
98
+ }
99
+ else if (isEnd) {
100
+ fusedEndFaces.push(f);
101
+ }
102
+ else {
103
+ remainingFaces.push(f);
104
+ }
105
+ }
106
+ }
107
+ let sideFaces;
108
+ let internalFaces;
109
+ let capFaces = [];
110
+ if (inwardEdges && inwardEdges.length > 0) {
111
+ // For thin open profiles: reclassify using 2D midpoint matching on the fused solid
112
+ const result = this.reclassifyThinFaces(remainingFaces, [...fusedStartFaces, ...fusedEndFaces], plane, inwardEdges, outwardEdges || []);
113
+ sideFaces = result.sideFaces;
114
+ internalFaces = result.internalFaces;
115
+ capFaces = result.capFaces;
116
+ }
117
+ else {
118
+ // Detect inner wire edges from the extruder's firstFaces (at the sketch plane,
119
+ // pre-fusion). These have the same wire orientation the Extruder uses internally.
120
+ // Then map to fused solid edges using 2D midpoint matching, since SimplifyResult
121
+ // merges half-faces and breaks TShape identity.
122
+ const preInnerEdges = [];
123
+ for (const sf of extruder1.getStartFaces()) {
124
+ for (const wire of sf.getWires()) {
125
+ if (!wire.isCW(plane.normal)) {
126
+ for (const edge of wire.getEdges()) {
127
+ preInnerEdges.push(edge);
128
+ }
81
129
  }
82
- else {
83
- sideFaces.push(f);
130
+ }
131
+ }
132
+ const fusedInnerEdges = [];
133
+ if (preInnerEdges.length > 0) {
134
+ const innerMids = preInnerEdges.map(e => plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(e.getShape())));
135
+ for (const sf of fusedStartFaces) {
136
+ for (const sfe of sf.getEdges()) {
137
+ const mid = plane.worldToLocal(EdgeOps.getEdgeMidPointRaw(sfe.getShape()));
138
+ if (innerMids.some(im => mid.distanceTo(im) < 1e-4)) {
139
+ fusedInnerEdges.push(sfe);
140
+ }
84
141
  }
85
142
  }
86
143
  }
144
+ sideFaces = [];
145
+ internalFaces = [];
146
+ for (const f of remainingFaces) {
147
+ const isInternal = fusedInnerEdges.length > 0 && f.getEdges().some(fe => fusedInnerEdges.some(ie => fe.getShape().IsPartner(ie.getShape())));
148
+ if (isInternal) {
149
+ internalFaces.push(f);
150
+ }
151
+ else {
152
+ sideFaces.push(f);
153
+ }
154
+ }
87
155
  }
88
156
  this.setState('start-faces', startFaces);
89
157
  this.setState('end-faces', endFaces);
90
158
  this.setState('side-faces', sideFaces);
91
159
  this.setState('internal-faces', internalFaces);
160
+ this.setState('cap-faces', capFaces);
92
161
  this.extrudable.removeShapes(this);
93
162
  if (extrusions.length === 0 || sceneObjects.length === 0) {
94
163
  this.addShapes(extrusions);
@@ -9,6 +9,7 @@ export declare class MirrorShape2D extends GeometrySceneObject {
9
9
  build(context: BuildSceneObjectContext): void;
10
10
  start(): LazyVertex;
11
11
  end(): LazyVertex;
12
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
12
13
  compareTo(other: MirrorShape2D): boolean;
13
14
  getType(): string;
14
15
  getUniqueType(): string;
@@ -74,6 +74,13 @@ export class MirrorShape2D extends GeometrySceneObject {
74
74
  end() {
75
75
  return new LazyVertex(this.generateUniqueName('end-vertex'), () => [this.getState('end')]);
76
76
  }
77
+ createCopy(remap) {
78
+ const axis = remap.get(this.axis) || this.axis;
79
+ const targetObjects = this.targetObjects
80
+ ? this.targetObjects.map(obj => remap.get(obj) || obj)
81
+ : null;
82
+ return new MirrorShape2D(axis, targetObjects);
83
+ }
77
84
  compareTo(other) {
78
85
  if (!(other instanceof MirrorShape2D)) {
79
86
  return false;
@@ -7,7 +7,7 @@ export class Remove extends SceneObject {
7
7
  }
8
8
  build() {
9
9
  for (const obj of this.objects) {
10
- obj.removeShapes(this);
10
+ obj.removeShapes(this, true);
11
11
  }
12
12
  }
13
13
  compareTo(other) {
@@ -3,7 +3,14 @@ import { Shape } from "../common/shapes.js";
3
3
  import { FilterBase } from "./filter-base.js";
4
4
  export declare class FilterBuilderBase<TShape extends Shape = Shape> {
5
5
  protected filters: FilterBase<TShape>[];
6
+ protected _withTangents: boolean;
6
7
  filter(filter: FilterBase<TShape>): this;
8
+ /**
9
+ * Expands the selection to include all shapes transitively connected
10
+ * by tangency (G1 continuity) to the initially matched shapes.
11
+ */
12
+ withTangents(): this;
13
+ hasTangentExpansion(): boolean;
7
14
  getFilters(): FilterBase<TShape>[];
8
15
  transform(matrix: Matrix4): FilterBuilderBase<TShape>;
9
16
  equals(other: FilterBuilderBase<TShape>): boolean;
@@ -1,9 +1,21 @@
1
1
  export class FilterBuilderBase {
2
2
  filters = [];
3
+ _withTangents = false;
3
4
  filter(filter) {
4
5
  this.filters.push(filter);
5
6
  return this;
6
7
  }
8
+ /**
9
+ * Expands the selection to include all shapes transitively connected
10
+ * by tangency (G1 continuity) to the initially matched shapes.
11
+ */
12
+ withTangents() {
13
+ this._withTangents = true;
14
+ return this;
15
+ }
16
+ hasTangentExpansion() {
17
+ return this._withTangents;
18
+ }
7
19
  getFilters() {
8
20
  return this.filters;
9
21
  }
@@ -12,9 +24,13 @@ export class FilterBuilderBase {
12
24
  for (const filter of this.filters) {
13
25
  transformedBuilder.filter(filter.transform(matrix));
14
26
  }
27
+ transformedBuilder._withTangents = this._withTangents;
15
28
  return transformedBuilder;
16
29
  }
17
30
  equals(other) {
31
+ if (this._withTangents !== other._withTangents) {
32
+ return false;
33
+ }
18
34
  if (this.filters.length !== other.filters.length) {
19
35
  return false;
20
36
  }
@@ -1,3 +1,4 @@
1
+ import { TangentExpander } from "./tangent-expander.js";
1
2
  export class ShapeFilter {
2
3
  shapes;
3
4
  builders;
@@ -28,6 +29,11 @@ export class ShapeFilter {
28
29
  }
29
30
  }
30
31
  }
32
+ // Tangent expansion: if any builder requests it, expand result set via BFS
33
+ const needsExpansion = this.builders.some(b => b.hasTangentExpansion());
34
+ if (needsExpansion && result.length > 0) {
35
+ return TangentExpander.expand(result, this.shapes);
36
+ }
31
37
  return result;
32
38
  }
33
39
  }
@@ -0,0 +1,47 @@
1
+ import type { TopoDS_Edge } from "occjs-wrapper";
2
+ import { Shape, Edge, Face } from "../common/shapes.js";
3
+ export declare class TangentExpander {
4
+ /**
5
+ * Expands a set of seed shapes to include all transitively tangent-connected
6
+ * shapes from the pool. Dispatches to edge or face expansion based on shape type.
7
+ */
8
+ static expand(seeds: Shape[], pool: Shape[]): Shape[];
9
+ /**
10
+ * BFS expansion for edges. Two edges are tangent if they share a vertex
11
+ * and their tangent vectors at that vertex are parallel.
12
+ */
13
+ private static expandEdges;
14
+ /**
15
+ * BFS expansion for faces. Two faces are tangent if they share an edge
16
+ * and have G1 or higher continuity across that edge.
17
+ */
18
+ private static expandFaces;
19
+ /**
20
+ * Checks if two edges are tangent at a shared vertex by comparing
21
+ * their tangent vectors (parallel = tangent).
22
+ */
23
+ static areEdgesTangent(e1: Edge, e2: Edge): boolean;
24
+ /**
25
+ * Checks if two faces are tangent along a shared edge.
26
+ * Uses BRepLib.ContinuityOfFaces which computes the continuity from geometry
27
+ * on-the-fly (unlike BRep_Tool.Continuity which reads stored flags).
28
+ */
29
+ static areFacesTangent(f1: Face, f2: Face, sharedEdge: TopoDS_Edge): boolean;
30
+ /**
31
+ * Builds a map from vertex TShape hash to the list of edges that share that vertex.
32
+ */
33
+ private static buildVertexToEdgeMap;
34
+ /**
35
+ * Builds a map from edge TShape hash to the list of faces that share that edge.
36
+ */
37
+ private static buildEdgeToFaceMap;
38
+ /**
39
+ * Returns edges from the adjacency map that share a vertex with the given edge.
40
+ */
41
+ private static getAdjacentEdges;
42
+ /**
43
+ * Returns faces from the adjacency map that share an edge with the given face,
44
+ * along with the shared edge's raw TopoDS_Edge.
45
+ */
46
+ private static getAdjacentFaces;
47
+ }
@@ -0,0 +1,223 @@
1
+ import { getOC } from "../oc/init.js";
2
+ export class TangentExpander {
3
+ /**
4
+ * Expands a set of seed shapes to include all transitively tangent-connected
5
+ * shapes from the pool. Dispatches to edge or face expansion based on shape type.
6
+ */
7
+ static expand(seeds, pool) {
8
+ if (seeds.length === 0) {
9
+ return [];
10
+ }
11
+ if (seeds[0].isEdge()) {
12
+ return TangentExpander.expandEdges(seeds, pool);
13
+ }
14
+ else {
15
+ return TangentExpander.expandFaces(seeds, pool);
16
+ }
17
+ }
18
+ /**
19
+ * BFS expansion for edges. Two edges are tangent if they share a vertex
20
+ * and their tangent vectors at that vertex are parallel.
21
+ */
22
+ static expandEdges(seeds, pool) {
23
+ // Build vertex → edge adjacency map for efficient lookup
24
+ const vertexToEdges = TangentExpander.buildVertexToEdgeMap(pool);
25
+ const included = new Set(seeds);
26
+ const queue = [...seeds];
27
+ while (queue.length > 0) {
28
+ const current = queue.shift();
29
+ const neighbors = TangentExpander.getAdjacentEdges(current, vertexToEdges);
30
+ for (const candidate of neighbors) {
31
+ if (included.has(candidate)) {
32
+ continue;
33
+ }
34
+ if (TangentExpander.areEdgesTangent(current, candidate)) {
35
+ included.add(candidate);
36
+ queue.push(candidate);
37
+ }
38
+ }
39
+ }
40
+ return [...included];
41
+ }
42
+ /**
43
+ * BFS expansion for faces. Two faces are tangent if they share an edge
44
+ * and have G1 or higher continuity across that edge.
45
+ */
46
+ static expandFaces(seeds, pool) {
47
+ // Build edge → face adjacency map
48
+ const edgeToFaces = TangentExpander.buildEdgeToFaceMap(pool);
49
+ const included = new Set(seeds);
50
+ const queue = [...seeds];
51
+ while (queue.length > 0) {
52
+ const current = queue.shift();
53
+ const neighbors = TangentExpander.getAdjacentFaces(current, edgeToFaces);
54
+ for (const [candidate, sharedEdge] of neighbors) {
55
+ if (included.has(candidate)) {
56
+ continue;
57
+ }
58
+ if (TangentExpander.areFacesTangent(current, candidate, sharedEdge)) {
59
+ included.add(candidate);
60
+ queue.push(candidate);
61
+ }
62
+ }
63
+ }
64
+ return [...included];
65
+ }
66
+ /**
67
+ * Checks if two edges are tangent at a shared vertex by comparing
68
+ * their tangent vectors (parallel = tangent).
69
+ */
70
+ static areEdgesTangent(e1, e2) {
71
+ const oc = getOC();
72
+ const angTol = 1e-4;
73
+ const e1Raw = e1.getShape();
74
+ const e2Raw = e2.getShape();
75
+ const e1First = oc.TopExp.FirstVertex(e1Raw, true);
76
+ const e1Last = oc.TopExp.LastVertex(e1Raw, true);
77
+ const e2First = oc.TopExp.FirstVertex(e2Raw, true);
78
+ const e2Last = oc.TopExp.LastVertex(e2Raw, true);
79
+ // Check all 4 vertex pairings for a shared vertex
80
+ const pairs = [
81
+ { v1: e1First, v2: e2First },
82
+ { v1: e1First, v2: e2Last },
83
+ { v1: e1Last, v2: e2First },
84
+ { v1: e1Last, v2: e2Last },
85
+ ];
86
+ for (const { v1, v2 } of pairs) {
87
+ if (!v1.IsPartner(v2)) {
88
+ continue;
89
+ }
90
+ // Get raw curves and parameters at the shared vertex
91
+ const curve1Handle = oc.BRep_Tool.Curve(e1Raw, 0, 1);
92
+ const curve1 = curve1Handle.get();
93
+ const param1 = oc.BRep_Tool.Parameter(v1, e1Raw);
94
+ const curve2Handle = oc.BRep_Tool.Curve(e2Raw, 0, 1);
95
+ const curve2 = curve2Handle.get();
96
+ const param2 = oc.BRep_Tool.Parameter(v2, e2Raw);
97
+ // Evaluate tangent vectors using D1
98
+ const pnt1 = new oc.gp_Pnt();
99
+ const vec1 = new oc.gp_Vec();
100
+ curve1.D1(param1, pnt1, vec1);
101
+ const pnt2 = new oc.gp_Pnt();
102
+ const vec2 = new oc.gp_Vec();
103
+ curve2.D1(param2, pnt2, vec2);
104
+ // Flip tangent if edge is reversed
105
+ if (e1Raw.Orientation() === oc.TopAbs_Orientation.TopAbs_REVERSED) {
106
+ vec1.Reverse();
107
+ }
108
+ if (e2Raw.Orientation() === oc.TopAbs_Orientation.TopAbs_REVERSED) {
109
+ vec2.Reverse();
110
+ }
111
+ // Check parallelism: |cos(angle)| > cos(angTol)
112
+ const mag1 = vec1.Magnitude();
113
+ const mag2 = vec2.Magnitude();
114
+ let tangent = false;
115
+ if (mag1 > 1e-10 && mag2 > 1e-10) {
116
+ const dot = Math.abs(vec1.Normalized().Dot(vec2.Normalized()));
117
+ tangent = dot > Math.cos(angTol);
118
+ }
119
+ // Cleanup
120
+ pnt1.delete();
121
+ vec1.delete();
122
+ pnt2.delete();
123
+ vec2.delete();
124
+ if (tangent) {
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ /**
131
+ * Checks if two faces are tangent along a shared edge.
132
+ * Uses BRepLib.ContinuityOfFaces which computes the continuity from geometry
133
+ * on-the-fly (unlike BRep_Tool.Continuity which reads stored flags).
134
+ */
135
+ static areFacesTangent(f1, f2, sharedEdge) {
136
+ const oc = getOC();
137
+ const f1Raw = f1.getShape();
138
+ const f2Raw = f2.getShape();
139
+ const angTol = 0.01; // ~0.57 degrees
140
+ const continuity = oc.BRepLib.ContinuityOfFaces(sharedEdge, f1Raw, f2Raw, angTol);
141
+ // C0 = sharp edge (not tangent). Anything else (G1, C1, G2, C2, C3, CN) = tangent.
142
+ return continuity !== oc.GeomAbs_Shape.GeomAbs_C0;
143
+ }
144
+ /**
145
+ * Builds a map from vertex TShape hash to the list of edges that share that vertex.
146
+ */
147
+ static buildVertexToEdgeMap(edges) {
148
+ const oc = getOC();
149
+ const map = new Map();
150
+ for (const edge of edges) {
151
+ const raw = edge.getShape();
152
+ const first = oc.TopExp.FirstVertex(raw, true);
153
+ const last = oc.TopExp.LastVertex(raw, true);
154
+ for (const vertex of [first, last]) {
155
+ const hash = vertex.HashCode(2147483647);
156
+ if (!map.has(hash)) {
157
+ map.set(hash, []);
158
+ }
159
+ map.get(hash).push(edge);
160
+ }
161
+ }
162
+ return map;
163
+ }
164
+ /**
165
+ * Builds a map from edge TShape hash to the list of faces that share that edge.
166
+ */
167
+ static buildEdgeToFaceMap(faces) {
168
+ const map = new Map();
169
+ for (const face of faces) {
170
+ const faceEdges = face.getEdges();
171
+ for (const edge of faceEdges) {
172
+ const hash = edge.getShape().HashCode(2147483647);
173
+ if (!map.has(hash)) {
174
+ map.set(hash, []);
175
+ }
176
+ map.get(hash).push(face);
177
+ }
178
+ }
179
+ return map;
180
+ }
181
+ /**
182
+ * Returns edges from the adjacency map that share a vertex with the given edge.
183
+ */
184
+ static getAdjacentEdges(edge, vertexToEdges) {
185
+ const oc = getOC();
186
+ const raw = edge.getShape();
187
+ const first = oc.TopExp.FirstVertex(raw, true);
188
+ const last = oc.TopExp.LastVertex(raw, true);
189
+ const neighbors = new Set();
190
+ for (const vertex of [first, last]) {
191
+ const hash = vertex.HashCode(2147483647);
192
+ const candidates = vertexToEdges.get(hash);
193
+ if (candidates) {
194
+ for (const c of candidates) {
195
+ if (c !== edge) {
196
+ neighbors.add(c);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return [...neighbors];
202
+ }
203
+ /**
204
+ * Returns faces from the adjacency map that share an edge with the given face,
205
+ * along with the shared edge's raw TopoDS_Edge.
206
+ */
207
+ static getAdjacentFaces(face, edgeToFaces) {
208
+ const result = [];
209
+ const faceEdges = face.getEdges();
210
+ for (const edge of faceEdges) {
211
+ const hash = edge.getShape().HashCode(2147483647);
212
+ const candidates = edgeToFaces.get(hash);
213
+ if (candidates) {
214
+ for (const c of candidates) {
215
+ if (c !== face) {
216
+ result.push([c, edge.getShape()]);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return result;
222
+ }
223
+ }
package/lib/dist/index.js CHANGED
@@ -9,8 +9,13 @@ export function captureSourceLocation() {
9
9
  for (const frame of lines) {
10
10
  const match = frame.match(/((?:[A-Za-z]:)?\/[^\s]+?\.fluid\.js):(\d+):(\d+)/);
11
11
  if (match) {
12
+ let filePath = match[1];
13
+ const virtualIdx = filePath.indexOf('virtual:live-render:');
14
+ if (virtualIdx !== -1) {
15
+ filePath = filePath.substring(virtualIdx + 'virtual:live-render:'.length);
16
+ }
12
17
  return {
13
- filePath: match[1],
18
+ filePath,
14
19
  line: parseInt(match[2], 10),
15
20
  column: parseInt(match[3], 10),
16
21
  };
@@ -2,8 +2,13 @@ import { Edge } from "../common/edge.js";
2
2
  import { Face } from "../common/face.js";
3
3
  import { Wire } from "../common/wire.js";
4
4
  import { Plane } from "../math/plane.js";
5
+ export interface ThinFaceResult {
6
+ faces: Face[];
7
+ inwardEdges: Edge[];
8
+ outwardEdges: Edge[];
9
+ }
5
10
  export declare class ThinFaceMaker {
6
- static make(edges: (Wire | Edge)[], plane: Plane, offset1: number, offset2?: number): Face[];
11
+ static make(edges: (Wire | Edge)[], plane: Plane, offset1: number, offset2?: number): ThinFaceResult;
7
12
  private static makeSingleOffsetFace;
8
13
  private static makeDualOffsetFace;
9
14
  /**
@@ -19,6 +24,12 @@ export declare class ThinFaceMaker {
19
24
  * Only handles positive distances — use doOffset for sign handling.
20
25
  */
21
26
  private static offsetWireOnPlane;
27
+ /**
28
+ * Finds face edges that geometrically match the given wire edges by comparing midpoints.
29
+ * This is needed because wire reversal (ShapeExtend_WireData.Reverse) creates new TShapes,
30
+ * breaking IsPartner identity between original wire edges and face edges.
31
+ */
32
+ private static matchFaceEdgesByMidpoint;
22
33
  private static makeLineEdge;
23
34
  /**
24
35
  * Creates a closed face from two open wires by capping the ends with straight lines.