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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { SceneObject } from "../../common/scene-object.js";
|
|
1
2
|
import { GeometrySceneObject } from "./geometry.js";
|
|
2
3
|
export type ConnectMode = 'line' | 'arc';
|
|
3
4
|
export declare class Connect extends GeometrySceneObject {
|
|
4
5
|
private mode;
|
|
5
6
|
constructor(mode?: ConnectMode);
|
|
6
7
|
build(): void;
|
|
8
|
+
createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
|
|
7
9
|
compareTo(other: Connect): boolean;
|
|
8
10
|
getType(): string;
|
|
9
11
|
serialize(): {
|
|
@@ -2,6 +2,7 @@ import { Vertex } from "../../common/vertex.js";
|
|
|
2
2
|
import { SectionOps } from "../../oc/section-ops.js";
|
|
3
3
|
import { WireOps } from "../../oc/wire-ops.js";
|
|
4
4
|
import { ExtrudableGeometryBase } from "./extrudable-base.js";
|
|
5
|
+
import { SelectSceneObject } from "../select.js";
|
|
5
6
|
export class Intersect extends ExtrudableGeometryBase {
|
|
6
7
|
sourceObjects;
|
|
7
8
|
constructor(sourceObjects, targetPlane = null) {
|
|
@@ -33,7 +34,9 @@ export class Intersect extends ExtrudableGeometryBase {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
for (const obj of this.sourceObjects) {
|
|
36
|
-
obj
|
|
37
|
+
if (obj instanceof SelectSceneObject) {
|
|
38
|
+
obj.removeShapes(this);
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
if (this.targetPlane) {
|
|
39
42
|
this.targetPlane.removeShapes(this);
|
|
@@ -55,9 +55,9 @@ export class Offset extends ExtrudableGeometryBase {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
let lastOffsetWire = null;
|
|
58
|
+
const plane = this.getPlane();
|
|
58
59
|
for (const wireInfo of wires) {
|
|
59
|
-
const
|
|
60
|
-
const offsetWire = WireOps.offsetWire(wireInfo.wire, this.distance, isOpen);
|
|
60
|
+
const offsetWire = WireOps.offsetWireOnPlane(wireInfo.wire, this.distance, wireInfo.wire.isClosed(), plane);
|
|
61
61
|
lastOffsetWire = offsetWire;
|
|
62
62
|
const edges = offsetWire.getEdges();
|
|
63
63
|
for (const edge of edges) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
|
|
2
|
+
import { TransformablePrimitive } from "../common/transformable-primitive.js";
|
|
3
|
+
/**
|
|
4
|
+
* A clone-only stand-in for sketch geometry that a repeated feature references
|
|
5
|
+
* but that lives in a sketch *outside* the cloned set — e.g. a `sweep` whose
|
|
6
|
+
* path is `otherSketch.regions.foo`.
|
|
7
|
+
*
|
|
8
|
+
* Such geometry cannot be rebuilt during the clone: a 2D curve resolves its
|
|
9
|
+
* plane by walking up to its owning `Sketch` (see GeometrySceneObject.sketch),
|
|
10
|
+
* and its pen position depends on sibling curves that are not part of the
|
|
11
|
+
* clone. Cloning it out of its sketch leaves it parented under the repeat
|
|
12
|
+
* container with no sketch ancestor, so `this.sketch` is `null` and build
|
|
13
|
+
* throws "Cannot read properties of null (reading 'getPlane')".
|
|
14
|
+
*
|
|
15
|
+
* Instead we freeze the source's already-built edges (which are final,
|
|
16
|
+
* world-space, and already incorporate any fillet/trim) and bake in the
|
|
17
|
+
* per-instance clone transform — exactly like cloned primitives
|
|
18
|
+
* (cylinder/sphere) do via {@link TransformablePrimitive.addPrimitiveShape}.
|
|
19
|
+
*/
|
|
20
|
+
export declare class FrozenGeometry extends TransformablePrimitive {
|
|
21
|
+
private source;
|
|
22
|
+
constructor(source: SceneObject);
|
|
23
|
+
build(context?: BuildSceneObjectContext): void;
|
|
24
|
+
getDependencies(): SceneObject[];
|
|
25
|
+
createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
|
|
26
|
+
compareTo(other: SceneObject): boolean;
|
|
27
|
+
getType(): string;
|
|
28
|
+
serialize(): {};
|
|
29
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TransformablePrimitive } from "../common/transformable-primitive.js";
|
|
2
|
+
/**
|
|
3
|
+
* A clone-only stand-in for sketch geometry that a repeated feature references
|
|
4
|
+
* but that lives in a sketch *outside* the cloned set — e.g. a `sweep` whose
|
|
5
|
+
* path is `otherSketch.regions.foo`.
|
|
6
|
+
*
|
|
7
|
+
* Such geometry cannot be rebuilt during the clone: a 2D curve resolves its
|
|
8
|
+
* plane by walking up to its owning `Sketch` (see GeometrySceneObject.sketch),
|
|
9
|
+
* and its pen position depends on sibling curves that are not part of the
|
|
10
|
+
* clone. Cloning it out of its sketch leaves it parented under the repeat
|
|
11
|
+
* container with no sketch ancestor, so `this.sketch` is `null` and build
|
|
12
|
+
* throws "Cannot read properties of null (reading 'getPlane')".
|
|
13
|
+
*
|
|
14
|
+
* Instead we freeze the source's already-built edges (which are final,
|
|
15
|
+
* world-space, and already incorporate any fillet/trim) and bake in the
|
|
16
|
+
* per-instance clone transform — exactly like cloned primitives
|
|
17
|
+
* (cylinder/sphere) do via {@link TransformablePrimitive.addPrimitiveShape}.
|
|
18
|
+
*/
|
|
19
|
+
export class FrozenGeometry extends TransformablePrimitive {
|
|
20
|
+
source;
|
|
21
|
+
constructor(source) {
|
|
22
|
+
super();
|
|
23
|
+
this.source = source;
|
|
24
|
+
}
|
|
25
|
+
build(context) {
|
|
26
|
+
// Read the source's raw built edges rather than getShapes(): the original
|
|
27
|
+
// feature that consumes this geometry (e.g. the un-cloned sweep) may have
|
|
28
|
+
// marked it removed via removeShapes(), which getShapes() filters out. The
|
|
29
|
+
// edges themselves are unchanged, so we still want them — skipping only
|
|
30
|
+
// meta/guide shapes, matching how a spine/region is normally read.
|
|
31
|
+
for (const shape of this.source.getAddedShapes()) {
|
|
32
|
+
if (shape.isMetaShape() || shape.isGuideShape()) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
this.addPrimitiveShape(shape, context);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getDependencies() {
|
|
39
|
+
// The source is the pre-existing original, not part of the clone set; it is
|
|
40
|
+
// built earlier in the scene order, so we read its shapes at build time
|
|
41
|
+
// rather than depending on a clone of it.
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
createCopy(remap) {
|
|
45
|
+
return new FrozenGeometry(this.source).syncPrimitiveWith(this);
|
|
46
|
+
}
|
|
47
|
+
compareTo(other) {
|
|
48
|
+
if (!(other instanceof FrozenGeometry)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!super.compareTo(other)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return this.source.compareTo(other.source);
|
|
55
|
+
}
|
|
56
|
+
getType() {
|
|
57
|
+
return "frozen-geometry";
|
|
58
|
+
}
|
|
59
|
+
serialize() {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,21 +1,58 @@
|
|
|
1
1
|
import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
|
|
2
|
-
import {
|
|
2
|
+
import { LoftEndCondition } from "../oc/loft-ops.js";
|
|
3
|
+
import { ILoft, LoftConditionType } from "../core/interfaces.js";
|
|
4
|
+
import { type NumberParam } from "../core/param.js";
|
|
3
5
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
4
6
|
export declare class Loft extends ExtrudeBase implements ILoft {
|
|
5
7
|
private _profiles;
|
|
8
|
+
private _guides;
|
|
9
|
+
private _startCondition?;
|
|
10
|
+
private _endCondition?;
|
|
6
11
|
constructor(...profiles: SceneObject[]);
|
|
7
12
|
get profiles(): SceneObject[];
|
|
13
|
+
/**
|
|
14
|
+
* Adds side guide curves the loft surface must follow. FluidCAD supports
|
|
15
|
+
* one or two guides (the underlying OCC algorithm has no notion of more);
|
|
16
|
+
* each guide must pass through every profile.
|
|
17
|
+
*/
|
|
18
|
+
guides(...guides: SceneObject[]): this;
|
|
19
|
+
/**
|
|
20
|
+
* Constrains how the surface leaves the first profile: 'normal' takes off
|
|
21
|
+
* perpendicular to the profile plane, 'tangent' takes off inside the
|
|
22
|
+
* profile plane (outward), 'none' clears the constraint. The magnitude
|
|
23
|
+
* scales the takeoff strength (default 1).
|
|
24
|
+
*/
|
|
25
|
+
startCondition(type: LoftConditionType, magnitude?: NumberParam): this;
|
|
26
|
+
/**
|
|
27
|
+
* Constrains how the surface arrives at the last profile — see
|
|
28
|
+
* {@link startCondition}.
|
|
29
|
+
*/
|
|
30
|
+
endCondition(type: LoftConditionType, magnitude?: NumberParam): this;
|
|
31
|
+
private static resolveCondition;
|
|
32
|
+
private hasConditions;
|
|
8
33
|
validate(): void;
|
|
9
34
|
build(context: BuildSceneObjectContext): void;
|
|
35
|
+
/**
|
|
36
|
+
* The options for `LoftOps.makeLoft`, with guide objects resolved to wires.
|
|
37
|
+
* A single guide argument may carry several separate curves (e.g. a sketch
|
|
38
|
+
* with a curve and its mirror) — each connected chain counts as one guide.
|
|
39
|
+
* Returns undefined for a plain loft, keeping the legacy multi-wire path
|
|
40
|
+
* untouched.
|
|
41
|
+
*/
|
|
42
|
+
private resolveLoftOptions;
|
|
10
43
|
private buildThinLoft;
|
|
11
44
|
private getProfilePlane;
|
|
12
45
|
private getWiresFromSceneObject;
|
|
13
46
|
getDependencies(): SceneObject[];
|
|
14
47
|
createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
|
|
15
48
|
compareTo(other: Loft): boolean;
|
|
49
|
+
private static conditionsEqual;
|
|
16
50
|
getType(): string;
|
|
17
51
|
serialize(): {
|
|
18
52
|
profiles: any[];
|
|
53
|
+
guides: any[];
|
|
54
|
+
startCondition: LoftEndCondition;
|
|
55
|
+
endCondition: LoftEndCondition;
|
|
19
56
|
operationMode: "new" | "remove";
|
|
20
57
|
thin: [number, number] | [number];
|
|
21
58
|
};
|
|
@@ -3,12 +3,16 @@ import { LoftOps } from "../oc/loft-ops.js";
|
|
|
3
3
|
import { FaceMaker2 } from "../oc/face-maker2.js";
|
|
4
4
|
import { FaceOps } from "../oc/face-ops.js";
|
|
5
5
|
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveParam } from "../core/param.js";
|
|
7
|
+
import { fuseWithSceneObjects, cutWithSceneObjects, wiresFromSceneObjectEdges } from "../helpers/scene-helpers.js";
|
|
7
8
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
8
9
|
import { ThinFaceMaker } from "../oc/thin-face-maker.js";
|
|
9
10
|
import { requireShapes } from "../common/operand-check.js";
|
|
10
11
|
export class Loft extends ExtrudeBase {
|
|
11
12
|
_profiles = [];
|
|
13
|
+
_guides = [];
|
|
14
|
+
_startCondition;
|
|
15
|
+
_endCondition;
|
|
12
16
|
constructor(...profiles) {
|
|
13
17
|
super();
|
|
14
18
|
this._profiles = profiles;
|
|
@@ -16,19 +20,78 @@ export class Loft extends ExtrudeBase {
|
|
|
16
20
|
get profiles() {
|
|
17
21
|
return this._profiles;
|
|
18
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Adds side guide curves the loft surface must follow. FluidCAD supports
|
|
25
|
+
* one or two guides (the underlying OCC algorithm has no notion of more);
|
|
26
|
+
* each guide must pass through every profile.
|
|
27
|
+
*/
|
|
28
|
+
guides(...guides) {
|
|
29
|
+
if (guides.length === 0) {
|
|
30
|
+
throw new Error("loft.guides: provide at least one guide curve.");
|
|
31
|
+
}
|
|
32
|
+
this._guides = guides;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Constrains how the surface leaves the first profile: 'normal' takes off
|
|
37
|
+
* perpendicular to the profile plane, 'tangent' takes off inside the
|
|
38
|
+
* profile plane (outward), 'none' clears the constraint. The magnitude
|
|
39
|
+
* scales the takeoff strength (default 1).
|
|
40
|
+
*/
|
|
41
|
+
startCondition(type, magnitude) {
|
|
42
|
+
this._startCondition = Loft.resolveCondition("startCondition", type, magnitude);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Constrains how the surface arrives at the last profile — see
|
|
47
|
+
* {@link startCondition}.
|
|
48
|
+
*/
|
|
49
|
+
endCondition(type, magnitude) {
|
|
50
|
+
this._endCondition = Loft.resolveCondition("endCondition", type, magnitude);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
static resolveCondition(method, type, magnitude) {
|
|
54
|
+
if (type === "none") {
|
|
55
|
+
if (magnitude !== undefined) {
|
|
56
|
+
throw new Error(`loft.${method}: magnitude has no effect with 'none'.`);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (type !== "normal" && type !== "tangent") {
|
|
61
|
+
throw new Error(`loft.${method}: expected 'none', 'normal' or 'tangent', got '${type}'.`);
|
|
62
|
+
}
|
|
63
|
+
const value = magnitude === undefined ? 1 : resolveParam(magnitude);
|
|
64
|
+
if (value === 0) {
|
|
65
|
+
throw new Error(`loft.${method}: magnitude must be non-zero.`);
|
|
66
|
+
}
|
|
67
|
+
return { kind: type, magnitude: value };
|
|
68
|
+
}
|
|
69
|
+
hasConditions() {
|
|
70
|
+
return this._startCondition !== undefined || this._endCondition !== undefined;
|
|
71
|
+
}
|
|
19
72
|
validate() {
|
|
20
73
|
for (let i = 0; i < this._profiles.length; i++) {
|
|
21
74
|
requireShapes(this._profiles[i], `profile ${i + 1}`, "loft");
|
|
22
75
|
}
|
|
76
|
+
for (let i = 0; i < this._guides.length; i++) {
|
|
77
|
+
requireShapes(this._guides[i], `guide ${i + 1}`, "loft");
|
|
78
|
+
}
|
|
79
|
+
if (this._guides.length > 2) {
|
|
80
|
+
throw new Error("Loft supports at most two guide curves.");
|
|
81
|
+
}
|
|
82
|
+
if (this._guides.length > 0 && this.isThin()) {
|
|
83
|
+
throw new Error("Loft guides cannot be combined with thin mode.");
|
|
84
|
+
}
|
|
23
85
|
}
|
|
24
86
|
build(context) {
|
|
25
87
|
if (this.profiles.length < 2) {
|
|
26
88
|
throw new Error("Loft requires at least two profiles.");
|
|
27
89
|
}
|
|
28
90
|
const p = context.getProfiler();
|
|
91
|
+
const options = p.record('Resolve loft options', () => this.resolveLoftOptions());
|
|
29
92
|
let newShapes;
|
|
30
93
|
if (this.isThin()) {
|
|
31
|
-
newShapes = p.record('Build thin loft', () => this.buildThinLoft());
|
|
94
|
+
newShapes = p.record('Build thin loft', () => this.buildThinLoft(options));
|
|
32
95
|
}
|
|
33
96
|
else {
|
|
34
97
|
const allWires = [];
|
|
@@ -37,15 +100,21 @@ export class Loft extends ExtrudeBase {
|
|
|
37
100
|
if (wires.length === 0) {
|
|
38
101
|
throw new Error("Could not extract wire from profile.");
|
|
39
102
|
}
|
|
103
|
+
if (options && wires.length !== 1) {
|
|
104
|
+
throw new Error("Loft with guides or start/end conditions requires exactly one region per profile.");
|
|
105
|
+
}
|
|
40
106
|
for (const wire of wires) {
|
|
41
107
|
allWires.push(wire);
|
|
42
108
|
}
|
|
43
109
|
}
|
|
44
|
-
newShapes = p.record('Make loft', () => LoftOps.makeLoft(allWires));
|
|
110
|
+
newShapes = p.record('Make loft', () => LoftOps.makeLoft(allWires, options));
|
|
45
111
|
}
|
|
46
112
|
for (const profile of this.profiles) {
|
|
47
113
|
profile.removeShapes(this);
|
|
48
114
|
}
|
|
115
|
+
for (const guide of this._guides) {
|
|
116
|
+
guide.removeShapes(this);
|
|
117
|
+
}
|
|
49
118
|
// Classify faces into start/end/side using profile planes
|
|
50
119
|
const firstPlane = this.getProfilePlane(this.profiles[0]);
|
|
51
120
|
const lastPlane = this.getProfilePlane(this.profiles[this.profiles.length - 1]);
|
|
@@ -102,7 +171,31 @@ export class Loft extends ExtrudeBase {
|
|
|
102
171
|
this.classifyExtrudeEdges();
|
|
103
172
|
this.setFinalShapes(this.getShapes());
|
|
104
173
|
}
|
|
105
|
-
|
|
174
|
+
/**
|
|
175
|
+
* The options for `LoftOps.makeLoft`, with guide objects resolved to wires.
|
|
176
|
+
* A single guide argument may carry several separate curves (e.g. a sketch
|
|
177
|
+
* with a curve and its mirror) — each connected chain counts as one guide.
|
|
178
|
+
* Returns undefined for a plain loft, keeping the legacy multi-wire path
|
|
179
|
+
* untouched.
|
|
180
|
+
*/
|
|
181
|
+
resolveLoftOptions() {
|
|
182
|
+
if (this._guides.length === 0 && !this.hasConditions()) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
let guides;
|
|
186
|
+
if (this._guides.length > 0) {
|
|
187
|
+
guides = this._guides.flatMap((guide, i) => wiresFromSceneObjectEdges(guide, `loft guide ${i + 1}`));
|
|
188
|
+
if (guides.length > 2) {
|
|
189
|
+
throw new Error(`Loft supports at most two guide curves, got ${guides.length}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
startCondition: this._startCondition,
|
|
194
|
+
endCondition: this._endCondition,
|
|
195
|
+
guides,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
buildThinLoft(options) {
|
|
106
199
|
const outerWires = [];
|
|
107
200
|
const innerWires = [];
|
|
108
201
|
for (const profile of this.profiles) {
|
|
@@ -120,9 +213,15 @@ export class Loft extends ExtrudeBase {
|
|
|
120
213
|
}
|
|
121
214
|
}
|
|
122
215
|
}
|
|
123
|
-
|
|
216
|
+
// With conditions, both walls come from the in-house skin — assemble the
|
|
217
|
+
// thin solid directly (walls + ring caps). Booleans between two
|
|
218
|
+
// nearly-parallel B-spline shells take OCC seconds.
|
|
219
|
+
if (options && innerWires.length > 0 && innerWires.length === outerWires.length) {
|
|
220
|
+
return LoftOps.makeThinLoft(outerWires, innerWires, options);
|
|
221
|
+
}
|
|
222
|
+
const outerSolids = LoftOps.makeLoft(outerWires, options);
|
|
124
223
|
if (innerWires.length > 0 && innerWires.length === outerWires.length) {
|
|
125
|
-
const innerSolids = LoftOps.makeLoft(innerWires);
|
|
224
|
+
const innerSolids = LoftOps.makeLoft(innerWires, options);
|
|
126
225
|
const outerFuse = BooleanOps.fuse(outerSolids);
|
|
127
226
|
const innerFuse = BooleanOps.fuse(innerSolids);
|
|
128
227
|
const cutResult = BooleanOps.cutShapes(outerFuse.result[0], innerFuse.result[0]);
|
|
@@ -187,11 +286,16 @@ export class Loft extends ExtrudeBase {
|
|
|
187
286
|
return [];
|
|
188
287
|
}
|
|
189
288
|
getDependencies() {
|
|
190
|
-
return [...this._profiles];
|
|
289
|
+
return [...this._profiles, ...this._guides];
|
|
191
290
|
}
|
|
192
291
|
createCopy(remap) {
|
|
193
292
|
const profiles = this._profiles.map(p => remap.get(p) || p);
|
|
194
|
-
|
|
293
|
+
const copy = new Loft(...profiles);
|
|
294
|
+
copy.syncWith(this);
|
|
295
|
+
copy._guides = this._guides.map(g => remap.get(g) || g);
|
|
296
|
+
copy._startCondition = this._startCondition ? { ...this._startCondition } : undefined;
|
|
297
|
+
copy._endCondition = this._endCondition ? { ...this._endCondition } : undefined;
|
|
298
|
+
return copy;
|
|
195
299
|
}
|
|
196
300
|
compareTo(other) {
|
|
197
301
|
if (!(other instanceof Loft)) {
|
|
@@ -208,14 +312,35 @@ export class Loft extends ExtrudeBase {
|
|
|
208
312
|
return false;
|
|
209
313
|
}
|
|
210
314
|
}
|
|
315
|
+
if (this._guides.length !== other._guides.length) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
for (let i = 0; i < this._guides.length; i++) {
|
|
319
|
+
if (!this._guides[i].compareTo(other._guides[i])) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!Loft.conditionsEqual(this._startCondition, other._startCondition)
|
|
324
|
+
|| !Loft.conditionsEqual(this._endCondition, other._endCondition)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
211
327
|
return true;
|
|
212
328
|
}
|
|
329
|
+
static conditionsEqual(a, b) {
|
|
330
|
+
if (a === undefined || b === undefined) {
|
|
331
|
+
return a === b;
|
|
332
|
+
}
|
|
333
|
+
return a.kind === b.kind && a.magnitude === b.magnitude;
|
|
334
|
+
}
|
|
213
335
|
getType() {
|
|
214
336
|
return "loft";
|
|
215
337
|
}
|
|
216
338
|
serialize() {
|
|
217
339
|
return {
|
|
218
340
|
profiles: this.profiles.map(f => f.serialize()),
|
|
341
|
+
guides: this._guides.length > 0 ? this._guides.map(g => g.serialize()) : undefined,
|
|
342
|
+
startCondition: this._startCondition,
|
|
343
|
+
endCondition: this._endCondition,
|
|
219
344
|
operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
|
|
220
345
|
thin: this._thin,
|
|
221
346
|
};
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
|
|
2
|
-
import { PlaneRenderableOptions } from "../core/plane.js";
|
|
2
|
+
import { EdgePlanePosition, PlaneRenderableOptions } from "../core/plane.js";
|
|
3
3
|
import { PlaneObjectBase } from "./plane-renderable-base.js";
|
|
4
4
|
import { Face } from "../common/face.js";
|
|
5
5
|
import { Point } from "../math/point.js";
|
|
6
6
|
import { Plane } from "../math/plane.js";
|
|
7
7
|
export declare class PlaneFromObject extends PlaneObjectBase {
|
|
8
8
|
sourceObject: SceneObject;
|
|
9
|
-
|
|
10
|
-
constructor(sourceObject: SceneObject,
|
|
9
|
+
optionsOrPosition?: PlaneRenderableOptions | EdgePlanePosition;
|
|
10
|
+
constructor(sourceObject: SceneObject, optionsOrPosition?: PlaneRenderableOptions | EdgePlanePosition);
|
|
11
11
|
validate(): void;
|
|
12
12
|
build(context?: BuildSceneObjectContext): void;
|
|
13
|
+
/**
|
|
14
|
+
* Builds a plane normal to `edge` at the configured position. The edge
|
|
15
|
+
* tangent at that point becomes the plane normal; the in-plane axes are
|
|
16
|
+
* an arbitrary (but deterministic) basis around it.
|
|
17
|
+
*/
|
|
18
|
+
private buildFromEdge;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the second argument for a face/plane source. A bare number is a
|
|
21
|
+
* normal-offset distance; a string position is only meaningful for edges
|
|
22
|
+
* and is rejected here.
|
|
23
|
+
*/
|
|
24
|
+
private faceOptions;
|
|
13
25
|
getFromSceneObject(sceneObject: SceneObject): {
|
|
14
26
|
plane: Plane;
|
|
15
27
|
sourceFace: Face;
|
|
@@ -24,7 +36,7 @@ export declare class PlaneFromObject extends PlaneObjectBase {
|
|
|
24
36
|
xDirection: import("../math/vector3d.js").Vector3d;
|
|
25
37
|
yDirection: import("../math/vector3d.js").Vector3d;
|
|
26
38
|
normal: import("../math/vector3d.js").Vector3d;
|
|
27
|
-
options: import("../math/plane.js").PlaneTransformOptions;
|
|
39
|
+
options: import("../math/plane.js").PlaneTransformOptions | EdgePlanePosition;
|
|
28
40
|
center: any;
|
|
29
41
|
};
|
|
30
42
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { PlaneObjectBase } from "./plane-renderable-base.js";
|
|
2
2
|
import { FaceOps } from "../oc/face-ops.js";
|
|
3
3
|
import { ShapeOps } from "../oc/shape-ops.js";
|
|
4
|
+
import { WireOps } from "../oc/wire-ops.js";
|
|
5
|
+
import { PathSampler } from "../oc/path-sampler.js";
|
|
4
6
|
import { Point } from "../math/point.js";
|
|
7
|
+
import { Plane } from "../math/plane.js";
|
|
5
8
|
import { requireShapes } from "../common/operand-check.js";
|
|
6
9
|
export class PlaneFromObject extends PlaneObjectBase {
|
|
7
10
|
sourceObject;
|
|
8
|
-
|
|
9
|
-
constructor(sourceObject,
|
|
11
|
+
optionsOrPosition;
|
|
12
|
+
constructor(sourceObject, optionsOrPosition) {
|
|
10
13
|
super();
|
|
11
14
|
this.sourceObject = sourceObject;
|
|
12
|
-
this.
|
|
15
|
+
this.optionsOrPosition = optionsOrPosition;
|
|
13
16
|
}
|
|
14
17
|
validate() {
|
|
15
18
|
// PlaneObjectBase sources expose the plane directly — no shapes required.
|
|
@@ -19,6 +22,17 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
19
22
|
requireShapes(this.sourceObject, "source", "plane");
|
|
20
23
|
}
|
|
21
24
|
build(context) {
|
|
25
|
+
// An edge source produces a plane normal to the edge at a position along
|
|
26
|
+
// it. The face-vs-edge decision is deferred to here (rather than the
|
|
27
|
+
// plane() builder) because the source shape type is only known once the
|
|
28
|
+
// selection has been resolved.
|
|
29
|
+
if (!(this.sourceObject instanceof PlaneObjectBase)) {
|
|
30
|
+
const shapes = this.sourceObject.getShapes({ excludeGuide: false });
|
|
31
|
+
if (shapes.length === 1 && shapes[0].isEdge()) {
|
|
32
|
+
this.buildFromEdge(context, shapes[0]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
let plane;
|
|
23
37
|
let sourceFace;
|
|
24
38
|
let center;
|
|
@@ -36,10 +50,11 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
36
50
|
const bbox = ShapeOps.getBoundingBox(sourceFace.getShape());
|
|
37
51
|
center = new Point(bbox.centerX, bbox.centerY, bbox.centerZ);
|
|
38
52
|
}
|
|
39
|
-
|
|
53
|
+
const options = this.faceOptions();
|
|
54
|
+
if (options) {
|
|
40
55
|
// Apply the same transform to the center so the preview face stays on
|
|
41
56
|
// the rotated plane instead of floating at its pre-rotation position.
|
|
42
|
-
const matrix = plane.getTransformMatrix(
|
|
57
|
+
const matrix = plane.getTransformMatrix(options);
|
|
43
58
|
plane = plane.applyMatrix(matrix);
|
|
44
59
|
if (center) {
|
|
45
60
|
center = center.transform(matrix);
|
|
@@ -60,6 +75,53 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
60
75
|
face.markAsMetaShape();
|
|
61
76
|
this.addShape(face);
|
|
62
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds a plane normal to `edge` at the configured position. The edge
|
|
80
|
+
* tangent at that point becomes the plane normal; the in-plane axes are
|
|
81
|
+
* an arbitrary (but deterministic) basis around it.
|
|
82
|
+
*/
|
|
83
|
+
buildFromEdge(context, edge) {
|
|
84
|
+
const t = normalizeEdgePosition(this.optionsOrPosition);
|
|
85
|
+
const frame = sampleEdgeFrame(edge, t);
|
|
86
|
+
// The forward tangent points *into* the edge at the start, so the plane
|
|
87
|
+
// would face inward there. Flip it at the start endpoint so it faces
|
|
88
|
+
// outward — like an extrude's start cap (the end already faces outward via
|
|
89
|
+
// the forward tangent). Interior/end positions keep the forward tangent.
|
|
90
|
+
const normal = t <= 0 ? frame.tangent.negate() : frame.tangent;
|
|
91
|
+
let plane = Plane.fromPointAndNormal(frame.point, normal);
|
|
92
|
+
let center = frame.point;
|
|
93
|
+
// Unlike the face path, an edge is only *referenced* to derive the plane —
|
|
94
|
+
// it is not consumed, so it stays available to its owning solid and to
|
|
95
|
+
// other features.
|
|
96
|
+
const transform = context?.getTransform() ?? null;
|
|
97
|
+
if (transform) {
|
|
98
|
+
plane = plane.applyMatrix(transform);
|
|
99
|
+
center = center.transform(transform);
|
|
100
|
+
}
|
|
101
|
+
this.setState('plane-center', center);
|
|
102
|
+
this.setState('plane', plane);
|
|
103
|
+
const face = FaceOps.planeToFace(plane, center);
|
|
104
|
+
face.markAsMetaShape();
|
|
105
|
+
this.addShape(face);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the second argument for a face/plane source. A bare number is a
|
|
109
|
+
* normal-offset distance; a string position is only meaningful for edges
|
|
110
|
+
* and is rejected here.
|
|
111
|
+
*/
|
|
112
|
+
faceOptions() {
|
|
113
|
+
const value = this.optionsOrPosition;
|
|
114
|
+
if (value == null) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === 'number') {
|
|
118
|
+
return { offset: value };
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
throw new Error(`Plane: position '${value}' is only valid for an edge source`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
63
125
|
getFromSceneObject(sceneObject) {
|
|
64
126
|
const shapes = sceneObject.getShapes();
|
|
65
127
|
console.log(`Plane: Retrieved ${shapes.length} shapes from selection`, shapes);
|
|
@@ -82,7 +144,7 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
82
144
|
return [];
|
|
83
145
|
}
|
|
84
146
|
createCopy(remap) {
|
|
85
|
-
return new PlaneFromObject(this, this.
|
|
147
|
+
return new PlaneFromObject(this, this.optionsOrPosition);
|
|
86
148
|
}
|
|
87
149
|
compareTo(other) {
|
|
88
150
|
if (!(other instanceof PlaneFromObject)) {
|
|
@@ -94,7 +156,7 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
94
156
|
if (!this.sourceObject.compareTo(other.sourceObject)) {
|
|
95
157
|
return false;
|
|
96
158
|
}
|
|
97
|
-
if (JSON.stringify(this.
|
|
159
|
+
if (JSON.stringify(this.optionsOrPosition) !== JSON.stringify(other.optionsOrPosition)) {
|
|
98
160
|
return false;
|
|
99
161
|
}
|
|
100
162
|
return true;
|
|
@@ -109,8 +171,39 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
109
171
|
xDirection: plane.xDirection,
|
|
110
172
|
yDirection: plane.yDirection,
|
|
111
173
|
normal: plane.normal,
|
|
112
|
-
options: this.
|
|
174
|
+
options: this.optionsOrPosition,
|
|
113
175
|
center: this.getState('plane-center') || plane.origin,
|
|
114
176
|
};
|
|
115
177
|
}
|
|
116
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Evaluates the point and unit (forward) tangent on `edge` at a normalized
|
|
181
|
+
* position `t` (`0` = start, `1` = end), measured by arc length.
|
|
182
|
+
*/
|
|
183
|
+
function sampleEdgeFrame(edge, t) {
|
|
184
|
+
const wire = WireOps.makeWireFromEdges([edge]);
|
|
185
|
+
const sampler = new PathSampler(wire);
|
|
186
|
+
try {
|
|
187
|
+
return sampler.evalAt(t * sampler.length);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
sampler.dispose();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function normalizeEdgePosition(position) {
|
|
194
|
+
if (position === undefined) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
if (typeof position === 'number') {
|
|
198
|
+
return position;
|
|
199
|
+
}
|
|
200
|
+
switch (position) {
|
|
201
|
+
case 'start':
|
|
202
|
+
return 0;
|
|
203
|
+
case 'middle':
|
|
204
|
+
return 0.5;
|
|
205
|
+
case 'end':
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
throw new Error("Plane: an edge plane takes a 0–1 position or 'start'/'middle'/'end', not transform options");
|
|
209
|
+
}
|
package/lib/dist/features/rib.js
CHANGED
|
@@ -57,8 +57,18 @@ export class Rib extends ExtrudeBase {
|
|
|
57
57
|
let direction;
|
|
58
58
|
let distance;
|
|
59
59
|
if (this._parallel) {
|
|
60
|
+
// The in-plane extrude direction is plane.normal × spineDir — a cross
|
|
61
|
+
// product, hence a pseudovector. Under a mirror clone (repeat("mirror",
|
|
62
|
+
// …)) both operands are already reflected, and for a reflection
|
|
63
|
+
// M(a) × M(b) = −M(a × b): the recomputed perpendicular points to the
|
|
64
|
+
// opposite side, sending the mirrored rib the wrong way. Flip it back
|
|
65
|
+
// when this instance's clone transform is a reflection. Proper
|
|
66
|
+
// transforms (linear/circular/rotate repeats, det > 0) need no fix, and
|
|
67
|
+
// normal-mode direction (±plane.normal) is linear so it is never
|
|
68
|
+
// affected.
|
|
69
|
+
const mirrorSign = context.getTransform()?.isMirroring() ? -1 : 1;
|
|
60
70
|
const perpDir = RibOps.computeSpinePerpendicularDirection(spineWire, plane);
|
|
61
|
-
direction = perpDir.multiply(dirSign);
|
|
71
|
+
direction = perpDir.multiply(dirSign * mirrorSign);
|
|
62
72
|
distance = p.record('Compute extrude distance', () => RibOps.computeExtrudeDistanceAlongDirection(direction, plane.origin, scopeShapes));
|
|
63
73
|
}
|
|
64
74
|
else {
|