fluidcad 0.0.37 → 0.0.39
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/core/2d/aline.d.ts +0 -8
- package/lib/dist/core/2d/aline.js +3 -22
- package/lib/dist/core/2d/arc.d.ts +1 -23
- package/lib/dist/core/2d/arc.js +11 -28
- package/lib/dist/core/2d/circle.d.ts +1 -8
- package/lib/dist/core/2d/circle.js +7 -24
- package/lib/dist/core/2d/ellipse.d.ts +1 -17
- package/lib/dist/core/2d/ellipse.js +7 -45
- package/lib/dist/core/2d/hline.d.ts +0 -7
- package/lib/dist/core/2d/hline.js +5 -27
- package/lib/dist/core/2d/intersect.d.ts +0 -7
- package/lib/dist/core/2d/intersect.js +0 -13
- package/lib/dist/core/2d/line.d.ts +1 -8
- package/lib/dist/core/2d/line.js +6 -24
- package/lib/dist/core/2d/offset.d.ts +1 -11
- package/lib/dist/core/2d/offset.js +0 -16
- package/lib/dist/core/2d/polygon.d.ts +1 -17
- package/lib/dist/core/2d/polygon.js +18 -37
- package/lib/dist/core/2d/project.d.ts +0 -7
- package/lib/dist/core/2d/project.js +0 -13
- package/lib/dist/core/2d/rect.d.ts +1 -9
- package/lib/dist/core/2d/rect.js +0 -19
- package/lib/dist/core/2d/slot.d.ts +0 -23
- package/lib/dist/core/2d/slot.js +18 -37
- package/lib/dist/core/2d/text.d.ts +0 -7
- package/lib/dist/core/2d/text.js +0 -14
- package/lib/dist/core/2d/vline.d.ts +0 -7
- package/lib/dist/core/2d/vline.js +5 -23
- package/lib/dist/core/fillet.js +7 -0
- package/lib/dist/core/index.d.ts +1 -1
- package/lib/dist/core/interfaces.d.ts +43 -0
- package/lib/dist/core/plane.d.ts +26 -6
- package/lib/dist/core/plane.js +21 -44
- package/lib/dist/features/2d/connect.d.ts +2 -0
- package/lib/dist/features/2d/connect.js +3 -0
- package/lib/dist/features/2d/intersect.js +4 -1
- package/lib/dist/features/2d/offset.js +2 -2
- package/lib/dist/features/frozen-geometry.d.ts +29 -0
- package/lib/dist/features/frozen-geometry.js +62 -0
- package/lib/dist/features/loft.d.ts +38 -1
- package/lib/dist/features/loft.js +133 -8
- package/lib/dist/features/plane-from-object.d.ts +16 -4
- package/lib/dist/features/plane-from-object.js +101 -8
- package/lib/dist/features/rib.js +11 -1
- package/lib/dist/features/shell.d.ts +1 -1
- package/lib/dist/features/shell.js +9 -0
- package/lib/dist/features/sweep.d.ts +11 -1
- package/lib/dist/features/sweep.js +43 -7
- package/lib/dist/helpers/clone-transform.js +62 -5
- package/lib/dist/helpers/scene-helpers.d.ts +12 -0
- package/lib/dist/helpers/scene-helpers.js +26 -0
- package/lib/dist/math/bspline-interpolation.d.ts +33 -6
- package/lib/dist/math/bspline-interpolation.js +187 -84
- package/lib/dist/oc/loft/constrained-loft.d.ts +32 -0
- package/lib/dist/oc/loft/constrained-loft.js +61 -0
- package/lib/dist/oc/loft/curve-data.d.ts +18 -0
- package/lib/dist/oc/loft/curve-data.js +54 -0
- package/lib/dist/oc/loft/curve-eval.d.ts +23 -0
- package/lib/dist/oc/loft/curve-eval.js +84 -0
- package/lib/dist/oc/loft/guided-loft.d.ts +89 -0
- package/lib/dist/oc/loft/guided-loft.js +368 -0
- package/lib/dist/oc/loft/section-compatibility.d.ts +131 -0
- package/lib/dist/oc/loft/section-compatibility.js +481 -0
- package/lib/dist/oc/loft/section-curve.d.ts +53 -0
- package/lib/dist/oc/loft/section-curve.js +264 -0
- package/lib/dist/oc/loft/skinning.d.ts +104 -0
- package/lib/dist/oc/loft/skinning.js +299 -0
- package/lib/dist/oc/loft-ops.d.ts +28 -1
- package/lib/dist/oc/loft-ops.js +31 -1
- package/lib/dist/oc/ncollection.d.ts +19 -0
- package/lib/dist/oc/ncollection.js +69 -0
- package/lib/dist/oc/rib-ops.js +5 -17
- package/lib/dist/oc/sweep-ops.d.ts +7 -16
- package/lib/dist/oc/sweep-ops.js +67 -99
- package/lib/dist/oc/thin-face-maker.d.ts +0 -19
- package/lib/dist/oc/thin-face-maker.js +3 -68
- package/lib/dist/oc/wire-extend-ops.d.ts +12 -0
- package/lib/dist/oc/wire-extend-ops.js +42 -0
- package/lib/dist/oc/wire-ops.d.ts +28 -2
- package/lib/dist/oc/wire-ops.js +88 -4
- package/lib/dist/oc/wrap-ops.js +8 -15
- package/lib/dist/tests/features/2d/circle.test.js +0 -10
- package/lib/dist/tests/features/2d/ellipse.test.js +0 -30
- package/lib/dist/tests/features/2d/line.test.js +0 -5
- package/lib/dist/tests/features/2d/offset.test.js +74 -11
- package/lib/dist/tests/features/2d/polygon.test.js +0 -16
- package/lib/dist/tests/features/2d/rect.test.js +0 -11
- package/lib/dist/tests/features/2d/slot.test.js +0 -8
- package/lib/dist/tests/features/fillet2d.test.js +37 -1
- package/lib/dist/tests/features/loft-conditions.test.d.ts +1 -0
- package/lib/dist/tests/features/loft-conditions.test.js +246 -0
- package/lib/dist/tests/features/loft-guides.test.d.ts +1 -0
- package/lib/dist/tests/features/loft-guides.test.js +276 -0
- package/lib/dist/tests/features/plane.test.js +95 -0
- package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-sweep-borrowed-path.test.js +35 -0
- package/lib/dist/tests/features/rib.test.js +50 -0
- package/lib/dist/tests/features/shell.test.js +35 -1
- package/lib/dist/tests/features/sweep.test.js +127 -1
- package/lib/dist/tests/features/text.test.js +33 -14
- package/lib/dist/tests/math/bspline-interpolation.test.js +97 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/index.json +1 -1
- package/llm-docs/api/types/loft.md +36 -1
- package/llm-docs/api/types/sweep.md +13 -1
- package/llm-docs/api/types/text.md +2 -2
- package/llm-docs/index.json +5 -5
- package/package.json +1 -1
- package/server/dist/fluidcad-server.d.ts +1 -1
- package/server/dist/fluidcad-server.js +11 -1
- package/server/dist/routes/params.js +1 -1
- package/ui/dist/assets/{index-no7mtr5s.js → index-D8zV21wB.js} +83 -83
- package/ui/dist/index.html +1 -1
|
@@ -9,8 +9,34 @@ export declare class WireOps {
|
|
|
9
9
|
static makeWireFromEdges(edges: Edge[]): Wire;
|
|
10
10
|
static reverseWire(wire: Wire): Wire;
|
|
11
11
|
static buildWire(edges: Edge[]): Wire;
|
|
12
|
-
static offsetWire(shape: Wire | Edge, distance: number, isOpen: boolean): Wire;
|
|
12
|
+
static offsetWire(shape: Wire | Edge, distance: number, isOpen: boolean, plane?: Plane): Wire;
|
|
13
|
+
/**
|
|
14
|
+
* Offsets a wire by `distance` on `plane`, handling open/closed wires and
|
|
15
|
+
* negative distances.
|
|
16
|
+
*
|
|
17
|
+
* The plane is passed as a reference face so BRepOffsetAPI_MakeOffset can
|
|
18
|
+
* resolve an offset direction even for wires with no curvature to imply a
|
|
19
|
+
* plane (e.g. a single straight segment, which otherwise fails outright).
|
|
20
|
+
*
|
|
21
|
+
* For open wires a negative distance offsets to the *opposite* side — the
|
|
22
|
+
* face-based offset does not flip sides on a sign change by itself, so the
|
|
23
|
+
* wire is reversed, offset by the magnitude, then reversed back. For closed
|
|
24
|
+
* wires the plane-less offset is tried first (the path circles rely on, where
|
|
25
|
+
* the sign selects inward/outward) and the face-based offset is the fallback.
|
|
26
|
+
*/
|
|
27
|
+
static offsetWireOnPlane(wire: Wire, distance: number, isClosed: boolean, plane: Plane): Wire;
|
|
13
28
|
static isCWRaw(wire: TopoDS_Wire, normal: Vector3d): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Groups edges into connected chains — one wire per chain — via
|
|
31
|
+
* `ShapeAnalysis_FreeBounds.ConnectEdgesToWires`. Edge ends within
|
|
32
|
+
* `tolerance` of each other connect; disconnected groups come back as
|
|
33
|
+
* separate wires (e.g. a guide sketch holding a curve and its mirror).
|
|
34
|
+
*
|
|
35
|
+
* V8-binding gotcha: the plain-name dispatcher can resolve to the
|
|
36
|
+
* deprecated out-parameter overload, which returns a `{ wires }` envelope
|
|
37
|
+
* instead of the sequence itself — unwrap at runtime.
|
|
38
|
+
*/
|
|
39
|
+
static connectEdgesToWires(edges: Edge[], tolerance?: number): Wire[];
|
|
14
40
|
static makeWireFromEdgesRaw(edges: TopoDS_Edge[]): TopoDS_Wire;
|
|
15
41
|
static reverseWireRaw(wire: TopoDS_Wire): TopoDS_Wire;
|
|
16
42
|
static buildWireRaw(edges: TopoDS_Edge[]): TopoDS_Wire;
|
|
@@ -31,5 +57,5 @@ export declare class WireOps {
|
|
|
31
57
|
} | null;
|
|
32
58
|
static groupConnectedEdges(edges: Edge[]): Edge[][];
|
|
33
59
|
private static verticesMatch;
|
|
34
|
-
static offsetWireRaw(wire: TopoDS_Wire, distance: number, isOpen: boolean): TopoDS_Wire;
|
|
60
|
+
static offsetWireRaw(wire: TopoDS_Wire, distance: number, isOpen: boolean, plane?: Plane): TopoDS_Wire;
|
|
35
61
|
}
|
package/lib/dist/oc/wire-ops.js
CHANGED
|
@@ -15,9 +15,39 @@ export class WireOps {
|
|
|
15
15
|
static buildWire(edges) {
|
|
16
16
|
return Wire.fromTopoDSWire(WireOps.buildWireRaw(edges.map(e => e.getShape())));
|
|
17
17
|
}
|
|
18
|
-
static offsetWire(shape, distance, isOpen) {
|
|
18
|
+
static offsetWire(shape, distance, isOpen, plane) {
|
|
19
19
|
const wire = shape instanceof Wire ? shape : WireOps.makeWireFromEdges([shape]);
|
|
20
|
-
return Wire.fromTopoDSWire(WireOps.offsetWireRaw(wire.getShape(), distance, isOpen));
|
|
20
|
+
return Wire.fromTopoDSWire(WireOps.offsetWireRaw(wire.getShape(), distance, isOpen, plane));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Offsets a wire by `distance` on `plane`, handling open/closed wires and
|
|
24
|
+
* negative distances.
|
|
25
|
+
*
|
|
26
|
+
* The plane is passed as a reference face so BRepOffsetAPI_MakeOffset can
|
|
27
|
+
* resolve an offset direction even for wires with no curvature to imply a
|
|
28
|
+
* plane (e.g. a single straight segment, which otherwise fails outright).
|
|
29
|
+
*
|
|
30
|
+
* For open wires a negative distance offsets to the *opposite* side — the
|
|
31
|
+
* face-based offset does not flip sides on a sign change by itself, so the
|
|
32
|
+
* wire is reversed, offset by the magnitude, then reversed back. For closed
|
|
33
|
+
* wires the plane-less offset is tried first (the path circles rely on, where
|
|
34
|
+
* the sign selects inward/outward) and the face-based offset is the fallback.
|
|
35
|
+
*/
|
|
36
|
+
static offsetWireOnPlane(wire, distance, isClosed, plane) {
|
|
37
|
+
if (!isClosed) {
|
|
38
|
+
if (distance < 0) {
|
|
39
|
+
const reversed = WireOps.reverseWire(wire);
|
|
40
|
+
const offsetResult = WireOps.offsetWire(reversed, -distance, true, plane);
|
|
41
|
+
return WireOps.reverseWire(offsetResult);
|
|
42
|
+
}
|
|
43
|
+
return WireOps.offsetWire(wire, distance, true, plane);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return WireOps.offsetWire(wire, distance, false);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return WireOps.offsetWire(wire, distance, false, plane);
|
|
50
|
+
}
|
|
21
51
|
}
|
|
22
52
|
static isCWRaw(wire, normal) {
|
|
23
53
|
const oc = getOC();
|
|
@@ -40,6 +70,39 @@ export class WireOps {
|
|
|
40
70
|
adaptor.delete();
|
|
41
71
|
return signedArea < 0;
|
|
42
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Groups edges into connected chains — one wire per chain — via
|
|
75
|
+
* `ShapeAnalysis_FreeBounds.ConnectEdgesToWires`. Edge ends within
|
|
76
|
+
* `tolerance` of each other connect; disconnected groups come back as
|
|
77
|
+
* separate wires (e.g. a guide sketch holding a curve and its mirror).
|
|
78
|
+
*
|
|
79
|
+
* V8-binding gotcha: the plain-name dispatcher can resolve to the
|
|
80
|
+
* deprecated out-parameter overload, which returns a `{ wires }` envelope
|
|
81
|
+
* instead of the sequence itself — unwrap at runtime.
|
|
82
|
+
*/
|
|
83
|
+
static connectEdgesToWires(edges, tolerance = 1e-6) {
|
|
84
|
+
const oc = getOC();
|
|
85
|
+
const input = new oc.NCollection_HSequence_TopoDS_Shape();
|
|
86
|
+
for (const edge of edges) {
|
|
87
|
+
input.Append(edge.getShape());
|
|
88
|
+
}
|
|
89
|
+
const result = oc.ShapeAnalysis_FreeBounds.ConnectEdgesToWires(input, tolerance, false);
|
|
90
|
+
const isEnvelope = result && typeof result.Sequence !== "function" && result.wires;
|
|
91
|
+
const handle = isEnvelope ? result.wires : result;
|
|
92
|
+
const wires = [];
|
|
93
|
+
const sequence = handle.Sequence();
|
|
94
|
+
for (let i = 1; i <= sequence.Length(); i++) {
|
|
95
|
+
wires.push(Wire.fromTopoDSWire(oc.TopoDS.Wire(sequence.Value(i))));
|
|
96
|
+
}
|
|
97
|
+
input.delete();
|
|
98
|
+
if (isEnvelope && typeof result[Symbol.dispose] === "function") {
|
|
99
|
+
result[Symbol.dispose]();
|
|
100
|
+
}
|
|
101
|
+
else if (typeof handle.delete === "function") {
|
|
102
|
+
handle.delete();
|
|
103
|
+
}
|
|
104
|
+
return wires;
|
|
105
|
+
}
|
|
43
106
|
static makeWireFromEdgesRaw(edges) {
|
|
44
107
|
const oc = getOC();
|
|
45
108
|
const wireMaker = new oc.BRepBuilderAPI_MakeWire();
|
|
@@ -193,10 +256,31 @@ export class WireOps {
|
|
|
193
256
|
const dz = p1.z - p2.z;
|
|
194
257
|
return (dx * dx + dy * dy + dz * dz) < 1e-14;
|
|
195
258
|
}
|
|
196
|
-
static offsetWireRaw(wire, distance, isOpen) {
|
|
259
|
+
static offsetWireRaw(wire, distance, isOpen, plane) {
|
|
197
260
|
const oc = getOC();
|
|
198
261
|
const maker = new oc.BRepOffsetAPI_MakeOffset();
|
|
199
|
-
|
|
262
|
+
// BRepOffsetAPI_MakeOffset can't infer an offset direction from a wire with
|
|
263
|
+
// no curvature (e.g. a single straight segment), so the face-less Init
|
|
264
|
+
// fails with "Failed to offset wire". Initializing it with a planar
|
|
265
|
+
// reference face supplies the normal it needs — pass `plane` whenever the
|
|
266
|
+
// offset happens on a known sketch/target plane.
|
|
267
|
+
if (plane) {
|
|
268
|
+
const [pln, disposePlane] = Convert.toGpPln(plane);
|
|
269
|
+
const faceMaker = new oc.BRepBuilderAPI_MakeFace(pln);
|
|
270
|
+
if (!faceMaker.IsDone()) {
|
|
271
|
+
faceMaker.delete();
|
|
272
|
+
disposePlane();
|
|
273
|
+
maker.delete();
|
|
274
|
+
throw new Error("Failed to create reference face for wire offset");
|
|
275
|
+
}
|
|
276
|
+
const face = faceMaker.Face();
|
|
277
|
+
faceMaker.delete();
|
|
278
|
+
disposePlane();
|
|
279
|
+
maker.Init(face, oc.GeomAbs_JoinType.GeomAbs_Arc, isOpen);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
maker.Init(oc.GeomAbs_JoinType.GeomAbs_Arc, isOpen);
|
|
283
|
+
}
|
|
200
284
|
maker.AddWire(wire);
|
|
201
285
|
maker.Perform(distance, 0);
|
|
202
286
|
if (!maker.IsDone()) {
|
package/lib/dist/oc/wrap-ops.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Explorer } from "./explorer.js";
|
|
|
4
4
|
import { FaceQuery } from "./face-query.js";
|
|
5
5
|
import { WireOps } from "./wire-ops.js";
|
|
6
6
|
import { ShapeFactory } from "../common/shape-factory.js";
|
|
7
|
+
import { NCollections } from "./ncollection.js";
|
|
8
|
+
import { Point2D } from "../math/point.js";
|
|
7
9
|
import { ConeDevelopment, CylinderDevelopment, } from "./wrap-development.js";
|
|
8
10
|
import { interpolateBSpline2d } from "../math/bspline-interpolation.js";
|
|
9
11
|
/** Sample points per curved sketch edge before fitting its UV pcurve. */
|
|
@@ -284,22 +286,13 @@ export class WrapOps {
|
|
|
284
286
|
static fitUvCurve(samples) {
|
|
285
287
|
const oc = getOC();
|
|
286
288
|
const data = interpolateBSpline2d(samples.map(s => ({ x: s.u, y: s.v })));
|
|
287
|
-
const poles = new
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
poles.SetValue(i + 1, point);
|
|
291
|
-
point.delete();
|
|
292
|
-
}
|
|
293
|
-
const knots = new oc.NCollection_Array1_double(1, data.knots.length);
|
|
294
|
-
const multiplicities = new oc.NCollection_Array1_int(1, data.knots.length);
|
|
295
|
-
for (let i = 0; i < data.knots.length; i++) {
|
|
296
|
-
knots.SetValue(i + 1, data.knots[i]);
|
|
297
|
-
multiplicities.SetValue(i + 1, data.multiplicities[i]);
|
|
298
|
-
}
|
|
289
|
+
const [poles, disposePoles] = NCollections.toArray1Pnt2d(data.poles.map(p => new Point2D(p.x, p.y)));
|
|
290
|
+
const [knots, disposeKnots] = NCollections.toArray1Double(data.knots);
|
|
291
|
+
const [multiplicities, disposeMultiplicities] = NCollections.toArray1Int(data.multiplicities);
|
|
299
292
|
const curve = new oc.Geom2d_BSplineCurve(poles, knots, multiplicities, data.degree);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
293
|
+
disposePoles();
|
|
294
|
+
disposeKnots();
|
|
295
|
+
disposeMultiplicities();
|
|
303
296
|
return curve;
|
|
304
297
|
}
|
|
305
298
|
/** Thickens the wrapped face along the surface normal into a solid. */
|
|
@@ -51,14 +51,4 @@ describe("circle", () => {
|
|
|
51
51
|
expect(getFacesByType(solid, "cylinder")).toHaveLength(1);
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
|
-
describe("standalone with targetPlane", () => {
|
|
55
|
-
it("should create a circle on a specific plane", () => {
|
|
56
|
-
circle("xy", 60);
|
|
57
|
-
const e = extrude(10);
|
|
58
|
-
render();
|
|
59
|
-
const solid = e.getShapes()[0];
|
|
60
|
-
const bbox = ShapeOps.getBoundingBox(solid);
|
|
61
|
-
expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
54
|
});
|
|
@@ -66,35 +66,5 @@ describe("ellipse", () => {
|
|
|
66
66
|
render();
|
|
67
67
|
expect(negEllipse?.getError()).toMatch(/positive/i);
|
|
68
68
|
});
|
|
69
|
-
it("throws when given a plane inside a sketch", () => {
|
|
70
|
-
expect(() => {
|
|
71
|
-
sketch("xy", () => {
|
|
72
|
-
ellipse("xy", 30, 20);
|
|
73
|
-
});
|
|
74
|
-
render();
|
|
75
|
-
}).toThrow();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
describe("standalone with targetPlane", () => {
|
|
79
|
-
it("creates an ellipse on a specific plane", () => {
|
|
80
|
-
ellipse("xy", 60, 40);
|
|
81
|
-
const e = extrude(10);
|
|
82
|
-
render();
|
|
83
|
-
const solid = e.getShapes()[0];
|
|
84
|
-
const bbox = ShapeOps.getBoundingBox(solid);
|
|
85
|
-
expect(bbox.maxX - bbox.minX).toBeCloseTo(120, 0);
|
|
86
|
-
expect(bbox.maxY - bbox.minY).toBeCloseTo(80, 0);
|
|
87
|
-
});
|
|
88
|
-
it("creates an ellipse on a plane at a given center", () => {
|
|
89
|
-
ellipse("xy", [10, 20], 30, 15);
|
|
90
|
-
const e = extrude(10);
|
|
91
|
-
render();
|
|
92
|
-
const solid = e.getShapes()[0];
|
|
93
|
-
const bbox = ShapeOps.getBoundingBox(solid);
|
|
94
|
-
expect(bbox.centerX).toBeCloseTo(10, 0);
|
|
95
|
-
expect(bbox.centerY).toBeCloseTo(20, 0);
|
|
96
|
-
expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
|
|
97
|
-
expect(bbox.maxY - bbox.minY).toBeCloseTo(30, 0);
|
|
98
|
-
});
|
|
99
69
|
});
|
|
100
70
|
});
|
|
@@ -32,11 +32,6 @@ describe("line functions", () => {
|
|
|
32
32
|
expect(bbox.maxX - bbox.minX).toBeCloseTo(80, 0);
|
|
33
33
|
expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
|
|
34
34
|
});
|
|
35
|
-
it("should support standalone mode with targetPlane", () => {
|
|
36
|
-
hLine("xy", 50);
|
|
37
|
-
render();
|
|
38
|
-
// Just verify no error — standalone line doesn't form a closed shape
|
|
39
|
-
});
|
|
40
35
|
});
|
|
41
36
|
describe("vLine", () => {
|
|
42
37
|
it("should create a vertical line and form a closed rect", () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { setupOC, render } from "../../setup.js";
|
|
3
3
|
import sketch from "../../../core/sketch.js";
|
|
4
|
-
import { circle, hLine, vLine, offset } from "../../../core/2d/index.js";
|
|
4
|
+
import { circle, hLine, vLine, aLine, line, offset } from "../../../core/2d/index.js";
|
|
5
5
|
import { ShapeOps } from "../../../oc/shape-ops.js";
|
|
6
6
|
describe("offset", () => {
|
|
7
7
|
setupOC();
|
|
@@ -42,6 +42,79 @@ describe("offset", () => {
|
|
|
42
42
|
expect(shapes.length).toBeGreaterThan(2);
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
|
+
// Regression: a single straight segment has no curvature to imply an offset
|
|
46
|
+
// plane, so BRepOffsetAPI_MakeOffset needs the sketch plane as a reference
|
|
47
|
+
// face. Without it, the offset failed with "Failed to offset wire" and no
|
|
48
|
+
// offset edge was produced.
|
|
49
|
+
describe("offset single open line", () => {
|
|
50
|
+
const endpoints = (edge) => [edge.getFirstVertex().toPoint(), edge.getLastVertex().toPoint()];
|
|
51
|
+
it("offsets a single hLine to a parallel line at the right distance", () => {
|
|
52
|
+
const s = sketch("xy", () => {
|
|
53
|
+
hLine(100);
|
|
54
|
+
offset(10);
|
|
55
|
+
});
|
|
56
|
+
render();
|
|
57
|
+
const shapes = s.getShapes();
|
|
58
|
+
expect(shapes.length).toBe(2);
|
|
59
|
+
const [a, b] = endpoints(shapes[1]);
|
|
60
|
+
expect(Math.abs(a.y)).toBeCloseTo(10, 6);
|
|
61
|
+
expect(Math.abs(b.y)).toBeCloseTo(10, 6);
|
|
62
|
+
expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(100, 6);
|
|
63
|
+
});
|
|
64
|
+
it("offsets a single vLine to a parallel line at the right distance", () => {
|
|
65
|
+
const s = sketch("xy", () => {
|
|
66
|
+
vLine(100);
|
|
67
|
+
offset(10);
|
|
68
|
+
});
|
|
69
|
+
render();
|
|
70
|
+
const shapes = s.getShapes();
|
|
71
|
+
expect(shapes.length).toBe(2);
|
|
72
|
+
const [a, b] = endpoints(shapes[1]);
|
|
73
|
+
expect(Math.abs(a.x)).toBeCloseTo(10, 6);
|
|
74
|
+
expect(Math.abs(b.x)).toBeCloseTo(10, 6);
|
|
75
|
+
expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(100, 6);
|
|
76
|
+
});
|
|
77
|
+
it("offsets a single aLine, preserving its length", () => {
|
|
78
|
+
const s = sketch("xy", () => {
|
|
79
|
+
aLine(30, 100);
|
|
80
|
+
offset(10);
|
|
81
|
+
});
|
|
82
|
+
render();
|
|
83
|
+
const shapes = s.getShapes();
|
|
84
|
+
expect(shapes.length).toBe(2);
|
|
85
|
+
const [a, b] = endpoints(shapes[1]);
|
|
86
|
+
expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(100, 6);
|
|
87
|
+
});
|
|
88
|
+
it("offsets a single line, preserving its length", () => {
|
|
89
|
+
const s = sketch("xy", () => {
|
|
90
|
+
line([100, 50]);
|
|
91
|
+
offset(10);
|
|
92
|
+
});
|
|
93
|
+
render();
|
|
94
|
+
const shapes = s.getShapes();
|
|
95
|
+
expect(shapes.length).toBe(2);
|
|
96
|
+
const [a, b] = endpoints(shapes[1]);
|
|
97
|
+
expect(Math.hypot(b.x - a.x, b.y - a.y)).toBeCloseTo(Math.hypot(100, 50), 6);
|
|
98
|
+
});
|
|
99
|
+
it("offsets a single line to the opposite side with a negative distance", () => {
|
|
100
|
+
const sPos = sketch("xy", () => {
|
|
101
|
+
hLine(100);
|
|
102
|
+
offset(10);
|
|
103
|
+
});
|
|
104
|
+
render();
|
|
105
|
+
const posY = endpoints(sPos.getShapes()[1])[0].y;
|
|
106
|
+
const sNeg = sketch("xy", () => {
|
|
107
|
+
hLine(100);
|
|
108
|
+
offset(-10);
|
|
109
|
+
});
|
|
110
|
+
render();
|
|
111
|
+
const negShapes = sNeg.getShapes();
|
|
112
|
+
expect(negShapes.length).toBe(2);
|
|
113
|
+
const negY = endpoints(negShapes[1])[0].y;
|
|
114
|
+
expect(Math.abs(negY)).toBeCloseTo(10, 6);
|
|
115
|
+
expect(Math.sign(negY)).toBe(-Math.sign(posY));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
45
118
|
describe("offset direction", () => {
|
|
46
119
|
it("positive and negative offsets should produce different results", () => {
|
|
47
120
|
const s1 = sketch("xy", () => {
|
|
@@ -86,16 +159,6 @@ describe("offset", () => {
|
|
|
86
159
|
expect(shapes).toHaveLength(1);
|
|
87
160
|
});
|
|
88
161
|
});
|
|
89
|
-
describe("specific targets (standalone)", () => {
|
|
90
|
-
it("should offset source geometries onto a target plane", () => {
|
|
91
|
-
const s = sketch("xy", () => {
|
|
92
|
-
circle(40);
|
|
93
|
-
});
|
|
94
|
-
offset("xy", 5, false, s);
|
|
95
|
-
render();
|
|
96
|
-
// Offset edges should exist on the target plane
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
162
|
describe("default distance", () => {
|
|
100
163
|
it("should use default distance of 1", () => {
|
|
101
164
|
const s = sketch("xy", () => {
|
|
@@ -131,20 +131,4 @@ describe("polygon", () => {
|
|
|
131
131
|
expect(bbox.centerY).toBeCloseTo(30, 0);
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
|
-
describe("standalone with targetPlane", () => {
|
|
135
|
-
it("should create a polygon on a specific plane", () => {
|
|
136
|
-
polygon("xy", 5, 50);
|
|
137
|
-
const e = extrude(10);
|
|
138
|
-
render();
|
|
139
|
-
const solid = e.getShapes()[0];
|
|
140
|
-
expect(solid.getFaces()).toHaveLength(7);
|
|
141
|
-
});
|
|
142
|
-
it("should create a circumscribed polygon on a specific plane", () => {
|
|
143
|
-
polygon("xy", 6, 60, "circumscribed");
|
|
144
|
-
const e = extrude(10);
|
|
145
|
-
render();
|
|
146
|
-
const solid = e.getShapes()[0];
|
|
147
|
-
expect(solid.getFaces()).toHaveLength(8);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
134
|
});
|
|
@@ -52,15 +52,4 @@ describe("rect", () => {
|
|
|
52
52
|
expect(solid.getEdges().length).toBeGreaterThan(12);
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
-
describe("standalone with targetPlane", () => {
|
|
56
|
-
it("should create a rectangle on a specific plane", () => {
|
|
57
|
-
rect("xy", 80, 40);
|
|
58
|
-
const e = extrude(10);
|
|
59
|
-
render();
|
|
60
|
-
const solid = e.getShapes()[0];
|
|
61
|
-
const bbox = ShapeOps.getBoundingBox(solid);
|
|
62
|
-
expect(bbox.maxX - bbox.minX).toBeCloseTo(80, 0);
|
|
63
|
-
expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
55
|
});
|
|
@@ -33,12 +33,4 @@ describe("slot", () => {
|
|
|
33
33
|
expect(arcEdges.length).toBeGreaterThan(0);
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
|
-
describe("standalone with targetPlane", () => {
|
|
37
|
-
it("should create a slot on a specific plane", () => {
|
|
38
|
-
slot("xy", 60, 10);
|
|
39
|
-
const e = extrude(10);
|
|
40
|
-
render();
|
|
41
|
-
expect(e.getShapes()).toHaveLength(1);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
36
|
});
|
|
@@ -4,7 +4,7 @@ import sketch from "../../core/sketch.js";
|
|
|
4
4
|
import extrude from "../../core/extrude.js";
|
|
5
5
|
import fillet from "../../core/fillet.js";
|
|
6
6
|
import fuse from "../../core/fuse.js";
|
|
7
|
-
import { hMove, rect } from "../../core/2d/index.js";
|
|
7
|
+
import { arc, hMove, rect, vLine, hLine, vMove, back } from "../../core/2d/index.js";
|
|
8
8
|
import { getEdgesByType } from "../utils.js";
|
|
9
9
|
describe("fillet2d", () => {
|
|
10
10
|
setupOC();
|
|
@@ -51,6 +51,42 @@ describe("fillet2d", () => {
|
|
|
51
51
|
// 4 shortened sides per face (8) + 4 vertical edges each split into segments (8) = 16
|
|
52
52
|
expect(getEdgesByType(solid, "line")).toHaveLength(16);
|
|
53
53
|
});
|
|
54
|
+
it("should fillet a corner passed as radius-first spread args", () => {
|
|
55
|
+
const s = sketch("xy", () => {
|
|
56
|
+
const l1 = hLine(40);
|
|
57
|
+
const l2 = vLine(-40);
|
|
58
|
+
fillet(8, l1, l2);
|
|
59
|
+
});
|
|
60
|
+
render();
|
|
61
|
+
// The corner is rounded: each line is shortened and one fillet arc inserted.
|
|
62
|
+
// Before the radius-first spread dispatch existed, fillet(8, l1, l2) was a
|
|
63
|
+
// silent no-op (returned undefined) and produced 0 arcs.
|
|
64
|
+
expect(getEdgesByType(s.getEdges(), "arc")).toHaveLength(1);
|
|
65
|
+
expect(getEdgesByType(s.getEdges(), "line")).toHaveLength(2);
|
|
66
|
+
});
|
|
67
|
+
it("fillets an arc/line corner passed as radius-first spread args", () => {
|
|
68
|
+
// The exact shape from the original bug report: a fillet between an arc (l1)
|
|
69
|
+
// and a line (l2) requested as fillet(radius, l2, l1). Before the radius-first
|
|
70
|
+
// spread form was dispatched, this was a silent no-op while fillet(8) (all
|
|
71
|
+
// corners) rounded the same arc/line corner.
|
|
72
|
+
const s = sketch("front", () => {
|
|
73
|
+
vMove(10);
|
|
74
|
+
const gl = hLine(140).guide().centered();
|
|
75
|
+
back();
|
|
76
|
+
vMove(33 + 16);
|
|
77
|
+
hLine(8);
|
|
78
|
+
vLine(-16);
|
|
79
|
+
hLine(15 - 8);
|
|
80
|
+
const l1 = arc(gl.end()).radius(55);
|
|
81
|
+
const l2 = vLine(-10);
|
|
82
|
+
fillet(8, l2, l1);
|
|
83
|
+
});
|
|
84
|
+
render();
|
|
85
|
+
// Baseline (no fillet) is 1 arc; rounding the arc/line corner shortens the
|
|
86
|
+
// original arc and inserts the fillet arc -> 2 arcs. The array form
|
|
87
|
+
// fillet([l2, l1], 8) produces the identical result.
|
|
88
|
+
expect(getEdgesByType(s.getEdges(), "arc")).toHaveLength(2);
|
|
89
|
+
});
|
|
54
90
|
});
|
|
55
91
|
describe("fillet radius", () => {
|
|
56
92
|
it("should use default radius of 1", () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|