fluidcad 0.0.30 → 0.0.32
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/lib/dist/common/build-error.d.ts +13 -0
- package/lib/dist/common/build-error.js +18 -0
- package/lib/dist/common/describe-error.d.ts +6 -0
- package/lib/dist/common/describe-error.js +26 -0
- package/lib/dist/common/operand-check.d.ts +19 -0
- package/lib/dist/common/operand-check.js +38 -0
- package/lib/dist/common/scene-object.d.ts +8 -0
- package/lib/dist/common/scene-object.js +10 -0
- package/lib/dist/common/shape-factory.d.ts +1 -1
- package/lib/dist/core/2d/arc.d.ts +4 -2
- package/lib/dist/core/2d/hmove.d.ts +8 -1
- package/lib/dist/core/2d/hmove.js +6 -2
- package/lib/dist/core/2d/pmove.d.ts +13 -3
- package/lib/dist/core/2d/pmove.js +6 -2
- package/lib/dist/core/2d/vmove.d.ts +8 -1
- package/lib/dist/core/2d/vmove.js +8 -4
- package/lib/dist/core/extrude.d.ts +17 -4
- package/lib/dist/core/extrude.js +8 -6
- package/lib/dist/core/interfaces.d.ts +16 -6
- package/lib/dist/core/mirror.d.ts +5 -5
- package/lib/dist/features/2d/aline.js +6 -2
- package/lib/dist/features/2d/arc.d.ts +3 -0
- package/lib/dist/features/2d/arc.js +28 -1
- package/lib/dist/features/2d/hline.js +5 -1
- package/lib/dist/features/2d/hmove.d.ts +2 -2
- package/lib/dist/features/2d/hmove.js +32 -7
- package/lib/dist/features/2d/intersect.js +17 -10
- package/lib/dist/features/2d/line.d.ts +2 -0
- package/lib/dist/features/2d/line.js +4 -0
- package/lib/dist/features/2d/pmove.d.ts +2 -2
- package/lib/dist/features/2d/pmove.js +47 -7
- package/lib/dist/features/2d/projection.d.ts +1 -1
- package/lib/dist/features/2d/projection.js +25 -15
- package/lib/dist/features/2d/sketch.d.ts +2 -2
- package/lib/dist/features/2d/sketch.js +10 -4
- package/lib/dist/features/2d/tarc-to-point.js +0 -3
- package/lib/dist/features/2d/tarc.js +0 -3
- package/lib/dist/features/2d/tline.js +0 -3
- package/lib/dist/features/2d/vline.js +5 -1
- package/lib/dist/features/2d/vmove.d.ts +2 -2
- package/lib/dist/features/2d/vmove.js +32 -7
- package/lib/dist/features/axis-from-edge.d.ts +1 -0
- package/lib/dist/features/axis-from-edge.js +8 -0
- package/lib/dist/features/chamfer.d.ts +1 -0
- package/lib/dist/features/chamfer.js +6 -0
- package/lib/dist/features/color.d.ts +1 -0
- package/lib/dist/features/color.js +6 -0
- package/lib/dist/features/common.d.ts +1 -0
- package/lib/dist/features/common.js +9 -0
- package/lib/dist/features/common2d.d.ts +1 -0
- package/lib/dist/features/common2d.js +9 -0
- package/lib/dist/features/draft.d.ts +1 -0
- package/lib/dist/features/draft.js +6 -0
- package/lib/dist/features/extrude-to-face.d.ts +5 -1
- package/lib/dist/features/extrude-to-face.js +50 -8
- package/lib/dist/features/extrude.js +19 -28
- package/lib/dist/features/fillet.d.ts +1 -0
- package/lib/dist/features/fillet.js +6 -0
- package/lib/dist/features/fillet2d.d.ts +1 -0
- package/lib/dist/features/fillet2d.js +9 -0
- package/lib/dist/features/fuse.d.ts +1 -0
- package/lib/dist/features/fuse.js +6 -0
- package/lib/dist/features/fuse2d.d.ts +1 -0
- package/lib/dist/features/fuse2d.js +9 -0
- package/lib/dist/features/loft.d.ts +1 -0
- package/lib/dist/features/loft.js +6 -0
- package/lib/dist/features/mirror-shape.d.ts +1 -0
- package/lib/dist/features/mirror-shape.js +28 -8
- package/lib/dist/features/plane-from-object.d.ts +1 -0
- package/lib/dist/features/plane-from-object.js +8 -0
- package/lib/dist/features/rotate.d.ts +1 -0
- package/lib/dist/features/rotate.js +11 -2
- package/lib/dist/features/select.d.ts +1 -0
- package/lib/dist/features/select.js +40 -12
- package/lib/dist/features/shell.d.ts +1 -0
- package/lib/dist/features/shell.js +6 -0
- package/lib/dist/features/simple-extruder.js +6 -3
- package/lib/dist/features/subtract.d.ts +1 -0
- package/lib/dist/features/subtract.js +5 -0
- package/lib/dist/features/subtract2d.d.ts +1 -0
- package/lib/dist/features/subtract2d.js +5 -0
- package/lib/dist/features/sweep.d.ts +1 -0
- package/lib/dist/features/sweep.js +4 -0
- package/lib/dist/features/translate.d.ts +1 -0
- package/lib/dist/features/translate.js +9 -0
- package/lib/dist/filters/face/above-below.d.ts +20 -0
- package/lib/dist/filters/face/above-below.js +57 -0
- package/lib/dist/filters/face/face-filter.d.ts +26 -0
- package/lib/dist/filters/face/face-filter.js +64 -0
- package/lib/dist/filters/face/planar-filter.d.ts +15 -0
- package/lib/dist/filters/face/planar-filter.js +30 -0
- package/lib/dist/filters/from-object.d.ts +1 -0
- package/lib/dist/filters/from-object.js +3 -0
- package/lib/dist/oc/boolean-ops.d.ts +2 -2
- package/lib/dist/oc/boolean-ops.js +8 -3
- package/lib/dist/oc/edge-ops.d.ts +17 -0
- package/lib/dist/oc/edge-ops.js +60 -0
- package/lib/dist/oc/face-maker2.d.ts +8 -0
- package/lib/dist/oc/face-maker2.js +42 -1
- package/lib/dist/oc/face-ops.d.ts +6 -1
- package/lib/dist/oc/face-ops.js +3 -2
- package/lib/dist/oc/face-query.js +19 -15
- package/lib/dist/oc/ray-intersect.d.ts +3 -2
- package/lib/dist/oc/ray-intersect.js +2 -4
- package/lib/dist/oc/shell-ops.js +15 -2
- package/lib/dist/oc/thin-face-maker.d.ts +15 -0
- package/lib/dist/oc/thin-face-maker.js +48 -7
- package/lib/dist/oc/wire-ops.d.ts +14 -0
- package/lib/dist/oc/wire-ops.js +38 -0
- package/lib/dist/rendering/render.js +6 -4
- package/lib/dist/tests/common/describe-error.test.d.ts +1 -0
- package/lib/dist/tests/common/describe-error.test.js +36 -0
- package/lib/dist/tests/features/2d/intersect.test.js +43 -0
- package/lib/dist/tests/features/2d/move.test.js +72 -1
- package/lib/dist/tests/features/2d/project-regression.test.js +35 -0
- package/lib/dist/tests/features/color-lineage.test.js +24 -0
- package/lib/dist/tests/features/cut.test.js +40 -0
- package/lib/dist/tests/features/cylinder-curve-filter.test.d.ts +1 -0
- package/lib/dist/tests/features/cylinder-curve-filter.test.js +99 -0
- package/lib/dist/tests/features/extrude-to-face.test.js +52 -0
- package/lib/dist/tests/features/extrude.test.js +46 -8
- package/lib/dist/tests/features/mirror.test.js +74 -0
- package/lib/dist/tests/features/select.test.js +141 -0
- package/lib/dist/tests/features/subtract-consumed-input.test.d.ts +1 -0
- package/lib/dist/tests/features/subtract-consumed-input.test.js +28 -0
- package/lib/dist/tests/features/thin-extrude-offset-fix.test.d.ts +1 -0
- package/lib/dist/tests/features/thin-extrude-offset-fix.test.js +34 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/ui/dist/assets/{index-6Ep4GPxf.js → index-DMw0OYCF.js} +70 -70
- package/ui/dist/assets/index-DR7c2Qk9.css +2 -0
- package/ui/dist/index.html +2 -2
- package/lib/dist/features/infinite-extrude.d.ts +0 -13
- package/lib/dist/features/infinite-extrude.js +0 -79
- package/ui/dist/assets/index-DRKfe6N9.css +0 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../setup.js";
|
|
3
|
+
import sketch from "../../core/sketch.js";
|
|
4
|
+
import extrude from "../../core/extrude.js";
|
|
5
|
+
import fillet from "../../core/fillet.js";
|
|
6
|
+
import select from "../../core/select.js";
|
|
7
|
+
import { rect } from "../../core/2d/index.js";
|
|
8
|
+
import { face, edge } from "../../filters/index.js";
|
|
9
|
+
import cylinder from "../../core/cylinder.js";
|
|
10
|
+
import { getFacesByType } from "../utils.js";
|
|
11
|
+
import { FaceQuery } from "../../oc/face-query.js";
|
|
12
|
+
import { Explorer } from "../../oc/explorer.js";
|
|
13
|
+
import { getOC } from "../../oc/init.js";
|
|
14
|
+
function getSolid() {
|
|
15
|
+
return render().getAllSceneObjects()
|
|
16
|
+
.flatMap(o => o.getShapes())
|
|
17
|
+
.find(s => s.getType() === "solid");
|
|
18
|
+
}
|
|
19
|
+
describe("cylinderCurve filter on fillet faces", () => {
|
|
20
|
+
setupOC();
|
|
21
|
+
it("recognizes fillet faces produced from straight vertical edges (no draft)", () => {
|
|
22
|
+
sketch("xy", () => {
|
|
23
|
+
rect(100, 50);
|
|
24
|
+
});
|
|
25
|
+
extrude(30);
|
|
26
|
+
select(edge().verticalTo("xy"));
|
|
27
|
+
fillet(5);
|
|
28
|
+
const solid = getSolid();
|
|
29
|
+
const cylFaces = getFacesByType(solid, "cylinder");
|
|
30
|
+
expect(cylFaces).toHaveLength(4);
|
|
31
|
+
const cylinderCurveFaces = cylFaces.filter(f => FaceQuery.isCylinderCurveFace(f));
|
|
32
|
+
expect(cylinderCurveFaces).toHaveLength(4);
|
|
33
|
+
});
|
|
34
|
+
it("recognizes fillet faces produced from drafted vertical edges (matches user repro)", () => {
|
|
35
|
+
sketch("xy", () => {
|
|
36
|
+
rect(205, 133).centered();
|
|
37
|
+
});
|
|
38
|
+
const body = extrude(100).draft(10);
|
|
39
|
+
fillet(32, body.sideEdges());
|
|
40
|
+
const solid = getSolid();
|
|
41
|
+
const cylFaces = getFacesByType(solid, "cylinder");
|
|
42
|
+
expect(cylFaces.length).toBeGreaterThan(0);
|
|
43
|
+
const cylinderCurveFaces = cylFaces.filter(f => FaceQuery.isCylinderCurveFace(f));
|
|
44
|
+
if (cylinderCurveFaces.length !== cylFaces.length) {
|
|
45
|
+
const oc = getOC();
|
|
46
|
+
const missed = cylFaces.filter(f => !FaceQuery.isCylinderCurveFace(f));
|
|
47
|
+
for (const face of missed) {
|
|
48
|
+
const ocFace = oc.TopoDS.Face(face.getShape());
|
|
49
|
+
const edges = Explorer.findShapes(ocFace, oc.TopAbs_ShapeEnum.TopAbs_EDGE);
|
|
50
|
+
const curveTypeNames = {};
|
|
51
|
+
for (const k of Object.keys(oc.GeomAbs_CurveType)) {
|
|
52
|
+
const v = oc.GeomAbs_CurveType[k];
|
|
53
|
+
if (v && typeof v.value === "number") {
|
|
54
|
+
curveTypeNames[v.value] = k;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const edgeInfo = edges.map(e => {
|
|
58
|
+
const adaptor = new oc.BRepAdaptor_Curve(oc.TopoDS.Edge(e));
|
|
59
|
+
const t = adaptor.GetType();
|
|
60
|
+
const closed = adaptor.IsClosed();
|
|
61
|
+
adaptor.delete();
|
|
62
|
+
return { type: curveTypeNames[t.value] ?? t.value, closed };
|
|
63
|
+
});
|
|
64
|
+
console.log("Cylinder face missed by cylinderCurve:", JSON.stringify(edgeInfo));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
expect(cylinderCurveFaces).toHaveLength(cylFaces.length);
|
|
68
|
+
});
|
|
69
|
+
it("face().cylinderCurve() returns fillet faces (drafted body)", () => {
|
|
70
|
+
sketch("xy", () => {
|
|
71
|
+
rect(205, 133).centered();
|
|
72
|
+
});
|
|
73
|
+
const body = extrude(100).draft(10);
|
|
74
|
+
fillet(32, body.sideEdges());
|
|
75
|
+
const sel = select(face().cylinderCurve());
|
|
76
|
+
render();
|
|
77
|
+
const selectedFaces = sel.getShapes();
|
|
78
|
+
expect(selectedFaces.length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
it("cylinderCurve(diameter) matches the cylinder surface radius, not bounding-edge radius", () => {
|
|
81
|
+
sketch("xy", () => {
|
|
82
|
+
rect(100, 50);
|
|
83
|
+
});
|
|
84
|
+
extrude(30);
|
|
85
|
+
select(edge().verticalTo("xy"));
|
|
86
|
+
fillet(5);
|
|
87
|
+
const matchingSel = select(face().cylinderCurve(10));
|
|
88
|
+
const nonMatchingSel = select(face().cylinderCurve(20));
|
|
89
|
+
render();
|
|
90
|
+
expect(matchingSel.getShapes()).toHaveLength(4);
|
|
91
|
+
expect(nonMatchingSel.getShapes()).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
it("cylinderCurve does NOT match a full cylinder primitive", () => {
|
|
94
|
+
cylinder(20, 30);
|
|
95
|
+
const sel = select(face().cylinderCurve());
|
|
96
|
+
render();
|
|
97
|
+
expect(sel.getShapes()).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -121,6 +121,58 @@ describe("extrude to face", () => {
|
|
|
121
121
|
expect(lastBBox.maxZ).toBeGreaterThan(firstBBox.maxZ);
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
|
+
describe("first-face / last-face with filters", () => {
|
|
125
|
+
it("should narrow the candidate set with a face filter", () => {
|
|
126
|
+
// Cylinder at origin and a planar slab nearby. Without a filter,
|
|
127
|
+
// 'first-face' would pick the slab's top (at z=20). The cylinder
|
|
128
|
+
// filter forces selection of the cylindrical side face (z=40).
|
|
129
|
+
cylinder(50, 80);
|
|
130
|
+
sketch("xy", () => {
|
|
131
|
+
move([200, 0]);
|
|
132
|
+
rect(50, 50);
|
|
133
|
+
});
|
|
134
|
+
extrude(20).new();
|
|
135
|
+
sketch("xy", () => {
|
|
136
|
+
move([200, 100]);
|
|
137
|
+
rect(30, 30);
|
|
138
|
+
});
|
|
139
|
+
const e = extrude("first-face", face().cylinder());
|
|
140
|
+
render();
|
|
141
|
+
const shapes = e.getShapes();
|
|
142
|
+
expect(shapes).toHaveLength(1);
|
|
143
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
144
|
+
});
|
|
145
|
+
it("should record an error when the filter eliminates all candidate faces", () => {
|
|
146
|
+
// Scene contains only planar geometry — cylinder filter matches nothing
|
|
147
|
+
sketch("xy", () => {
|
|
148
|
+
move([200, 0]);
|
|
149
|
+
rect(50, 50);
|
|
150
|
+
});
|
|
151
|
+
extrude(30).new();
|
|
152
|
+
sketch("xy", () => {
|
|
153
|
+
rect(20, 20);
|
|
154
|
+
});
|
|
155
|
+
const e = extrude("first-face", face().cylinder());
|
|
156
|
+
render();
|
|
157
|
+
expect(e.getError()).toMatch(/No face found for 'first-face' extrusion/);
|
|
158
|
+
});
|
|
159
|
+
it("should accept a filter together with an explicit target", () => {
|
|
160
|
+
cylinder(50, 80);
|
|
161
|
+
const target = sketch("xy", () => {
|
|
162
|
+
move([200, 100]);
|
|
163
|
+
rect(20, 20);
|
|
164
|
+
});
|
|
165
|
+
// Some other sketch in scope so the sketch context is non-trivial.
|
|
166
|
+
sketch("xy", () => {
|
|
167
|
+
rect(10, 10);
|
|
168
|
+
});
|
|
169
|
+
const e = extrude("first-face", face().cylinder(), target);
|
|
170
|
+
render();
|
|
171
|
+
const shapes = e.getShapes();
|
|
172
|
+
expect(shapes).toHaveLength(1);
|
|
173
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
124
176
|
describe("non-parallel planar face", () => {
|
|
125
177
|
it("should extrude up to a drafted side face", () => {
|
|
126
178
|
// Create a box with drafted sides — side faces are inclined planes
|
|
@@ -84,6 +84,27 @@ describe("extrude", () => {
|
|
|
84
84
|
const scene = render();
|
|
85
85
|
expect(countShapes(scene)).toBe(2);
|
|
86
86
|
});
|
|
87
|
+
it("should only fuse with scene objects the new extrusion actually touches", () => {
|
|
88
|
+
// C1: cylinder at origin, z = 0..50
|
|
89
|
+
sketch("top", () => {
|
|
90
|
+
circle(50);
|
|
91
|
+
});
|
|
92
|
+
const e = extrude(50);
|
|
93
|
+
// C2: cylinder stacked on C1, z = 50..100, kept as a separate scene object
|
|
94
|
+
sketch(e.endFaces(), () => {
|
|
95
|
+
circle(50);
|
|
96
|
+
});
|
|
97
|
+
extrude(50).new();
|
|
98
|
+
// C3: cylinder at [100, 0] radius 50, externally tangent to C1, doesn't touch C2 at all
|
|
99
|
+
sketch("top", () => {
|
|
100
|
+
circle([100, 0], 50);
|
|
101
|
+
});
|
|
102
|
+
extrude();
|
|
103
|
+
const scene = render();
|
|
104
|
+
// C1 and C2 must remain separate solids — the third extrude should not
|
|
105
|
+
// pull them into a fusion with each other.
|
|
106
|
+
expect(countShapes(scene)).toBe(3);
|
|
107
|
+
});
|
|
87
108
|
});
|
|
88
109
|
describe("startFaces / endFaces", () => {
|
|
89
110
|
it("should expose start face", () => {
|
|
@@ -259,6 +280,25 @@ describe("extrude", () => {
|
|
|
259
280
|
expect(bbox.minZ).toBeCloseTo(0, 0);
|
|
260
281
|
expect(bbox.maxZ).toBeCloseTo(30, 0);
|
|
261
282
|
});
|
|
283
|
+
it("reverse-direction extrude classifies side/internal faces correctly", () => {
|
|
284
|
+
sketch("xy", () => {
|
|
285
|
+
rect(7, 5).centered();
|
|
286
|
+
});
|
|
287
|
+
const e = extrude(-1.5);
|
|
288
|
+
const sf = e.sideFaces();
|
|
289
|
+
const inf = e.internalFaces();
|
|
290
|
+
const se = e.sideEdges();
|
|
291
|
+
const ine = e.internalEdges();
|
|
292
|
+
addToScene(sf);
|
|
293
|
+
addToScene(inf);
|
|
294
|
+
addToScene(se);
|
|
295
|
+
addToScene(ine);
|
|
296
|
+
render();
|
|
297
|
+
expect(sf.getShapes()).toHaveLength(4);
|
|
298
|
+
expect(inf.getShapes()).toHaveLength(0);
|
|
299
|
+
expect(se.getShapes()).toHaveLength(4);
|
|
300
|
+
expect(ine.getShapes()).toHaveLength(0);
|
|
301
|
+
});
|
|
262
302
|
});
|
|
263
303
|
describe("startEdges / endEdges", () => {
|
|
264
304
|
it("should expose start edges", () => {
|
|
@@ -446,17 +486,15 @@ describe("extrude", () => {
|
|
|
446
486
|
it("should not include guide shapes in getShapes()", () => {
|
|
447
487
|
sketch("xy", () => {
|
|
448
488
|
rect(100, 50);
|
|
489
|
+
circle([50, 25], 20).guide();
|
|
449
490
|
});
|
|
450
|
-
const e = extrude(30)
|
|
491
|
+
const e = extrude(30);
|
|
451
492
|
render();
|
|
493
|
+
// The guide circle is excluded from the extrusion — the result is a
|
|
494
|
+
// plain box (6 faces), not a box with a circular hole (8 faces).
|
|
452
495
|
const shapes = e.getShapes();
|
|
453
|
-
expect(shapes).toHaveLength(
|
|
454
|
-
|
|
455
|
-
const allShapes = e.getAddedShapes();
|
|
456
|
-
expect(allShapes.length).toBeGreaterThan(0);
|
|
457
|
-
for (const shape of allShapes) {
|
|
458
|
-
expect(shape.isGuideShape()).toBe(true);
|
|
459
|
-
}
|
|
496
|
+
expect(shapes).toHaveLength(1);
|
|
497
|
+
expect(shapes[0].getFaces()).toHaveLength(6);
|
|
460
498
|
});
|
|
461
499
|
it("should include meta shapes when filter is disabled", () => {
|
|
462
500
|
sketch("xy", () => {
|
|
@@ -109,6 +109,80 @@ describe("mirror (3D)", () => {
|
|
|
109
109
|
expect(countShapes(scene)).toBe(1);
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
|
+
describe("mirror with .new()", () => {
|
|
113
|
+
it("should not fuse the mirrored copy with overlapping objects", () => {
|
|
114
|
+
// Centered cube — mirror across YZ produces a copy that overlaps the original.
|
|
115
|
+
// Default .add() would fuse to a single solid; .new() must keep them separate.
|
|
116
|
+
sketch("xy", () => {
|
|
117
|
+
rect(30, 30);
|
|
118
|
+
});
|
|
119
|
+
extrude(20).new();
|
|
120
|
+
mirror("yz").new();
|
|
121
|
+
const scene = render();
|
|
122
|
+
expect(countShapes(scene)).toBe(2);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe("mirror with .remove()", () => {
|
|
126
|
+
it("should cut the mirrored copy out of the scene", () => {
|
|
127
|
+
// Solid spans y=-5..25 — mirroring across XZ overlaps in the y=-5..5 strip.
|
|
128
|
+
// .remove() uses the mirrored copy as a cut tool, leaving y=5..25.
|
|
129
|
+
sketch("xy", () => {
|
|
130
|
+
move([0, -5]);
|
|
131
|
+
rect(30, 30);
|
|
132
|
+
});
|
|
133
|
+
extrude(20).new();
|
|
134
|
+
mirror("xz").remove();
|
|
135
|
+
const scene = render();
|
|
136
|
+
expect(countShapes(scene)).toBe(1);
|
|
137
|
+
const solid = scene.getAllSceneObjects()
|
|
138
|
+
.flatMap(o => o.getShapes())
|
|
139
|
+
.find(s => s.getType() === "solid");
|
|
140
|
+
const bbox = ShapeOps.getBoundingBox(solid);
|
|
141
|
+
expect(bbox.minY).toBeCloseTo(5, 0);
|
|
142
|
+
expect(bbox.maxY).toBeCloseTo(25, 0);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("mirror with .scope()", () => {
|
|
146
|
+
it("should narrow .add() fusion to scoped objects only", () => {
|
|
147
|
+
// e1 is centered (mirror would overlap it); e2 is far away.
|
|
148
|
+
sketch("xy", () => {
|
|
149
|
+
rect(30, 30);
|
|
150
|
+
});
|
|
151
|
+
const e1 = extrude(20).new();
|
|
152
|
+
sketch("xy", () => {
|
|
153
|
+
move([60, 0]);
|
|
154
|
+
rect(30, 30);
|
|
155
|
+
});
|
|
156
|
+
const e2 = extrude(20).new();
|
|
157
|
+
// Mirror only e1 across YZ, but scope fusion to e2 (which doesn't intersect).
|
|
158
|
+
// The mirrored copy of e1 stays standalone; e1 and e2 are untouched.
|
|
159
|
+
mirror("yz", e1).add().scope(e2);
|
|
160
|
+
const scene = render();
|
|
161
|
+
// e1 + e2 + standalone mirrored-e1 = 3
|
|
162
|
+
expect(countShapes(scene)).toBe(3);
|
|
163
|
+
});
|
|
164
|
+
it("should narrow .remove() cut to scoped objects only", () => {
|
|
165
|
+
// Both solids cross the XZ plane — mirror across XZ would cut both by default.
|
|
166
|
+
sketch("xy", () => {
|
|
167
|
+
move([0, -5]);
|
|
168
|
+
rect(30, 30);
|
|
169
|
+
});
|
|
170
|
+
const e1 = extrude(20).new();
|
|
171
|
+
sketch("xy", () => {
|
|
172
|
+
move([60, -5]);
|
|
173
|
+
rect(30, 30);
|
|
174
|
+
});
|
|
175
|
+
const e2 = extrude(20).new();
|
|
176
|
+
// Cut only e1, leave e2 untouched.
|
|
177
|
+
mirror("xz", e1).remove().scope(e1);
|
|
178
|
+
const scene = render();
|
|
179
|
+
expect(countShapes(scene)).toBe(2);
|
|
180
|
+
const e2Shapes = e2.getShapes();
|
|
181
|
+
const e2Bbox = ShapeOps.getBoundingBox(e2Shapes[0]);
|
|
182
|
+
expect(e2Bbox.minY).toBeCloseTo(-5, 0);
|
|
183
|
+
expect(e2Bbox.maxY).toBeCloseTo(25, 0);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
112
186
|
describe("mirror with .exclude()", () => {
|
|
113
187
|
it("should skip excluded objects when mirroring everything", () => {
|
|
114
188
|
sketch("xy", () => {
|
|
@@ -7,6 +7,7 @@ import cylinder from "../../core/cylinder.js";
|
|
|
7
7
|
import fillet from "../../core/fillet.js";
|
|
8
8
|
import { circle, move, rect } from "../../core/2d/index.js";
|
|
9
9
|
import { face, edge } from "../../filters/index.js";
|
|
10
|
+
import part from "../../core/part.js";
|
|
10
11
|
describe("select", () => {
|
|
11
12
|
setupOC();
|
|
12
13
|
describe("face filters", () => {
|
|
@@ -227,6 +228,30 @@ describe("select", () => {
|
|
|
227
228
|
expect(sel.getShapes()).toHaveLength(3);
|
|
228
229
|
});
|
|
229
230
|
});
|
|
231
|
+
describe("planar / notPlanar", () => {
|
|
232
|
+
it("should select planar faces from a drafted extrusion", () => {
|
|
233
|
+
sketch("xy", () => {
|
|
234
|
+
circle(60);
|
|
235
|
+
});
|
|
236
|
+
extrude(50).draft(10);
|
|
237
|
+
const sel = select(face().planar());
|
|
238
|
+
render();
|
|
239
|
+
// Drafted cylinder: top + bottom flat circles are planar (1 cone side is not)
|
|
240
|
+
const shapes = sel.getShapes();
|
|
241
|
+
expect(shapes).toHaveLength(2);
|
|
242
|
+
});
|
|
243
|
+
it("should exclude planar faces", () => {
|
|
244
|
+
sketch("xy", () => {
|
|
245
|
+
circle(60);
|
|
246
|
+
});
|
|
247
|
+
extrude(50).draft(10);
|
|
248
|
+
const sel = select(face().notPlanar());
|
|
249
|
+
render();
|
|
250
|
+
// Only the conical side face is non-planar
|
|
251
|
+
const shapes = sel.getShapes();
|
|
252
|
+
expect(shapes).toHaveLength(1);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
230
255
|
describe("cone / notCone", () => {
|
|
231
256
|
it("should select conical faces from a drafted extrusion", () => {
|
|
232
257
|
sketch("xy", () => {
|
|
@@ -251,6 +276,72 @@ describe("select", () => {
|
|
|
251
276
|
expect(shapes).toHaveLength(2);
|
|
252
277
|
});
|
|
253
278
|
});
|
|
279
|
+
describe("above / below", () => {
|
|
280
|
+
it("should select faces entirely above a plane", () => {
|
|
281
|
+
sketch("xy", () => {
|
|
282
|
+
rect(100, 50);
|
|
283
|
+
});
|
|
284
|
+
extrude(30);
|
|
285
|
+
// Faces above z=15: only the top face (all verts at z=30) qualifies.
|
|
286
|
+
// Side faces have 2 verts at z=0 (not above), so they don't match.
|
|
287
|
+
const sel = select(face().above("xy", { offset: 15 }));
|
|
288
|
+
render();
|
|
289
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
290
|
+
});
|
|
291
|
+
it("should select faces entirely below a plane", () => {
|
|
292
|
+
sketch("xy", () => {
|
|
293
|
+
rect(100, 50);
|
|
294
|
+
});
|
|
295
|
+
extrude(30);
|
|
296
|
+
// Faces below z=15: only the bottom face (all verts at z=0) qualifies.
|
|
297
|
+
const sel = select(face().below("xy", { offset: 15 }));
|
|
298
|
+
render();
|
|
299
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
300
|
+
});
|
|
301
|
+
it("should not match faces on the plane itself", () => {
|
|
302
|
+
sketch("xy", () => {
|
|
303
|
+
rect(100, 50);
|
|
304
|
+
});
|
|
305
|
+
extrude(30);
|
|
306
|
+
// Faces above z=0: bottom face is ON the plane (dist=0), not above.
|
|
307
|
+
// Side faces straddle (2 verts on plane). Only top face qualifies.
|
|
308
|
+
const sel = select(face().above("xy"));
|
|
309
|
+
render();
|
|
310
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
311
|
+
});
|
|
312
|
+
it("should select partially above faces", () => {
|
|
313
|
+
sketch("xy", () => {
|
|
314
|
+
rect(100, 50);
|
|
315
|
+
});
|
|
316
|
+
extrude(30);
|
|
317
|
+
// partial: true — match faces with at least one vertex above z=0.
|
|
318
|
+
// Top face + 4 side faces (each has 2 verts at z=30) = 5.
|
|
319
|
+
const sel = select(face().above("xy", { partial: true }));
|
|
320
|
+
render();
|
|
321
|
+
expect(sel.getShapes()).toHaveLength(5);
|
|
322
|
+
});
|
|
323
|
+
it("should select partially below faces", () => {
|
|
324
|
+
sketch("xy", () => {
|
|
325
|
+
rect(100, 50);
|
|
326
|
+
});
|
|
327
|
+
extrude(30);
|
|
328
|
+
// partial: true — match faces with at least one vertex below z=30.
|
|
329
|
+
// Bottom face + 4 side faces (each has 2 verts at z=0) = 5.
|
|
330
|
+
const sel = select(face().below("xy", { offset: 30, partial: true }));
|
|
331
|
+
render();
|
|
332
|
+
expect(sel.getShapes()).toHaveLength(5);
|
|
333
|
+
});
|
|
334
|
+
it("should return empty when no faces match", () => {
|
|
335
|
+
sketch("xy", () => {
|
|
336
|
+
rect(100, 50);
|
|
337
|
+
});
|
|
338
|
+
extrude(30);
|
|
339
|
+
// Nothing is below z=0 on a box sitting on XY.
|
|
340
|
+
const sel = select(face().below("xy"));
|
|
341
|
+
render();
|
|
342
|
+
expect(sel.getShapes()).toHaveLength(0);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
254
345
|
});
|
|
255
346
|
describe("edge filters", () => {
|
|
256
347
|
describe("onPlane / notOnPlane", () => {
|
|
@@ -834,4 +925,54 @@ describe("select", () => {
|
|
|
834
925
|
});
|
|
835
926
|
});
|
|
836
927
|
});
|
|
928
|
+
describe("cross-part selection", () => {
|
|
929
|
+
it("should select faces from another part via from()", () => {
|
|
930
|
+
const p1 = part("p1", () => {
|
|
931
|
+
cylinder(50, 100);
|
|
932
|
+
});
|
|
933
|
+
let sel;
|
|
934
|
+
part("p2", () => {
|
|
935
|
+
sel = select(face().from(p1));
|
|
936
|
+
});
|
|
937
|
+
render();
|
|
938
|
+
// A cylinder has 3 faces (top, bottom, lateral)
|
|
939
|
+
expect(sel.getShapes()).toHaveLength(3);
|
|
940
|
+
});
|
|
941
|
+
it("should narrow cross-part selection by additional filters", () => {
|
|
942
|
+
const p1 = part("p1", () => {
|
|
943
|
+
cylinder(50, 100);
|
|
944
|
+
});
|
|
945
|
+
let sel;
|
|
946
|
+
part("p2", () => {
|
|
947
|
+
sel = select(face().cylinder().from(p1));
|
|
948
|
+
});
|
|
949
|
+
render();
|
|
950
|
+
// Only the lateral cylindrical face
|
|
951
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
952
|
+
});
|
|
953
|
+
it("should select edges from another part via edge().from()", () => {
|
|
954
|
+
const p1 = part("p1", () => {
|
|
955
|
+
cylinder(50, 100);
|
|
956
|
+
});
|
|
957
|
+
let sel;
|
|
958
|
+
part("p2", () => {
|
|
959
|
+
sel = select(edge().from(p1));
|
|
960
|
+
});
|
|
961
|
+
render();
|
|
962
|
+
// A cylinder has 3 edges (top circle, bottom circle, seam)
|
|
963
|
+
expect(sel.getShapes()).toHaveLength(3);
|
|
964
|
+
});
|
|
965
|
+
it("should narrow cross-part edge selection by additional filters", () => {
|
|
966
|
+
const p1 = part("p1", () => {
|
|
967
|
+
cylinder(50, 100);
|
|
968
|
+
});
|
|
969
|
+
let sel;
|
|
970
|
+
part("p2", () => {
|
|
971
|
+
sel = select(edge().circle().from(p1));
|
|
972
|
+
});
|
|
973
|
+
render();
|
|
974
|
+
// The two circular edges (top + bottom) — seam is excluded
|
|
975
|
+
expect(sel.getShapes()).toHaveLength(2);
|
|
976
|
+
});
|
|
977
|
+
});
|
|
837
978
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../setup.js";
|
|
3
|
+
import sketch from "../../core/sketch.js";
|
|
4
|
+
import revolve from "../../core/revolve.js";
|
|
5
|
+
import subtract from "../../core/subtract.js";
|
|
6
|
+
import axis from "../../core/axis.js";
|
|
7
|
+
import translate from "../../core/translate.js";
|
|
8
|
+
import { circle } from "../../core/2d/index.js";
|
|
9
|
+
describe("subtract: consumed-input diagnostic (issue #50)", () => {
|
|
10
|
+
setupOC();
|
|
11
|
+
it("explains which earlier op consumed an empty operand", () => {
|
|
12
|
+
const torusAxis = axis('y', { offsetZ: 20 });
|
|
13
|
+
sketch('yz', () => {
|
|
14
|
+
circle(8);
|
|
15
|
+
});
|
|
16
|
+
const torus1 = revolve(torusAxis).name(' torus1');
|
|
17
|
+
translate([0, 5, 0], torus1);
|
|
18
|
+
sketch('yz', () => {
|
|
19
|
+
circle(8);
|
|
20
|
+
});
|
|
21
|
+
const torus2 = revolve(torusAxis).new().name(' torus2');
|
|
22
|
+
const sub = subtract(torus1, torus2);
|
|
23
|
+
render();
|
|
24
|
+
const err = sub.getError();
|
|
25
|
+
expect(err).toMatch(/subtract:.*first operand.*has no shapes.*translate/);
|
|
26
|
+
expect(err).toMatch(/Hint:/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../setup.js";
|
|
3
|
+
import sketch from "../../core/sketch.js";
|
|
4
|
+
import extrude from "../../core/extrude.js";
|
|
5
|
+
import select from "../../core/select.js";
|
|
6
|
+
import fillet from "../../core/fillet.js";
|
|
7
|
+
import { rect, intersect, offset } from "../../core/2d/index.js";
|
|
8
|
+
import { edge } from "../../filters/index.js";
|
|
9
|
+
describe("thin extrude offset auto-fix", () => {
|
|
10
|
+
setupOC();
|
|
11
|
+
it("falls back to face-spine offset when wire-spine offset fails", () => {
|
|
12
|
+
// The drafted body's filleted corners produce GeomAbs_OffsetCurve edges
|
|
13
|
+
// when sectioned at z=0 and offset by -6. BRepOffsetAPI_MakeOffset's
|
|
14
|
+
// wire-spine path can't re-offset that wire (it throws "Offset wire is
|
|
15
|
+
// not closed."), but the face-spine path can — ThinFaceMaker.doOffset
|
|
16
|
+
// should retry on failure.
|
|
17
|
+
sketch("top", () => {
|
|
18
|
+
rect(205, 133).centered();
|
|
19
|
+
});
|
|
20
|
+
const body = extrude(100).draft(10);
|
|
21
|
+
fillet(32, body.sideEdges());
|
|
22
|
+
const s = select(edge().onPlane("top"));
|
|
23
|
+
sketch("bottom", () => {
|
|
24
|
+
intersect(s);
|
|
25
|
+
offset(-6, true);
|
|
26
|
+
});
|
|
27
|
+
const thin = extrude(5).thin(1.25, 1.25);
|
|
28
|
+
render();
|
|
29
|
+
expect(thin.getError()).toBeNull();
|
|
30
|
+
const shapes = thin.getShapes();
|
|
31
|
+
expect(shapes.length).toBeGreaterThan(0);
|
|
32
|
+
expect(shapes[0].getType()).toBe('solid');
|
|
33
|
+
});
|
|
34
|
+
});
|