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.
- package/bin/fluidcad.js +2 -2
- package/lib/dist/common/scene-object.d.ts +4 -1
- package/lib/dist/common/scene-object.js +14 -3
- package/lib/dist/core/interfaces.d.ts +18 -0
- package/lib/dist/core/part.js +1 -1
- package/lib/dist/core/repeat.js +2 -1
- package/lib/dist/features/2d/aline.d.ts +2 -0
- package/lib/dist/features/2d/aline.js +4 -0
- package/lib/dist/features/axis.d.ts +2 -0
- package/lib/dist/features/axis.js +3 -0
- package/lib/dist/features/color.d.ts +1 -0
- package/lib/dist/features/color.js +4 -0
- package/lib/dist/features/extrude-base.d.ts +13 -0
- package/lib/dist/features/extrude-base.js +64 -0
- package/lib/dist/features/extrude-to-face.js +28 -7
- package/lib/dist/features/extrude-two-distances.js +72 -17
- package/lib/dist/features/extrude.js +90 -21
- package/lib/dist/features/mirror-shape2d.d.ts +1 -0
- package/lib/dist/features/mirror-shape2d.js +7 -0
- package/lib/dist/features/remove.js +1 -1
- package/lib/dist/filters/filter-builder-base.d.ts +7 -0
- package/lib/dist/filters/filter-builder-base.js +16 -0
- package/lib/dist/filters/filter.js +6 -0
- package/lib/dist/filters/tangent-expander.d.ts +47 -0
- package/lib/dist/filters/tangent-expander.js +223 -0
- package/lib/dist/index.js +6 -1
- package/lib/dist/oc/thin-face-maker.d.ts +12 -1
- package/lib/dist/oc/thin-face-maker.js +54 -13
- package/lib/dist/rendering/scene.js +5 -4
- package/lib/dist/tests/features/extrude-two-distances.test.js +16 -0
- package/lib/dist/tests/features/repeat-circular.test.js +12 -0
- package/lib/dist/tests/features/repeat-linear.test.js +22 -0
- package/lib/dist/tests/features/select.test.js +55 -0
- package/lib/dist/tests/features/thin-extrude.test.js +61 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/ui/dist/assets/{index-BJG141m7.js → index-CLQhpG6A.js} +1 -1
- package/ui/dist/index.html +1 -1
|
@@ -3,6 +3,7 @@ import { Face } from "../common/face.js";
|
|
|
3
3
|
import { Wire } from "../common/wire.js";
|
|
4
4
|
import { WireOps } from "./wire-ops.js";
|
|
5
5
|
import { FaceOps } from "./face-ops.js";
|
|
6
|
+
import { EdgeOps } from "./edge-ops.js";
|
|
6
7
|
import { Explorer } from "./explorer.js";
|
|
7
8
|
import { Convert } from "./convert.js";
|
|
8
9
|
import { getOC } from "./init.js";
|
|
@@ -21,17 +22,25 @@ export class ThinFaceMaker {
|
|
|
21
22
|
}
|
|
22
23
|
const groups = WireOps.groupConnectedEdges(edgesOnly);
|
|
23
24
|
const faces = [];
|
|
25
|
+
const inwardEdges = [];
|
|
26
|
+
const outwardEdges = [];
|
|
24
27
|
for (const group of groups) {
|
|
25
28
|
const wire = WireOps.makeWireFromEdges(group);
|
|
26
29
|
const isClosed = wire.isClosed();
|
|
27
30
|
if (offset2 !== undefined) {
|
|
28
|
-
|
|
31
|
+
const result = this.makeDualOffsetFace(wire, isClosed, plane, offset1, offset2);
|
|
32
|
+
faces.push(result.face);
|
|
33
|
+
inwardEdges.push(...result.inwardEdges);
|
|
34
|
+
outwardEdges.push(...result.outwardEdges);
|
|
29
35
|
}
|
|
30
36
|
else {
|
|
31
|
-
|
|
37
|
+
const result = this.makeSingleOffsetFace(wire, isClosed, plane, offset1);
|
|
38
|
+
faces.push(result.face);
|
|
39
|
+
inwardEdges.push(...result.inwardEdges);
|
|
40
|
+
outwardEdges.push(...result.outwardEdges);
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
|
-
return faces;
|
|
43
|
+
return { faces, inwardEdges, outwardEdges };
|
|
35
44
|
}
|
|
36
45
|
static makeSingleOffsetFace(wire, isClosed, plane, offset) {
|
|
37
46
|
const offsetWire = this.doOffset(wire, plane, offset, isClosed);
|
|
@@ -41,12 +50,22 @@ export class ThinFaceMaker {
|
|
|
41
50
|
? [offsetWire, wire]
|
|
42
51
|
: [wire, offsetWire];
|
|
43
52
|
const reversedInner = WireOps.reverseWire(inner);
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
return {
|
|
54
|
+
face: Face.fromTopoDSFace(FaceOps.makeFaceFromWires([
|
|
55
|
+
outer.getShape(),
|
|
56
|
+
reversedInner.getShape(),
|
|
57
|
+
])),
|
|
58
|
+
inwardEdges: [],
|
|
59
|
+
outwardEdges: [],
|
|
60
|
+
};
|
|
48
61
|
}
|
|
49
|
-
|
|
62
|
+
// For open profiles: positive offset goes inward (toward profile interior)
|
|
63
|
+
const inwardWireEdges = offset >= 0 ? offsetWire.getEdges() : wire.getEdges();
|
|
64
|
+
const outwardWireEdges = offset >= 0 ? wire.getEdges() : offsetWire.getEdges();
|
|
65
|
+
const face = this.makeOpenFaceWithCaps(wire, offsetWire);
|
|
66
|
+
const inwardEdges = this.matchFaceEdgesByMidpoint(face, inwardWireEdges);
|
|
67
|
+
const outwardEdges = this.matchFaceEdgesByMidpoint(face, outwardWireEdges);
|
|
68
|
+
return { face, inwardEdges, outwardEdges };
|
|
50
69
|
}
|
|
51
70
|
static makeDualOffsetFace(wire, isClosed, plane, offset1, offset2) {
|
|
52
71
|
// Ensure offset2 goes in the opposite direction of offset1
|
|
@@ -61,12 +80,22 @@ export class ThinFaceMaker {
|
|
|
61
80
|
? [wire1, wire2]
|
|
62
81
|
: [wire2, wire1];
|
|
63
82
|
const reversedInner = WireOps.reverseWire(inner);
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
return {
|
|
84
|
+
face: Face.fromTopoDSFace(FaceOps.makeFaceFromWires([
|
|
85
|
+
outer.getShape(),
|
|
86
|
+
reversedInner.getShape(),
|
|
87
|
+
])),
|
|
88
|
+
inwardEdges: [],
|
|
89
|
+
outwardEdges: [],
|
|
90
|
+
};
|
|
68
91
|
}
|
|
69
|
-
|
|
92
|
+
// For open profiles: positive offset goes inward (toward profile interior)
|
|
93
|
+
const inwardWireEdges = offset1 > 0 ? wire1.getEdges() : wire2.getEdges();
|
|
94
|
+
const outwardWireEdges = offset1 > 0 ? wire2.getEdges() : wire1.getEdges();
|
|
95
|
+
const face = this.makeOpenFaceWithCaps(wire1, wire2);
|
|
96
|
+
const inwardEdges = this.matchFaceEdgesByMidpoint(face, inwardWireEdges);
|
|
97
|
+
const outwardEdges = this.matchFaceEdgesByMidpoint(face, outwardWireEdges);
|
|
98
|
+
return { face, inwardEdges, outwardEdges };
|
|
70
99
|
}
|
|
71
100
|
/**
|
|
72
101
|
* Offsets a wire by the given distance, handling both closed and open wires.
|
|
@@ -121,6 +150,18 @@ export class ThinFaceMaker {
|
|
|
121
150
|
}
|
|
122
151
|
return Wire.fromTopoDSWire(oc.TopoDS.Wire(wires[0]));
|
|
123
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Finds face edges that geometrically match the given wire edges by comparing midpoints.
|
|
155
|
+
* This is needed because wire reversal (ShapeExtend_WireData.Reverse) creates new TShapes,
|
|
156
|
+
* breaking IsPartner identity between original wire edges and face edges.
|
|
157
|
+
*/
|
|
158
|
+
static matchFaceEdgesByMidpoint(face, wireEdges) {
|
|
159
|
+
const wireMidpoints = wireEdges.map(we => EdgeOps.getEdgeMidPointRaw(we.getShape()));
|
|
160
|
+
return face.getEdges().filter(fe => {
|
|
161
|
+
const feMid = EdgeOps.getEdgeMidPointRaw(fe.getShape());
|
|
162
|
+
return wireMidpoints.some(mp => feMid.distanceTo(mp) < 1e-4);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
124
165
|
static makeLineEdge(p1, p2) {
|
|
125
166
|
const oc = getOC();
|
|
126
167
|
const [gp1, dispose1] = Convert.toGpPnt(p1);
|
|
@@ -10,15 +10,16 @@ export class Scene {
|
|
|
10
10
|
constructor() {
|
|
11
11
|
}
|
|
12
12
|
addSceneObject(obj) {
|
|
13
|
+
if (this.sceneObjects.includes(obj)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
13
16
|
obj.setOrder(this.sceneObjects.length);
|
|
14
17
|
const activeObj = this.getActiveContainer();
|
|
15
18
|
if (activeObj && !obj.getParent()) {
|
|
16
19
|
activeObj.addChildObject(obj);
|
|
17
20
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.idMap.set(obj.id, obj);
|
|
21
|
-
}
|
|
21
|
+
this.sceneObjects.push(obj);
|
|
22
|
+
this.idMap.set(obj.id, obj);
|
|
22
23
|
}
|
|
23
24
|
startProgressiveContainer(obj) {
|
|
24
25
|
this.addSceneObject(obj);
|
|
@@ -213,6 +213,22 @@ describe("extrude two distances", () => {
|
|
|
213
213
|
expect(bbox.maxX - bbox.minX).toBeGreaterThan(100);
|
|
214
214
|
expect(bbox.maxY - bbox.minY).toBeGreaterThan(50);
|
|
215
215
|
});
|
|
216
|
+
it("should apply different draft angles per direction", () => {
|
|
217
|
+
sketch("xy", () => {
|
|
218
|
+
rect(100, 50);
|
|
219
|
+
});
|
|
220
|
+
const e = extrude(20, 20).draft([8, 2]);
|
|
221
|
+
render();
|
|
222
|
+
const startFaces = e.getState('start-faces');
|
|
223
|
+
const endFaces = e.getState('end-faces');
|
|
224
|
+
const startBbox = ShapeOps.getBoundingBox(startFaces[0].getShape());
|
|
225
|
+
const endBbox = ShapeOps.getBoundingBox(endFaces[0].getShape());
|
|
226
|
+
const startWidth = startBbox.maxX - startBbox.minX;
|
|
227
|
+
const endWidth = endBbox.maxX - endBbox.minX;
|
|
228
|
+
// Start face (distance1=20, draft=8°) should be wider than
|
|
229
|
+
// end face (distance2=20, draft=2°) due to greater taper
|
|
230
|
+
expect(startWidth).toBeGreaterThan(endWidth);
|
|
231
|
+
});
|
|
216
232
|
});
|
|
217
233
|
describe("drill", () => {
|
|
218
234
|
it("should drill hole when inner shape is nested (default)", () => {
|
|
@@ -40,6 +40,18 @@ describe("repeat circular", () => {
|
|
|
40
40
|
// Original (1) + 2 repeated = 3
|
|
41
41
|
expect(countShapes(scene)).toBe(3);
|
|
42
42
|
});
|
|
43
|
+
it("should evenly space 3 instances around full circle", () => {
|
|
44
|
+
sketch("xy", () => {
|
|
45
|
+
move([50, 0]);
|
|
46
|
+
rect(20, 20);
|
|
47
|
+
});
|
|
48
|
+
const e = extrude(10).new();
|
|
49
|
+
// count: 3, angle: 360 → 120° apart
|
|
50
|
+
repeat("circular", "z", { count: 3, angle: 360 }, e);
|
|
51
|
+
const scene = render();
|
|
52
|
+
// Original (1) + 2 repeated = 3
|
|
53
|
+
expect(countShapes(scene)).toBe(3);
|
|
54
|
+
});
|
|
43
55
|
it("should use explicit offset between instances", () => {
|
|
44
56
|
sketch("xy", () => {
|
|
45
57
|
move([50, 0]);
|
|
@@ -89,6 +89,28 @@ describe("repeat linear", () => {
|
|
|
89
89
|
// Original (1) + 4 repeated = 5
|
|
90
90
|
expect(countShapes(scene)).toBe(5);
|
|
91
91
|
});
|
|
92
|
+
it("should center correctly with count 3", () => {
|
|
93
|
+
sketch("xy", () => {
|
|
94
|
+
rect(20, 20);
|
|
95
|
+
});
|
|
96
|
+
const e = extrude(10).new();
|
|
97
|
+
// count: 3, centered → one clone on each side of the original
|
|
98
|
+
repeat("linear", "x", { count: 3, offset: 25, centered: true }, e);
|
|
99
|
+
const scene = render();
|
|
100
|
+
// Original (1) + 2 repeated = 3
|
|
101
|
+
expect(countShapes(scene)).toBe(3);
|
|
102
|
+
});
|
|
103
|
+
it("should center correctly in a multi-axis grid", () => {
|
|
104
|
+
sketch("xy", () => {
|
|
105
|
+
rect(10, 10);
|
|
106
|
+
});
|
|
107
|
+
const e = extrude(10).new();
|
|
108
|
+
// 3×3 centered grid → 9 positions, 1 is the center (original) = 8 clones
|
|
109
|
+
repeat("linear", ["x", "y"], { count: [3, 3], offset: 30, centered: true }, e);
|
|
110
|
+
const scene = render();
|
|
111
|
+
// Original (1) + 8 repeated = 9
|
|
112
|
+
expect(countShapes(scene)).toBe(9);
|
|
113
|
+
});
|
|
92
114
|
it("should skip specified index", () => {
|
|
93
115
|
sketch("xy", () => {
|
|
94
116
|
rect(20, 20);
|
|
@@ -631,4 +631,59 @@ describe("select", () => {
|
|
|
631
631
|
expect(sel.getShapes()).toHaveLength(1);
|
|
632
632
|
});
|
|
633
633
|
});
|
|
634
|
+
describe("withTangents", () => {
|
|
635
|
+
describe("face withTangents", () => {
|
|
636
|
+
it("should expand to tangent side faces but not perpendicular top/bottom", () => {
|
|
637
|
+
sketch("xy", () => {
|
|
638
|
+
rect(200, 100).radius(10);
|
|
639
|
+
});
|
|
640
|
+
extrude(50);
|
|
641
|
+
// "front" = XZ plane (y=0). Matches one flat side face.
|
|
642
|
+
// withTangents should follow cylindrical fillets to adjacent side faces,
|
|
643
|
+
// but NOT cross to top/bottom faces (90° angle = not tangent).
|
|
644
|
+
const sel = select(face().onPlane("front").withTangents());
|
|
645
|
+
render();
|
|
646
|
+
const shapes = sel.getShapes();
|
|
647
|
+
// Rounded rect extrusion has:
|
|
648
|
+
// - 2 cap faces (top/bottom on XY planes)
|
|
649
|
+
// - 4 flat side faces
|
|
650
|
+
// - 4 cylindrical fillet faces connecting adjacent sides
|
|
651
|
+
// Total = 10 faces
|
|
652
|
+
//
|
|
653
|
+
// Starting from front face, tangent expansion should include:
|
|
654
|
+
// 4 flat side faces + 4 cylindrical fillet faces = 8
|
|
655
|
+
// (NOT the 2 cap faces which meet at 90°)
|
|
656
|
+
expect(shapes.length).toBe(8);
|
|
657
|
+
});
|
|
658
|
+
it("should not expand when no tangent neighbors exist (sharp box)", () => {
|
|
659
|
+
sketch("xy", () => {
|
|
660
|
+
rect(100, 50);
|
|
661
|
+
});
|
|
662
|
+
extrude(30);
|
|
663
|
+
// Sharp box: all edges are 90° → no tangent expansion
|
|
664
|
+
const sel = select(face().onPlane("front").withTangents());
|
|
665
|
+
render();
|
|
666
|
+
// Should still be just the 1 front face
|
|
667
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
describe("edge withTangents", () => {
|
|
671
|
+
it("should expand to tangent edges along a filleted box bottom", () => {
|
|
672
|
+
sketch("xy", () => {
|
|
673
|
+
rect(200, 100).radius(10);
|
|
674
|
+
});
|
|
675
|
+
extrude(50);
|
|
676
|
+
// Bottom edges on XY plane: 4 line edges + 4 arc edges = 8 edges
|
|
677
|
+
// All are tangent-connected (line→arc→line→arc→...)
|
|
678
|
+
// Selecting one line edge on "front" plane + withTangents should get all 8
|
|
679
|
+
const sel = select(edge().onPlane("xy").onPlane("front").withTangents());
|
|
680
|
+
render();
|
|
681
|
+
const shapes = sel.getShapes();
|
|
682
|
+
// The front-bottom edge is tangent to the 2 corner arcs,
|
|
683
|
+
// which are tangent to the left/right bottom edges, etc.
|
|
684
|
+
// All 8 bottom edges form a tangent chain.
|
|
685
|
+
expect(shapes.length).toBe(8);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
});
|
|
634
689
|
});
|
|
@@ -19,6 +19,15 @@ describe("thin extrude", () => {
|
|
|
19
19
|
const bbox = ShapeOps.getBoundingBox(shapes[0]);
|
|
20
20
|
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(30, 0);
|
|
21
21
|
});
|
|
22
|
+
it("should still classify internal faces for closed profile", () => {
|
|
23
|
+
sketch("xy", () => {
|
|
24
|
+
rect(100, 100);
|
|
25
|
+
});
|
|
26
|
+
const e = extrude(30).thin(5);
|
|
27
|
+
render();
|
|
28
|
+
const internalFaces = e.getState('internal-faces');
|
|
29
|
+
expect(internalFaces.length).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
22
31
|
it("should create a thin-walled solid with dual offset", () => {
|
|
23
32
|
sketch("xy", () => {
|
|
24
33
|
rect(100, 100);
|
|
@@ -103,6 +112,58 @@ describe("thin extrude", () => {
|
|
|
103
112
|
expect(shapes).toHaveLength(1);
|
|
104
113
|
expect(shapes[0].getType()).toBe('solid');
|
|
105
114
|
});
|
|
115
|
+
it("should classify side, internal, and cap faces for positive offset", () => {
|
|
116
|
+
sketch("xy", () => {
|
|
117
|
+
line([0, 0], [100, 0]);
|
|
118
|
+
});
|
|
119
|
+
const e = extrude(20).thin(10).new();
|
|
120
|
+
render();
|
|
121
|
+
const sideFaces = e.getState('side-faces');
|
|
122
|
+
const internalFaces = e.getState('internal-faces');
|
|
123
|
+
const capFaces = e.getState('cap-faces');
|
|
124
|
+
expect(sideFaces.length).toBeGreaterThan(0);
|
|
125
|
+
expect(internalFaces.length).toBeGreaterThan(0);
|
|
126
|
+
expect(capFaces.length).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
it("should classify side, internal, and cap faces for negative offset", () => {
|
|
129
|
+
sketch("xy", () => {
|
|
130
|
+
line([0, 0], [100, 0]);
|
|
131
|
+
});
|
|
132
|
+
const e = extrude(20).thin(-10).new();
|
|
133
|
+
render();
|
|
134
|
+
const sideFaces = e.getState('side-faces');
|
|
135
|
+
const internalFaces = e.getState('internal-faces');
|
|
136
|
+
const capFaces = e.getState('cap-faces');
|
|
137
|
+
expect(sideFaces.length).toBeGreaterThan(0);
|
|
138
|
+
expect(internalFaces.length).toBeGreaterThan(0);
|
|
139
|
+
expect(capFaces.length).toBe(2);
|
|
140
|
+
});
|
|
141
|
+
it("should classify side, internal, and cap faces for dual offset", () => {
|
|
142
|
+
sketch("xy", () => {
|
|
143
|
+
line([0, 0], [100, 0]);
|
|
144
|
+
});
|
|
145
|
+
const e = extrude(20).thin(5, -10).new();
|
|
146
|
+
render();
|
|
147
|
+
const sideFaces = e.getState('side-faces');
|
|
148
|
+
const internalFaces = e.getState('internal-faces');
|
|
149
|
+
const capFaces = e.getState('cap-faces');
|
|
150
|
+
expect(sideFaces.length).toBeGreaterThan(0);
|
|
151
|
+
expect(internalFaces.length).toBeGreaterThan(0);
|
|
152
|
+
expect(capFaces.length).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
it("should classify faces correctly with symmetric thin extrude", () => {
|
|
155
|
+
sketch("xy", () => {
|
|
156
|
+
line([0, 0], [100, 0]);
|
|
157
|
+
});
|
|
158
|
+
const e = extrude(20).thin(10).symmetric().new();
|
|
159
|
+
render();
|
|
160
|
+
const sideFaces = e.getState('side-faces');
|
|
161
|
+
const internalFaces = e.getState('internal-faces');
|
|
162
|
+
const capFaces = e.getState('cap-faces');
|
|
163
|
+
expect(sideFaces.length).toBeGreaterThan(0);
|
|
164
|
+
expect(internalFaces.length).toBeGreaterThan(0);
|
|
165
|
+
expect(capFaces.length).toBe(2);
|
|
166
|
+
});
|
|
106
167
|
});
|
|
107
168
|
describe("two-distance extrude", () => {
|
|
108
169
|
it("should create a thin-walled solid with two distances", () => {
|