fluidcad 0.0.38 → 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/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/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/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/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 +11 -0
- package/lib/dist/oc/wire-ops.js +33 -0
- 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 +0 -10
- 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/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 +81 -0
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { setupOC, render, addToScene } from "../setup.js";
|
|
3
3
|
import sketch from "../../core/sketch.js";
|
|
4
4
|
import extrude from "../../core/extrude.js";
|
|
@@ -8,6 +8,7 @@ import cylinder from "../../core/cylinder.js";
|
|
|
8
8
|
import { rect } from "../../core/2d/index.js";
|
|
9
9
|
import { Shell } from "../../features/shell.js";
|
|
10
10
|
import { countShapes } from "../utils.js";
|
|
11
|
+
import { ShellOps } from "../../oc/shell-ops.js";
|
|
11
12
|
import { ShapeProps } from "../../oc/props.js";
|
|
12
13
|
import { face } from "../../filters/index.js";
|
|
13
14
|
describe("shell", () => {
|
|
@@ -263,4 +264,37 @@ describe("shell", () => {
|
|
|
263
264
|
expect(sel2.getShapes()).toHaveLength(0);
|
|
264
265
|
});
|
|
265
266
|
});
|
|
267
|
+
describe("failure surfacing", () => {
|
|
268
|
+
it("flags an error instead of silently serving the original solid when the offset fails", () => {
|
|
269
|
+
// OCCT's MakeThickSolid throws on geometry it cannot hollow (a wall
|
|
270
|
+
// thicker than a nearby feature, a cut opening onto a removed face,
|
|
271
|
+
// etc.). Force that failure deterministically and assert the feature
|
|
272
|
+
// surfaces it rather than swallowing it into a silent no-op.
|
|
273
|
+
const spy = vi.spyOn(ShellOps, "makeThickSolid").mockImplementation(() => {
|
|
274
|
+
throw new Error("Failed to create thick solid.");
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
sketch("xy", () => {
|
|
278
|
+
rect(100, 100);
|
|
279
|
+
});
|
|
280
|
+
extrude(50);
|
|
281
|
+
select(face().onPlane("xy", 50));
|
|
282
|
+
const s = shell(5);
|
|
283
|
+
const scene = render();
|
|
284
|
+
// The failure is now visible to the user instead of being swallowed.
|
|
285
|
+
expect(s.getError()).toBeTruthy();
|
|
286
|
+
expect(s.getError()).toContain("shell");
|
|
287
|
+
// The original solid is preserved (downstream features still have
|
|
288
|
+
// geometry) — the no-op fallback is intact, just no longer silent.
|
|
289
|
+
const solid = scene.getAllSceneObjects()
|
|
290
|
+
.flatMap(o => o.getShapes())
|
|
291
|
+
.find(sh => sh.getType() === "solid");
|
|
292
|
+
expect(solid).toBeDefined();
|
|
293
|
+
expect(ShapeProps.getProperties(solid.getShape()).volumeMm3).toBeCloseTo(100 * 100 * 50, 1);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
spy.mockRestore();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
266
300
|
});
|
|
@@ -478,4 +478,85 @@ describe("sweep", () => {
|
|
|
478
478
|
expect(radialExtent).toBeLessThan(30);
|
|
479
479
|
});
|
|
480
480
|
});
|
|
481
|
+
describe("extend", () => {
|
|
482
|
+
it("extends the run-out past the path end along the tangent", () => {
|
|
483
|
+
const profile = sketch("xy", () => {
|
|
484
|
+
circle(10);
|
|
485
|
+
});
|
|
486
|
+
const path = sketch("xz", () => {
|
|
487
|
+
vLine(50);
|
|
488
|
+
});
|
|
489
|
+
const s = sweep(path, profile).extend("end", 20);
|
|
490
|
+
render();
|
|
491
|
+
expect(s.getError()).toBeNull();
|
|
492
|
+
const shapes = s.getShapes();
|
|
493
|
+
expect(shapes).toHaveLength(1);
|
|
494
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
495
|
+
// Straight Z sweep with flat caps: Z extent = path length + extension.
|
|
496
|
+
const bbox = ShapeOps.getBoundingBox(shapes[0]);
|
|
497
|
+
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(70, 0);
|
|
498
|
+
});
|
|
499
|
+
it("extends the lead-in before the path start along the tangent", () => {
|
|
500
|
+
const profile = sketch("xy", () => {
|
|
501
|
+
circle(10);
|
|
502
|
+
});
|
|
503
|
+
const path = sketch("xz", () => {
|
|
504
|
+
vLine(50);
|
|
505
|
+
});
|
|
506
|
+
const s = sweep(path, profile).extend("start", 20);
|
|
507
|
+
render();
|
|
508
|
+
expect(s.getError()).toBeNull();
|
|
509
|
+
const bbox = ShapeOps.getBoundingBox(s.getShapes()[0]);
|
|
510
|
+
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(70, 0);
|
|
511
|
+
});
|
|
512
|
+
it("extends both ends when chained", () => {
|
|
513
|
+
const profile = sketch("xy", () => {
|
|
514
|
+
circle(10);
|
|
515
|
+
});
|
|
516
|
+
const path = sketch("xz", () => {
|
|
517
|
+
vLine(50);
|
|
518
|
+
});
|
|
519
|
+
const s = sweep(path, profile).extend("start", 10).extend("end", 20);
|
|
520
|
+
render();
|
|
521
|
+
expect(s.getError()).toBeNull();
|
|
522
|
+
const bbox = ShapeOps.getBoundingBox(s.getShapes()[0]);
|
|
523
|
+
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(80, 0);
|
|
524
|
+
});
|
|
525
|
+
it("adds volume proportional to the extension length", () => {
|
|
526
|
+
const profile = sketch("xy", () => {
|
|
527
|
+
circle(10); // diameter 10 ⇒ radius 5 ⇒ area 25π
|
|
528
|
+
});
|
|
529
|
+
const path = sketch("xz", () => {
|
|
530
|
+
vLine(50);
|
|
531
|
+
});
|
|
532
|
+
const s = sweep(path, profile).extend("end", 30);
|
|
533
|
+
render();
|
|
534
|
+
// Right cylinder: π·5²·(50 + 30).
|
|
535
|
+
const vol = ShapeProps.getProperties(s.getShapes()[0].getShape()).volumeMm3;
|
|
536
|
+
const expected = 25 * Math.PI * 80;
|
|
537
|
+
expect(vol).toBeGreaterThan(expected * 0.98);
|
|
538
|
+
expect(vol).toBeLessThan(expected * 1.02);
|
|
539
|
+
});
|
|
540
|
+
it("is a no-op for a non-positive amount", () => {
|
|
541
|
+
const profile = sketch("xy", () => {
|
|
542
|
+
circle(10);
|
|
543
|
+
});
|
|
544
|
+
const path = sketch("xz", () => {
|
|
545
|
+
vLine(50);
|
|
546
|
+
});
|
|
547
|
+
const s = sweep(path, profile).extend("end", 0);
|
|
548
|
+
render();
|
|
549
|
+
const bbox = ShapeOps.getBoundingBox(s.getShapes()[0]);
|
|
550
|
+
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(50, 0);
|
|
551
|
+
});
|
|
552
|
+
it("throws on an invalid side", () => {
|
|
553
|
+
const profile = sketch("xy", () => {
|
|
554
|
+
circle(10);
|
|
555
|
+
});
|
|
556
|
+
const path = sketch("xz", () => {
|
|
557
|
+
vLine(50);
|
|
558
|
+
});
|
|
559
|
+
expect(() => sweep(path, profile).extend("middle", 10)).toThrow();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
481
562
|
});
|
|
@@ -20,8 +20,10 @@ function solidsOf(e) {
|
|
|
20
20
|
}
|
|
21
21
|
describe("text", () => {
|
|
22
22
|
setupOC();
|
|
23
|
-
it("extrudes
|
|
24
|
-
|
|
23
|
+
it("extrudes text on a plane into a solid", () => {
|
|
24
|
+
sketch("xy", () => {
|
|
25
|
+
text("A").size(20);
|
|
26
|
+
});
|
|
25
27
|
const e = extrude(5);
|
|
26
28
|
render();
|
|
27
29
|
const solids = solidsOf(e);
|
|
@@ -53,19 +55,25 @@ describe("text", () => {
|
|
|
53
55
|
it("builds a letter with a counter (hole) such as 'o'", () => {
|
|
54
56
|
// Outer + inner contours must both build; FaceMaker2 drills the counter.
|
|
55
57
|
// We don't assert exact topology (font-dependent) — only a valid solid.
|
|
56
|
-
|
|
58
|
+
sketch("xy", () => {
|
|
59
|
+
text("o").size(20);
|
|
60
|
+
});
|
|
57
61
|
const e = extrude(4);
|
|
58
62
|
render();
|
|
59
63
|
expect(solidsOf(e).length).toBeGreaterThanOrEqual(1);
|
|
60
64
|
});
|
|
61
65
|
it("falls back to a default font when the named font is missing", () => {
|
|
62
|
-
|
|
66
|
+
sketch("xy", () => {
|
|
67
|
+
text("Z").size(20).font("NoSuchFont__XYZ");
|
|
68
|
+
});
|
|
63
69
|
const e = extrude(4);
|
|
64
70
|
render();
|
|
65
71
|
expect(solidsOf(e).length).toBeGreaterThanOrEqual(1);
|
|
66
72
|
});
|
|
67
73
|
it("honours font weight without throwing", () => {
|
|
68
|
-
|
|
74
|
+
sketch("xy", () => {
|
|
75
|
+
text("B").size(20).weight("bold");
|
|
76
|
+
});
|
|
69
77
|
const e = extrude(4);
|
|
70
78
|
render();
|
|
71
79
|
expect(solidsOf(e).length).toBeGreaterThanOrEqual(1);
|
|
@@ -88,7 +96,9 @@ describe("text", () => {
|
|
|
88
96
|
const root = process.env.FLUIDCAD_WORKSPACE_PATH;
|
|
89
97
|
fs.mkdirSync(join(root, "fonts"), { recursive: true });
|
|
90
98
|
fs.copyFileSync(src, join(root, "fonts", "test.ttf"));
|
|
91
|
-
|
|
99
|
+
sketch("xy", () => {
|
|
100
|
+
text("A").size(20).font("fonts/test.ttf");
|
|
101
|
+
});
|
|
92
102
|
const e = extrude(5);
|
|
93
103
|
render();
|
|
94
104
|
expect(solidsOf(e).length).toBeGreaterThanOrEqual(1);
|
|
@@ -187,9 +197,9 @@ describe("text along a path", () => {
|
|
|
187
197
|
expect(bbox.maxX).toBeLessThan(101);
|
|
188
198
|
expect(bbox.maxX).toBeGreaterThan(85);
|
|
189
199
|
});
|
|
190
|
-
it("follows a
|
|
200
|
+
it("follows a closed circular path and wraps around", () => {
|
|
191
201
|
// Text much longer than the circumference must wrap, not error.
|
|
192
|
-
const ring =
|
|
202
|
+
const ring = sketch("xy", () => circle(30)); // radius 15, circumference ~94
|
|
193
203
|
const t = text("WRAPPING ALL THE WAY AROUND", ring).size(8);
|
|
194
204
|
render();
|
|
195
205
|
expect(t.getError()).toBeFalsy();
|
|
@@ -232,12 +242,15 @@ describe("text along a path", () => {
|
|
|
232
242
|
expect(t.getError()).toMatch(/planar/i);
|
|
233
243
|
});
|
|
234
244
|
it("rejects path-only modifiers without a path", () => {
|
|
235
|
-
|
|
245
|
+
let t;
|
|
246
|
+
sketch("xy", () => {
|
|
247
|
+
t = text("Hi").offset(3);
|
|
248
|
+
});
|
|
236
249
|
render();
|
|
237
250
|
expect(t.getError()).toMatch(/offset/i);
|
|
238
251
|
});
|
|
239
252
|
it("sits on the outside of a closed circle by default", () => {
|
|
240
|
-
const ring =
|
|
253
|
+
const ring = sketch("xy", () => circle(100)); // radius 50
|
|
241
254
|
const t = text("OUTSIDE", ring).size(8);
|
|
242
255
|
render();
|
|
243
256
|
expect(t.getError()).toBeFalsy();
|
|
@@ -249,7 +262,7 @@ describe("text along a path", () => {
|
|
|
249
262
|
expect(mean).toBeGreaterThan(50);
|
|
250
263
|
});
|
|
251
264
|
it("moves closed-path text inside with flip", () => {
|
|
252
|
-
const ring =
|
|
265
|
+
const ring = sketch("xy", () => circle(100)); // radius 50
|
|
253
266
|
const t = text("INSIDE", ring).size(8).flip();
|
|
254
267
|
render();
|
|
255
268
|
expect(t.getError()).toBeFalsy();
|
|
@@ -297,12 +310,18 @@ describe("text along a path", () => {
|
|
|
297
310
|
expect(bbox.maxX).toBeGreaterThan(85);
|
|
298
311
|
});
|
|
299
312
|
it("rejects space-between alignment without a path", () => {
|
|
300
|
-
|
|
313
|
+
let t;
|
|
314
|
+
sketch("xy", () => {
|
|
315
|
+
t = text("Hi").align("space-between");
|
|
316
|
+
});
|
|
301
317
|
render();
|
|
302
318
|
expect(t.getError()).toMatch(/space-between/i);
|
|
303
319
|
});
|
|
304
320
|
it("rejects space-around alignment without a path", () => {
|
|
305
|
-
|
|
321
|
+
let t;
|
|
322
|
+
sketch("xy", () => {
|
|
323
|
+
t = text("Hi").align("space-around");
|
|
324
|
+
});
|
|
306
325
|
render();
|
|
307
326
|
expect(t.getError()).toMatch(/space-around/i);
|
|
308
327
|
});
|
|
@@ -337,7 +356,7 @@ describe("text along a path", () => {
|
|
|
337
356
|
expect(bbox.maxZ - bbox.minZ).toBeCloseTo(4, 0);
|
|
338
357
|
});
|
|
339
358
|
it("multiple texts can share one path", () => {
|
|
340
|
-
const ring =
|
|
359
|
+
const ring = sketch("xy", () => circle(100));
|
|
341
360
|
const outside = text("OUTSIDE", ring).size(8);
|
|
342
361
|
const inside = text("INSIDE", ring).size(8).flip();
|
|
343
362
|
render();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { interpolateBSpline2d } from "../../math/bspline-interpolation.js";
|
|
2
|
+
import { interpolateBSpline2d, interpolateWithDerivatives, } from "../../math/bspline-interpolation.js";
|
|
3
3
|
/** Evaluates the B-spline at t via Cox–de Boor (independent of OCC). */
|
|
4
4
|
function evaluate(data, t) {
|
|
5
5
|
const flat = [];
|
|
@@ -117,3 +117,99 @@ describe("interpolateBSpline2d", () => {
|
|
|
117
117
|
expect(data.poles.length).toBe(4);
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
|
+
/** Evaluates an N-dimensional B-spline at t via Cox–de Boor. */
|
|
121
|
+
function evaluateNd(data, t) {
|
|
122
|
+
const flat = [];
|
|
123
|
+
for (let i = 0; i < data.knots.length; i++) {
|
|
124
|
+
for (let m = 0; m < data.multiplicities[i]; m++) {
|
|
125
|
+
flat.push(data.knots[i]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const asXY = {
|
|
129
|
+
poles: data.poles.map(p => ({ x: p[0], y: p.length > 1 ? p[1] : 0 })),
|
|
130
|
+
knots: data.knots,
|
|
131
|
+
multiplicities: data.multiplicities,
|
|
132
|
+
degree: data.degree,
|
|
133
|
+
};
|
|
134
|
+
// Reuse the 2D evaluator per dimension pair to keep this helper simple.
|
|
135
|
+
const result = [];
|
|
136
|
+
const dimension = data.poles[0].length;
|
|
137
|
+
for (let d = 0; d < dimension; d += 2) {
|
|
138
|
+
const pair = {
|
|
139
|
+
...asXY,
|
|
140
|
+
poles: data.poles.map(p => ({ x: p[d], y: d + 1 < dimension ? p[d + 1] : 0 })),
|
|
141
|
+
};
|
|
142
|
+
const q = evaluate(pair, t);
|
|
143
|
+
result.push(q.x);
|
|
144
|
+
if (d + 1 < dimension) {
|
|
145
|
+
result.push(q.y);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
/** Central-difference derivative of the curve at t. */
|
|
151
|
+
function numericDerivative(data, t) {
|
|
152
|
+
const h = 1e-7;
|
|
153
|
+
const t0 = Math.max(data.knots[0], t - h);
|
|
154
|
+
const t1 = Math.min(data.knots[data.knots.length - 1], t + h);
|
|
155
|
+
const a = evaluateNd(data, t0);
|
|
156
|
+
const b = evaluateNd(data, t1);
|
|
157
|
+
return a.map((v, d) => (b[d] - v) / (t1 - t0));
|
|
158
|
+
}
|
|
159
|
+
describe("interpolateWithDerivatives", () => {
|
|
160
|
+
const points = [
|
|
161
|
+
[0, 0, 0],
|
|
162
|
+
[1, 2, 1],
|
|
163
|
+
[3, 1, 2],
|
|
164
|
+
[4, 4, 3],
|
|
165
|
+
];
|
|
166
|
+
const params = [0, 0.3, 0.7, 1];
|
|
167
|
+
it("passes through every point with both end derivatives constrained", () => {
|
|
168
|
+
const data = interpolateWithDerivatives(points, params, [0, 0, 5], [5, 0, 0]);
|
|
169
|
+
expect(data.degree).toBe(3);
|
|
170
|
+
expect(data.poles.length).toBe(points.length + 2);
|
|
171
|
+
for (let k = 0; k < points.length; k++) {
|
|
172
|
+
const q = evaluateNd(data, params[k]);
|
|
173
|
+
for (let d = 0; d < 3; d++) {
|
|
174
|
+
expect(q[d]).toBeCloseTo(points[k][d], 9);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
it("matches the requested end derivatives", () => {
|
|
179
|
+
const startD = [0, 0, 5];
|
|
180
|
+
const endD = [5, 0, 0];
|
|
181
|
+
const data = interpolateWithDerivatives(points, params, startD, endD);
|
|
182
|
+
const atStart = numericDerivative(data, 0);
|
|
183
|
+
const atEnd = numericDerivative(data, 1);
|
|
184
|
+
for (let d = 0; d < 3; d++) {
|
|
185
|
+
expect(atStart[d]).toBeCloseTo(startD[d], 4);
|
|
186
|
+
expect(atEnd[d]).toBeCloseTo(endD[d], 4);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
it("supports a single constrained end", () => {
|
|
190
|
+
const startD = [0, 0, 4];
|
|
191
|
+
const data = interpolateWithDerivatives(points, params, startD, undefined);
|
|
192
|
+
expect(data.poles.length).toBe(points.length + 1);
|
|
193
|
+
for (let k = 0; k < points.length; k++) {
|
|
194
|
+
const q = evaluateNd(data, params[k]);
|
|
195
|
+
for (let d = 0; d < 3; d++) {
|
|
196
|
+
expect(q[d]).toBeCloseTo(points[k][d], 9);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const atStart = numericDerivative(data, 0);
|
|
200
|
+
for (let d = 0; d < 3; d++) {
|
|
201
|
+
expect(atStart[d]).toBeCloseTo(startD[d], 4);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
it("reproduces a cubic Hermite segment for two points with derivatives", () => {
|
|
205
|
+
const data = interpolateWithDerivatives([[0, 0, 0], [1, 0, 0]], [0, 1], [0, 1, 0], [0, -1, 0]);
|
|
206
|
+
expect(data.degree).toBe(3);
|
|
207
|
+
expect(data.poles.length).toBe(4);
|
|
208
|
+
// Bézier form of the Hermite segment: P1 = P0 + D0/3, P2 = P3 - D1/3.
|
|
209
|
+
expect(data.poles[1][1]).toBeCloseTo(1 / 3, 9);
|
|
210
|
+
expect(data.poles[2][1]).toBeCloseTo(1 / 3, 9);
|
|
211
|
+
});
|
|
212
|
+
it("rejects non-increasing parameters", () => {
|
|
213
|
+
expect(() => interpolateWithDerivatives([[0, 0], [1, 1], [2, 2]], [0, 0.5, 0.5])).toThrow(/strictly increasing/);
|
|
214
|
+
});
|
|
215
|
+
});
|