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
|
@@ -63,8 +63,17 @@ export class Shell extends SceneObject {
|
|
|
63
63
|
originalObj.removeShape(shape, this);
|
|
64
64
|
}
|
|
65
65
|
catch {
|
|
66
|
+
// OCCT's MakeThickSolid could not offset the inner wall. Keep the
|
|
67
|
+
// original (un-shelled) solid in the scene so downstream features
|
|
68
|
+
// still have geometry to work with, but surface the failure instead
|
|
69
|
+
// of silently serving an unchanged part.
|
|
66
70
|
newShapes.push(shape);
|
|
67
71
|
console.warn("Shell: Failed to create thick solid.");
|
|
72
|
+
this.setError("shell: could not hollow the solid — wall offset failed.\n" +
|
|
73
|
+
"Hint: the wall may be thicker than a nearby feature or radius of " +
|
|
74
|
+
"curvature, or a cut/groove may open onto a removed face. Try a " +
|
|
75
|
+
"thinner wall, fewer/simpler open faces, or shelling before cutting " +
|
|
76
|
+
"small features.");
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
79
|
for (const sel of this.faceSelections) {
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
|
|
2
2
|
import { Extrudable } from "../helpers/types.js";
|
|
3
3
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
4
|
-
import { ISweep } from "../core/interfaces.js";
|
|
4
|
+
import { ISweep, SweepSide } from "../core/interfaces.js";
|
|
5
|
+
import { type NumberParam } from "../core/param.js";
|
|
5
6
|
export declare class Sweep extends ExtrudeBase implements ISweep {
|
|
6
7
|
private _path;
|
|
8
|
+
private _extendStart?;
|
|
9
|
+
private _extendEnd?;
|
|
7
10
|
constructor(path: SceneObject, extrudable?: Extrudable);
|
|
8
11
|
get path(): SceneObject;
|
|
12
|
+
/**
|
|
13
|
+
* Extends the swept solid beyond the path at the given end by `amount`,
|
|
14
|
+
* continuing straight along the path's tangent there. Chain twice for both ends.
|
|
15
|
+
*/
|
|
16
|
+
extend(side: SweepSide, amount: NumberParam): this;
|
|
9
17
|
validate(): void;
|
|
10
18
|
build(context: BuildSceneObjectContext): void;
|
|
11
19
|
/** Plain sweep: classify by inner-wire detection on the start face. */
|
|
@@ -41,5 +49,7 @@ export declare class Sweep extends ExtrudeBase implements ISweep {
|
|
|
41
49
|
extrudable: any;
|
|
42
50
|
operationMode: "new" | "remove";
|
|
43
51
|
thin: [number, number] | [number];
|
|
52
|
+
extendStart: number;
|
|
53
|
+
extendEnd: number;
|
|
44
54
|
};
|
|
45
55
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Explorer } from "../oc/explorer.js";
|
|
2
2
|
import { SweepOps } from "../oc/sweep-ops.js";
|
|
3
|
-
import {
|
|
3
|
+
import { WireExtendOps } from "../oc/wire-extend-ops.js";
|
|
4
4
|
import { FaceMaker2 } from "../oc/face-maker2.js";
|
|
5
5
|
import { ExtrudeBase } from "./extrude-base.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveParam } from "../core/param.js";
|
|
7
|
+
import { cutWithSceneObjects, wireFromSceneObjectEdges } from "../helpers/scene-helpers.js";
|
|
7
8
|
import { ThinFaceMaker } from "../oc/thin-face-maker.js";
|
|
8
9
|
import { requireShapes } from "../common/operand-check.js";
|
|
9
10
|
export class Sweep extends ExtrudeBase {
|
|
10
11
|
_path;
|
|
12
|
+
_extendStart;
|
|
13
|
+
_extendEnd;
|
|
11
14
|
constructor(path, extrudable) {
|
|
12
15
|
super(extrudable);
|
|
13
16
|
this._path = path;
|
|
@@ -15,6 +18,23 @@ export class Sweep extends ExtrudeBase {
|
|
|
15
18
|
get path() {
|
|
16
19
|
return this._path;
|
|
17
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Extends the swept solid beyond the path at the given end by `amount`,
|
|
23
|
+
* continuing straight along the path's tangent there. Chain twice for both ends.
|
|
24
|
+
*/
|
|
25
|
+
extend(side, amount) {
|
|
26
|
+
const value = resolveParam(amount);
|
|
27
|
+
if (side === "start") {
|
|
28
|
+
this._extendStart = value;
|
|
29
|
+
}
|
|
30
|
+
else if (side === "end") {
|
|
31
|
+
this._extendEnd = value;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new Error(`sweep.extend: side must be 'start' or 'end', got '${side}'.`);
|
|
35
|
+
}
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
18
38
|
validate() {
|
|
19
39
|
requireShapes(this._path, "path", "sweep");
|
|
20
40
|
}
|
|
@@ -152,10 +172,14 @@ export class Sweep extends ExtrudeBase {
|
|
|
152
172
|
this.finalizeAndFuse(solids, classified, context, { skipSimplify: true });
|
|
153
173
|
}
|
|
154
174
|
getSpineWire(pathObj) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
let wire = wireFromSceneObjectEdges(pathObj, "sweep path");
|
|
176
|
+
if (this._extendStart !== undefined) {
|
|
177
|
+
wire = WireExtendOps.extendWire(wire, "start", this._extendStart);
|
|
178
|
+
}
|
|
179
|
+
if (this._extendEnd !== undefined) {
|
|
180
|
+
wire = WireExtendOps.extendWire(wire, "end", this._extendEnd);
|
|
181
|
+
}
|
|
182
|
+
return wire;
|
|
159
183
|
}
|
|
160
184
|
getDependencies() {
|
|
161
185
|
const deps = [];
|
|
@@ -172,7 +196,11 @@ export class Sweep extends ExtrudeBase {
|
|
|
172
196
|
? (remap.get(this.extrudable) || this.extrudable)
|
|
173
197
|
: undefined;
|
|
174
198
|
const path = remap.get(this._path) || this._path;
|
|
175
|
-
|
|
199
|
+
const copy = new Sweep(path, extrudable);
|
|
200
|
+
copy.syncWith(this);
|
|
201
|
+
copy._extendStart = this._extendStart;
|
|
202
|
+
copy._extendEnd = this._extendEnd;
|
|
203
|
+
return copy;
|
|
176
204
|
}
|
|
177
205
|
compareTo(other) {
|
|
178
206
|
if (!(other instanceof Sweep)) {
|
|
@@ -187,6 +215,12 @@ export class Sweep extends ExtrudeBase {
|
|
|
187
215
|
if (!this.extrudable.compareTo(other.extrudable)) {
|
|
188
216
|
return false;
|
|
189
217
|
}
|
|
218
|
+
if (this._extendStart !== other._extendStart) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (this._extendEnd !== other._extendEnd) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
190
224
|
return true;
|
|
191
225
|
}
|
|
192
226
|
getType() {
|
|
@@ -198,6 +232,8 @@ export class Sweep extends ExtrudeBase {
|
|
|
198
232
|
extrudable: this.extrudable.serialize(),
|
|
199
233
|
operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
|
|
200
234
|
thin: this._thin,
|
|
235
|
+
extendStart: this._extendStart,
|
|
236
|
+
extendEnd: this._extendEnd,
|
|
201
237
|
...this.serializePickFields(),
|
|
202
238
|
};
|
|
203
239
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { GeometrySceneObject } from "../features/2d/geometry.js";
|
|
2
|
+
import { Sketch } from "../features/2d/sketch.js";
|
|
3
|
+
import { FrozenGeometry } from "../features/frozen-geometry.js";
|
|
1
4
|
export function cloneWithTransform(objects, transform, container) {
|
|
2
5
|
const visited = new Set();
|
|
3
6
|
const ordered = [];
|
|
@@ -27,17 +30,71 @@ export function cloneWithTransform(objects, transform, container) {
|
|
|
27
30
|
for (const obj of objects) {
|
|
28
31
|
collectDeps(obj);
|
|
29
32
|
}
|
|
33
|
+
// Sketch geometry whose owning sketch is part of the clone set rebuilds
|
|
34
|
+
// normally (on the cloned, transformed plane). But a repeated feature can
|
|
35
|
+
// also reference a bare curve that lives in a sketch *outside* the clone set
|
|
36
|
+
// (e.g. a sweep path `otherSketch.regions.foo`). That curve has no sketch
|
|
37
|
+
// ancestor among the clones, so it can't rebuild — `this.sketch` would be
|
|
38
|
+
// null. Freeze such geometry's built edges instead (see FrozenGeometry).
|
|
39
|
+
const clonedSketches = new Set();
|
|
40
|
+
for (const obj of ordered) {
|
|
41
|
+
if (obj instanceof Sketch) {
|
|
42
|
+
clonedSketches.add(obj);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const owningSketchInCloneSet = (geom) => {
|
|
46
|
+
let parent = geom.getParent();
|
|
47
|
+
while (parent && !(parent instanceof Sketch)) {
|
|
48
|
+
parent = parent.getParent();
|
|
49
|
+
}
|
|
50
|
+
return parent ? clonedSketches.has(parent) : false;
|
|
51
|
+
};
|
|
52
|
+
const isOrphanGeometry = (obj) => obj instanceof GeometrySceneObject && !owningSketchInCloneSet(obj);
|
|
53
|
+
// A boundary orphan is one a kept (non-orphan) clone — or a repeat root —
|
|
54
|
+
// actually references. Those get frozen; orphan geometry reachable only as
|
|
55
|
+
// the source curves of another orphan (e.g. the lines behind a fillet) is
|
|
56
|
+
// dropped, since the frozen edges already incorporate it.
|
|
57
|
+
const roots = new Set(objects);
|
|
58
|
+
const frozenSources = new Set();
|
|
59
|
+
for (const obj of ordered) {
|
|
60
|
+
if (isOrphanGeometry(obj)) {
|
|
61
|
+
if (roots.has(obj)) {
|
|
62
|
+
frozenSources.add(obj);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const dep of obj.getDependencies()) {
|
|
67
|
+
if (isOrphanGeometry(dep)) {
|
|
68
|
+
frozenSources.add(dep);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
30
72
|
const remap = new Map();
|
|
31
73
|
const allCloned = [];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
remap.set(obj, copy);
|
|
74
|
+
const registerClone = (source, copy) => {
|
|
75
|
+
remap.set(source, copy);
|
|
35
76
|
copy.setTransform(transform);
|
|
36
|
-
copy.setCloneSource(
|
|
37
|
-
if (
|
|
77
|
+
copy.setCloneSource(source);
|
|
78
|
+
if (source.isReusable()) {
|
|
38
79
|
copy.reusable();
|
|
39
80
|
}
|
|
40
81
|
allCloned.push(copy);
|
|
82
|
+
};
|
|
83
|
+
// Freeze boundary orphans first so referring clones remap onto them. They
|
|
84
|
+
// hold baked edges and need no sketch parent, so they live under the
|
|
85
|
+
// container directly.
|
|
86
|
+
for (const source of frozenSources) {
|
|
87
|
+
const frozen = new FrozenGeometry(source);
|
|
88
|
+
registerClone(source, frozen);
|
|
89
|
+
container.addChildObject(frozen);
|
|
90
|
+
}
|
|
91
|
+
for (const obj of ordered) {
|
|
92
|
+
if (isOrphanGeometry(obj)) {
|
|
93
|
+
// Frozen above, or dropped as the source of a frozen curve.
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const copy = obj.createCopy(remap);
|
|
97
|
+
registerClone(obj, copy);
|
|
41
98
|
const parent = obj.getParent();
|
|
42
99
|
if (parent && remap.has(parent)) {
|
|
43
100
|
remap.get(parent).addChildObject(copy);
|
|
@@ -3,6 +3,18 @@ import { Shape } from "../common/shapes.js";
|
|
|
3
3
|
import { Plane } from "../math/plane.js";
|
|
4
4
|
import { ShapeHistory } from "../common/shape-history-tracker.js";
|
|
5
5
|
import { Profiler } from "../common/profiler.js";
|
|
6
|
+
import { Wire } from "../common/wire.js";
|
|
7
|
+
/**
|
|
8
|
+
* Collects every edge of the object's shapes into one connected wire.
|
|
9
|
+
* Used by path-style inputs that need a single curve (sweep paths).
|
|
10
|
+
*/
|
|
11
|
+
export declare function wireFromSceneObjectEdges(obj: SceneObject, label: string): Wire;
|
|
12
|
+
/**
|
|
13
|
+
* Groups the object's edges into connected chains — one wire per chain.
|
|
14
|
+
* Used by inputs that may carry several separate curves (e.g. a loft guide
|
|
15
|
+
* sketch holding a curve and its mirror).
|
|
16
|
+
*/
|
|
17
|
+
export declare function wiresFromSceneObjectEdges(obj: SceneObject, label: string): Wire[];
|
|
6
18
|
export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[], opts?: {
|
|
7
19
|
glue?: 'full' | 'shift';
|
|
8
20
|
recordHistoryFor?: SceneObject;
|
|
@@ -7,6 +7,32 @@ import { Face } from "../common/face.js";
|
|
|
7
7
|
import { Edge } from "../common/edge.js";
|
|
8
8
|
import { getOC } from "../oc/init.js";
|
|
9
9
|
import { ColorTransfer } from "../oc/color-transfer.js";
|
|
10
|
+
import { WireOps } from "../oc/wire-ops.js";
|
|
11
|
+
/**
|
|
12
|
+
* Collects every edge of the object's shapes into one connected wire.
|
|
13
|
+
* Used by path-style inputs that need a single curve (sweep paths).
|
|
14
|
+
*/
|
|
15
|
+
export function wireFromSceneObjectEdges(obj, label) {
|
|
16
|
+
const shapes = obj.getShapes({ excludeMeta: false });
|
|
17
|
+
const edges = shapes.flatMap(s => s.getSubShapes('edge'));
|
|
18
|
+
if (edges.length === 0) {
|
|
19
|
+
throw new Error(`${label} has no edges to build a curve from.`);
|
|
20
|
+
}
|
|
21
|
+
return WireOps.makeWireFromEdges(edges);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Groups the object's edges into connected chains — one wire per chain.
|
|
25
|
+
* Used by inputs that may carry several separate curves (e.g. a loft guide
|
|
26
|
+
* sketch holding a curve and its mirror).
|
|
27
|
+
*/
|
|
28
|
+
export function wiresFromSceneObjectEdges(obj, label) {
|
|
29
|
+
const shapes = obj.getShapes({ excludeMeta: false });
|
|
30
|
+
const edges = shapes.flatMap(s => s.getSubShapes('edge'));
|
|
31
|
+
if (edges.length === 0) {
|
|
32
|
+
throw new Error(`${label} has no edges to build a curve from.`);
|
|
33
|
+
}
|
|
34
|
+
return WireOps.connectEdgesToWires(edges);
|
|
35
|
+
}
|
|
10
36
|
export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
|
|
11
37
|
const p = opts?.profiler;
|
|
12
38
|
const modified = [];
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Global B-spline interpolation
|
|
3
|
-
*
|
|
4
|
-
* solved densely. The resulting curve passes through every input
|
|
5
|
-
* is C2 (for the cubic case).
|
|
2
|
+
* Global B-spline interpolation (The NURBS Book, §9.2.1–9.2.2): chord-length
|
|
3
|
+
* or caller-supplied parameters, knots by averaging, and a banded linear
|
|
4
|
+
* system solved densely. The resulting curve passes through every input
|
|
5
|
+
* point and is C2 (for the cubic case). Optional end-derivative constraints
|
|
6
|
+
* (§9.2.2) pin the takeoff/arrival tangent vectors exactly — used by the
|
|
7
|
+
* loft end conditions to control how a skinned surface leaves its profiles.
|
|
6
8
|
*
|
|
7
9
|
* Exists because fluidcad-ocjs currently miscompiles the `Geom2dAPI` fitting
|
|
8
10
|
* classes (both their constructors and `Init` produce corrupted curves);
|
|
9
|
-
* `Geom2d_BSplineCurve`
|
|
10
|
-
* poles/knots ourselves and hand them over.
|
|
11
|
+
* the `Geom_BSplineCurve`/`Geom2d_BSplineCurve` array constructors are
|
|
12
|
+
* unaffected, so we compute the poles/knots ourselves and hand them over.
|
|
11
13
|
*/
|
|
12
14
|
export interface XY {
|
|
13
15
|
x: number;
|
|
@@ -21,6 +23,31 @@ export interface BSpline2dData {
|
|
|
21
23
|
multiplicities: number[];
|
|
22
24
|
degree: number;
|
|
23
25
|
}
|
|
26
|
+
/** Dimension-agnostic interpolation result; each pole is one point (length = dimension). */
|
|
27
|
+
export interface BSplineCurveData {
|
|
28
|
+
poles: number[][];
|
|
29
|
+
/** Distinct knot values (OCC convention). */
|
|
30
|
+
knots: number[];
|
|
31
|
+
/** Multiplicity per distinct knot. */
|
|
32
|
+
multiplicities: number[];
|
|
33
|
+
degree: number;
|
|
34
|
+
}
|
|
35
|
+
/** Largest knot span index whose half-open interval contains t. */
|
|
36
|
+
export declare function findSpan(poleCount: number, degree: number, t: number, flatKnots: number[]): number;
|
|
37
|
+
/** Nonzero basis function values N[span-degree .. span] at t (Cox–de Boor). */
|
|
38
|
+
export declare function basisFunctions(span: number, t: number, degree: number, flatKnots: number[]): number[];
|
|
39
|
+
/**
|
|
40
|
+
* Interpolates points (of any dimension) with a B-spline of degree up to 3,
|
|
41
|
+
* passing exactly through every point at the given parameters. Optional end
|
|
42
|
+
* derivatives constrain the curve's takeoff / arrival vectors (with respect
|
|
43
|
+
* to the same parameterization as `params`), adding one pole per constraint
|
|
44
|
+
* (The NURBS Book §9.2.2).
|
|
45
|
+
*
|
|
46
|
+
* With an end derivative D at a clamped end, the neighbouring pole is pinned
|
|
47
|
+
* directly (P1 = P0 + D·Δu/p and its mirror), so the constraint is exact and
|
|
48
|
+
* the linear system only solves for the remaining interior poles.
|
|
49
|
+
*/
|
|
50
|
+
export declare function interpolateWithDerivatives(points: ReadonlyArray<ReadonlyArray<number>>, params: ReadonlyArray<number>, startDerivative?: ReadonlyArray<number>, endDerivative?: ReadonlyArray<number>): BSplineCurveData;
|
|
24
51
|
/**
|
|
25
52
|
* Interpolates the given planar points with a B-spline of degree up to 3
|
|
26
53
|
* (lower for 2 or 3 points). The curve passes exactly through every point,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Global B-spline interpolation
|
|
3
|
-
*
|
|
4
|
-
* solved densely. The resulting curve passes through every input
|
|
5
|
-
* is C2 (for the cubic case).
|
|
2
|
+
* Global B-spline interpolation (The NURBS Book, §9.2.1–9.2.2): chord-length
|
|
3
|
+
* or caller-supplied parameters, knots by averaging, and a banded linear
|
|
4
|
+
* system solved densely. The resulting curve passes through every input
|
|
5
|
+
* point and is C2 (for the cubic case). Optional end-derivative constraints
|
|
6
|
+
* (§9.2.2) pin the takeoff/arrival tangent vectors exactly — used by the
|
|
7
|
+
* loft end conditions to control how a skinned surface leaves its profiles.
|
|
6
8
|
*
|
|
7
9
|
* Exists because fluidcad-ocjs currently miscompiles the `Geom2dAPI` fitting
|
|
8
10
|
* classes (both their constructors and `Init` produce corrupted curves);
|
|
9
|
-
* `Geom2d_BSplineCurve`
|
|
10
|
-
* poles/knots ourselves and hand them over.
|
|
11
|
+
* the `Geom_BSplineCurve`/`Geom2d_BSplineCurve` array constructors are
|
|
12
|
+
* unaffected, so we compute the poles/knots ourselves and hand them over.
|
|
11
13
|
*/
|
|
12
14
|
/** Largest knot span index whose half-open interval contains t. */
|
|
13
|
-
function findSpan(poleCount, degree, t, flatKnots) {
|
|
15
|
+
export function findSpan(poleCount, degree, t, flatKnots) {
|
|
14
16
|
const n = poleCount - 1;
|
|
15
17
|
if (t >= flatKnots[n + 1]) {
|
|
16
18
|
return n;
|
|
@@ -30,7 +32,7 @@ function findSpan(poleCount, degree, t, flatKnots) {
|
|
|
30
32
|
return mid;
|
|
31
33
|
}
|
|
32
34
|
/** Nonzero basis function values N[span-degree .. span] at t (Cox–de Boor). */
|
|
33
|
-
function basisFunctions(span, t, degree, flatKnots) {
|
|
35
|
+
export function basisFunctions(span, t, degree, flatKnots) {
|
|
34
36
|
const values = [1];
|
|
35
37
|
const left = [];
|
|
36
38
|
const right = [];
|
|
@@ -47,49 +49,73 @@ function basisFunctions(span, t, degree, flatKnots) {
|
|
|
47
49
|
}
|
|
48
50
|
return values;
|
|
49
51
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Gaussian elimination restricted to the matrix band; solves A·x = b for
|
|
54
|
+
* every RHS column at once. Mutates its inputs.
|
|
55
|
+
*
|
|
56
|
+
* B-spline collocation matrices are banded (bandwidth ≤ degree) and totally
|
|
57
|
+
* positive, so elimination without pivoting is stable (de Boor) — and the
|
|
58
|
+
* band restriction turns the O(n³) dense solve into O(n·b²), which is what
|
|
59
|
+
* makes dense-sample curve approximation affordable.
|
|
60
|
+
*/
|
|
61
|
+
function solveBanded(matrix, rhsColumns) {
|
|
52
62
|
const n = matrix.length;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
let lowerBandwidth = 0;
|
|
64
|
+
let upperBandwidth = 0;
|
|
65
|
+
for (let row = 0; row < n; row++) {
|
|
66
|
+
for (let col = 0; col < n; col++) {
|
|
67
|
+
if (matrix[row][col] !== 0) {
|
|
68
|
+
lowerBandwidth = Math.max(lowerBandwidth, row - col);
|
|
69
|
+
upperBandwidth = Math.max(upperBandwidth, col - row);
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
|
-
|
|
72
|
+
}
|
|
73
|
+
for (let col = 0; col < n; col++) {
|
|
74
|
+
if (Math.abs(matrix[col][col]) < 1e-14) {
|
|
61
75
|
throw new Error("B-spline interpolation: singular system (degenerate or duplicate points)");
|
|
62
76
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
[rhsY[pivot], rhsY[col]] = [rhsY[col], rhsY[pivot]];
|
|
67
|
-
}
|
|
68
|
-
for (let row = col + 1; row < n; row++) {
|
|
77
|
+
const lastRow = Math.min(n - 1, col + lowerBandwidth);
|
|
78
|
+
const lastCol = Math.min(n - 1, col + upperBandwidth);
|
|
79
|
+
for (let row = col + 1; row <= lastRow; row++) {
|
|
69
80
|
const factor = matrix[row][col] / matrix[col][col];
|
|
70
81
|
if (factor === 0) {
|
|
71
82
|
continue;
|
|
72
83
|
}
|
|
73
|
-
for (let k = col; k
|
|
84
|
+
for (let k = col; k <= lastCol; k++) {
|
|
74
85
|
matrix[row][k] -= factor * matrix[col][k];
|
|
75
86
|
}
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
for (const rhs of rhsColumns) {
|
|
88
|
+
rhs[row] -= factor * rhs[col];
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
|
-
const
|
|
81
|
-
const y = new Array(n);
|
|
92
|
+
const solutions = rhsColumns.map(() => new Array(n));
|
|
82
93
|
for (let row = n - 1; row >= 0; row--) {
|
|
83
|
-
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
const lastCol = Math.min(n - 1, row + upperBandwidth);
|
|
95
|
+
for (let d = 0; d < rhsColumns.length; d++) {
|
|
96
|
+
let sum = rhsColumns[d][row];
|
|
97
|
+
for (let k = row + 1; k <= lastCol; k++) {
|
|
98
|
+
sum -= matrix[row][k] * solutions[d][k];
|
|
99
|
+
}
|
|
100
|
+
solutions[d][row] = sum / matrix[row][row];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return solutions;
|
|
104
|
+
}
|
|
105
|
+
/** Converts a flat knot vector to OCC's distinct-knots + multiplicities. */
|
|
106
|
+
function toDistinctKnots(flatKnots) {
|
|
107
|
+
const knots = [];
|
|
108
|
+
const multiplicities = [];
|
|
109
|
+
for (const knot of flatKnots) {
|
|
110
|
+
if (knots.length > 0 && knot === knots[knots.length - 1]) {
|
|
111
|
+
multiplicities[multiplicities.length - 1]++;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
knots.push(knot);
|
|
115
|
+
multiplicities.push(1);
|
|
88
116
|
}
|
|
89
|
-
x[row] = sumX / matrix[row][row];
|
|
90
|
-
y[row] = sumY / matrix[row][row];
|
|
91
117
|
}
|
|
92
|
-
return {
|
|
118
|
+
return { knots, multiplicities };
|
|
93
119
|
}
|
|
94
120
|
/** Drops consecutive points closer than a millionth of the total chord. */
|
|
95
121
|
function dedupe(points) {
|
|
@@ -118,6 +144,127 @@ function dedupe(points) {
|
|
|
118
144
|
}
|
|
119
145
|
return result;
|
|
120
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Interpolates points (of any dimension) with a B-spline of degree up to 3,
|
|
149
|
+
* passing exactly through every point at the given parameters. Optional end
|
|
150
|
+
* derivatives constrain the curve's takeoff / arrival vectors (with respect
|
|
151
|
+
* to the same parameterization as `params`), adding one pole per constraint
|
|
152
|
+
* (The NURBS Book §9.2.2).
|
|
153
|
+
*
|
|
154
|
+
* With an end derivative D at a clamped end, the neighbouring pole is pinned
|
|
155
|
+
* directly (P1 = P0 + D·Δu/p and its mirror), so the constraint is exact and
|
|
156
|
+
* the linear system only solves for the remaining interior poles.
|
|
157
|
+
*/
|
|
158
|
+
export function interpolateWithDerivatives(points, params, startDerivative, endDerivative) {
|
|
159
|
+
const pointCount = points.length;
|
|
160
|
+
if (pointCount < 2) {
|
|
161
|
+
throw new Error("B-spline interpolation needs at least two points");
|
|
162
|
+
}
|
|
163
|
+
if (params.length !== pointCount) {
|
|
164
|
+
throw new Error("B-spline interpolation: params must match points in length");
|
|
165
|
+
}
|
|
166
|
+
for (let i = 1; i < pointCount; i++) {
|
|
167
|
+
if (!(params[i] > params[i - 1])) {
|
|
168
|
+
throw new Error("B-spline interpolation: params must be strictly increasing");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const dimension = points[0].length;
|
|
172
|
+
for (const derivative of [startDerivative, endDerivative]) {
|
|
173
|
+
if (derivative && derivative.length !== dimension) {
|
|
174
|
+
throw new Error("B-spline interpolation: derivative dimension must match points");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const constraintCount = (startDerivative ? 1 : 0) + (endDerivative ? 1 : 0);
|
|
178
|
+
const poleCount = pointCount + constraintCount;
|
|
179
|
+
const degree = Math.min(3, poleCount - 1);
|
|
180
|
+
// Knots by averaging (NURBS book eq. 9.8) over a parameter list padded with
|
|
181
|
+
// one extra copy of the first/last parameter per derivative constraint —
|
|
182
|
+
// this reduces to eq. 9.8 unconstrained and to eq. 9.11 with both ends
|
|
183
|
+
// constrained, and keeps the collocation matrix nonsingular in between.
|
|
184
|
+
const padded = [params[0]];
|
|
185
|
+
if (startDerivative) {
|
|
186
|
+
padded.push(params[0]);
|
|
187
|
+
}
|
|
188
|
+
for (let i = 1; i < pointCount - 1; i++) {
|
|
189
|
+
padded.push(params[i]);
|
|
190
|
+
}
|
|
191
|
+
if (endDerivative) {
|
|
192
|
+
padded.push(params[pointCount - 1]);
|
|
193
|
+
}
|
|
194
|
+
padded.push(params[pointCount - 1]);
|
|
195
|
+
const flatKnots = [];
|
|
196
|
+
for (let i = 0; i <= degree; i++) {
|
|
197
|
+
flatKnots.push(params[0]);
|
|
198
|
+
}
|
|
199
|
+
for (let j = 1; j <= poleCount - 1 - degree; j++) {
|
|
200
|
+
let sum = 0;
|
|
201
|
+
for (let i = j; i <= j + degree - 1; i++) {
|
|
202
|
+
sum += padded[i];
|
|
203
|
+
}
|
|
204
|
+
flatKnots.push(sum / degree);
|
|
205
|
+
}
|
|
206
|
+
for (let i = 0; i <= degree; i++) {
|
|
207
|
+
flatKnots.push(params[pointCount - 1]);
|
|
208
|
+
}
|
|
209
|
+
// Pinned poles: clamped endpoints, plus the derivative neighbours.
|
|
210
|
+
const poles = new Array(poleCount).fill(null);
|
|
211
|
+
poles[0] = [...points[0]];
|
|
212
|
+
poles[poleCount - 1] = [...points[pointCount - 1]];
|
|
213
|
+
if (startDerivative) {
|
|
214
|
+
const span = (flatKnots[degree + 1] - params[0]) / degree;
|
|
215
|
+
poles[1] = points[0].map((v, d) => v + startDerivative[d] * span);
|
|
216
|
+
}
|
|
217
|
+
if (endDerivative) {
|
|
218
|
+
const span = (params[pointCount - 1] - flatKnots[poleCount - 1]) / degree;
|
|
219
|
+
poles[poleCount - 2] = points[pointCount - 1].map((v, d) => v - endDerivative[d] * span);
|
|
220
|
+
}
|
|
221
|
+
const unknownIndices = [];
|
|
222
|
+
for (let i = 0; i < poleCount; i++) {
|
|
223
|
+
if (poles[i] === null) {
|
|
224
|
+
unknownIndices.push(i);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (unknownIndices.length > 0) {
|
|
228
|
+
const columnOfPole = new Map();
|
|
229
|
+
unknownIndices.forEach((poleIndex, column) => columnOfPole.set(poleIndex, column));
|
|
230
|
+
// One collocation row per interior point; pinned poles move to the RHS.
|
|
231
|
+
const matrix = [];
|
|
232
|
+
const rhsColumns = Array.from({ length: dimension }, () => []);
|
|
233
|
+
for (let k = 1; k <= pointCount - 2; k++) {
|
|
234
|
+
const row = new Array(unknownIndices.length).fill(0);
|
|
235
|
+
const rhs = [...points[k]];
|
|
236
|
+
const span = findSpan(poleCount, degree, params[k], flatKnots);
|
|
237
|
+
const values = basisFunctions(span, params[k], degree, flatKnots);
|
|
238
|
+
for (let j = 0; j <= degree; j++) {
|
|
239
|
+
const poleIndex = span - degree + j;
|
|
240
|
+
const pinned = poles[poleIndex];
|
|
241
|
+
if (pinned) {
|
|
242
|
+
for (let d = 0; d < dimension; d++) {
|
|
243
|
+
rhs[d] -= values[j] * pinned[d];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
row[columnOfPole.get(poleIndex)] = values[j];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
matrix.push(row);
|
|
251
|
+
for (let d = 0; d < dimension; d++) {
|
|
252
|
+
rhsColumns[d].push(rhs[d]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const solved = solveBanded(matrix, rhsColumns);
|
|
256
|
+
unknownIndices.forEach((poleIndex, column) => {
|
|
257
|
+
poles[poleIndex] = solved.map(solution => solution[column]);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const { knots, multiplicities } = toDistinctKnots(flatKnots);
|
|
261
|
+
return {
|
|
262
|
+
poles: poles,
|
|
263
|
+
knots,
|
|
264
|
+
multiplicities,
|
|
265
|
+
degree,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
121
268
|
/**
|
|
122
269
|
* Interpolates the given planar points with a B-spline of degree up to 3
|
|
123
270
|
* (lower for 2 or 3 points). The curve passes exactly through every point,
|
|
@@ -129,7 +276,6 @@ export function interpolateBSpline2d(rawPoints) {
|
|
|
129
276
|
throw new Error("B-spline interpolation needs at least two distinct points");
|
|
130
277
|
}
|
|
131
278
|
const n = points.length - 1;
|
|
132
|
-
const degree = Math.min(3, n);
|
|
133
279
|
// Chord-length parameters, normalized to [0, 1].
|
|
134
280
|
const params = new Array(points.length);
|
|
135
281
|
params[0] = 0;
|
|
@@ -141,54 +287,11 @@ export function interpolateBSpline2d(rawPoints) {
|
|
|
141
287
|
params[i] /= total;
|
|
142
288
|
}
|
|
143
289
|
params[n] = 1;
|
|
144
|
-
|
|
145
|
-
// per unconstrained pole.
|
|
146
|
-
const flatKnots = [];
|
|
147
|
-
for (let i = 0; i <= degree; i++) {
|
|
148
|
-
flatKnots.push(0);
|
|
149
|
-
}
|
|
150
|
-
for (let j = 1; j <= n - degree; j++) {
|
|
151
|
-
let sum = 0;
|
|
152
|
-
for (let i = j; i <= j + degree - 1; i++) {
|
|
153
|
-
sum += params[i];
|
|
154
|
-
}
|
|
155
|
-
flatKnots.push(sum / degree);
|
|
156
|
-
}
|
|
157
|
-
for (let i = 0; i <= degree; i++) {
|
|
158
|
-
flatKnots.push(1);
|
|
159
|
-
}
|
|
160
|
-
// Interpolation system: one basis row per parameter.
|
|
161
|
-
const matrix = [];
|
|
162
|
-
const rhsX = [];
|
|
163
|
-
const rhsY = [];
|
|
164
|
-
for (let k = 0; k <= n; k++) {
|
|
165
|
-
const row = new Array(n + 1).fill(0);
|
|
166
|
-
const span = findSpan(n + 1, degree, params[k], flatKnots);
|
|
167
|
-
const values = basisFunctions(span, params[k], degree, flatKnots);
|
|
168
|
-
for (let j = 0; j <= degree; j++) {
|
|
169
|
-
row[span - degree + j] = values[j];
|
|
170
|
-
}
|
|
171
|
-
matrix.push(row);
|
|
172
|
-
rhsX.push(points[k].x);
|
|
173
|
-
rhsY.push(points[k].y);
|
|
174
|
-
}
|
|
175
|
-
const solved = solveDense(matrix, rhsX, rhsY);
|
|
176
|
-
// Convert the flat knot vector to OCC's distinct-knots + multiplicities.
|
|
177
|
-
const knots = [];
|
|
178
|
-
const multiplicities = [];
|
|
179
|
-
for (const knot of flatKnots) {
|
|
180
|
-
if (knots.length > 0 && knot === knots[knots.length - 1]) {
|
|
181
|
-
multiplicities[multiplicities.length - 1]++;
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
knots.push(knot);
|
|
185
|
-
multiplicities.push(1);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
290
|
+
const data = interpolateWithDerivatives(points.map(p => [p.x, p.y]), params);
|
|
188
291
|
return {
|
|
189
|
-
poles:
|
|
190
|
-
knots,
|
|
191
|
-
multiplicities,
|
|
192
|
-
degree,
|
|
292
|
+
poles: data.poles.map(p => ({ x: p[0], y: p[1] })),
|
|
293
|
+
knots: data.knots,
|
|
294
|
+
multiplicities: data.multiplicities,
|
|
295
|
+
degree: data.degree,
|
|
193
296
|
};
|
|
194
297
|
}
|