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,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
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Wire } from "../../common/wire.js";
|
|
2
|
+
import { Solid } from "../../common/solid.js";
|
|
3
|
+
import { LoftEndCondition } from "./skinning.js";
|
|
4
|
+
export type { LoftConditionKind, LoftEndCondition } from "./skinning.js";
|
|
5
|
+
/**
|
|
6
|
+
* Loft with start/end conditions. OCC's `BRepOffsetAPI_ThruSections` cannot
|
|
7
|
+
* constrain end tangency, so this path skins the surface itself: profiles
|
|
8
|
+
* become compatible B-spline sections (`SectionCompatibility`), matching pole
|
|
9
|
+
* columns are interpolated along the loft with the end derivatives pinned,
|
|
10
|
+
* and the resulting surface is capped and sewn into a solid (`Skinning`).
|
|
11
|
+
*
|
|
12
|
+
* Conditions:
|
|
13
|
+
* - `normal`: the surface leaves the profile along the profile's plane
|
|
14
|
+
* normal — a perpendicular takeoff.
|
|
15
|
+
* - `tangent`: the surface leaves the profile inside the profile's plane,
|
|
16
|
+
* directed outward — profiles become tangency planes (e.g. a barrel from
|
|
17
|
+
* two stacked circles). Negative magnitudes direct it inward.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ConstrainedLoft {
|
|
20
|
+
static build(wires: Wire[], startCondition: LoftEndCondition | undefined, endCondition: LoftEndCondition | undefined): Solid[];
|
|
21
|
+
/**
|
|
22
|
+
* Thin-walled conditioned loft, assembled directly: outer wall, inner
|
|
23
|
+
* wall, and two planar ring caps sewn into one solid. Cutting the inner
|
|
24
|
+
* loft out of the outer with a boolean instead takes OCC seconds — two
|
|
25
|
+
* nearly-parallel B-spline shells are the pave-filler's worst case — and
|
|
26
|
+
* the walls are already exact offsets, so no boolean is needed.
|
|
27
|
+
*/
|
|
28
|
+
static buildThin(outerWires: Wire[], innerWires: Wire[], startCondition: LoftEndCondition | undefined, endCondition: LoftEndCondition | undefined): Solid[];
|
|
29
|
+
private static skinWires;
|
|
30
|
+
/** Planar ring between the outer and inner wall boundaries at one end. */
|
|
31
|
+
private static ringCap;
|
|
32
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Wire } from "../../common/wire.js";
|
|
2
|
+
import { FaceOps } from "../face-ops.js";
|
|
3
|
+
import { SectionCompatibility } from "./section-compatibility.js";
|
|
4
|
+
import { Skinning } from "./skinning.js";
|
|
5
|
+
/**
|
|
6
|
+
* Loft with start/end conditions. OCC's `BRepOffsetAPI_ThruSections` cannot
|
|
7
|
+
* constrain end tangency, so this path skins the surface itself: profiles
|
|
8
|
+
* become compatible B-spline sections (`SectionCompatibility`), matching pole
|
|
9
|
+
* columns are interpolated along the loft with the end derivatives pinned,
|
|
10
|
+
* and the resulting surface is capped and sewn into a solid (`Skinning`).
|
|
11
|
+
*
|
|
12
|
+
* Conditions:
|
|
13
|
+
* - `normal`: the surface leaves the profile along the profile's plane
|
|
14
|
+
* normal — a perpendicular takeoff.
|
|
15
|
+
* - `tangent`: the surface leaves the profile inside the profile's plane,
|
|
16
|
+
* directed outward — profiles become tangency planes (e.g. a barrel from
|
|
17
|
+
* two stacked circles). Negative magnitudes direct it inward.
|
|
18
|
+
*/
|
|
19
|
+
export class ConstrainedLoft {
|
|
20
|
+
static build(wires, startCondition, endCondition) {
|
|
21
|
+
const compatible = ConstrainedLoft.skinWires(wires);
|
|
22
|
+
const skinned = Skinning.skinSections(compatible, startCondition, endCondition);
|
|
23
|
+
return [Skinning.buildLoftSolid(compatible, skinned.grid, skinned.vBasis)];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Thin-walled conditioned loft, assembled directly: outer wall, inner
|
|
27
|
+
* wall, and two planar ring caps sewn into one solid. Cutting the inner
|
|
28
|
+
* loft out of the outer with a boolean instead takes OCC seconds — two
|
|
29
|
+
* nearly-parallel B-spline shells are the pave-filler's worst case — and
|
|
30
|
+
* the walls are already exact offsets, so no boolean is needed.
|
|
31
|
+
*/
|
|
32
|
+
static buildThin(outerWires, innerWires, startCondition, endCondition) {
|
|
33
|
+
const outer = ConstrainedLoft.skinWires(outerWires);
|
|
34
|
+
const inner = ConstrainedLoft.skinWires(innerWires);
|
|
35
|
+
const outerSkin = Skinning.skinSections(outer, startCondition, endCondition);
|
|
36
|
+
const innerSkin = Skinning.skinSections(inner, startCondition, endCondition);
|
|
37
|
+
const faces = [
|
|
38
|
+
...Skinning.sideFaces(outer, outerSkin.grid, outerSkin.vBasis),
|
|
39
|
+
...Skinning.sideFaces(inner, innerSkin.grid, innerSkin.vBasis),
|
|
40
|
+
ConstrainedLoft.ringCap(outer, outerSkin, inner, innerSkin, false),
|
|
41
|
+
ConstrainedLoft.ringCap(outer, outerSkin, inner, innerSkin, true),
|
|
42
|
+
];
|
|
43
|
+
return [Skinning.sewSolid(faces)];
|
|
44
|
+
}
|
|
45
|
+
static skinWires(wires) {
|
|
46
|
+
for (const wire of wires) {
|
|
47
|
+
if (!wire.isClosed()) {
|
|
48
|
+
throw new Error("Loft with start/end conditions requires closed profiles.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return SectionCompatibility.build(wires.map(w => w.getShape()));
|
|
52
|
+
}
|
|
53
|
+
/** Planar ring between the outer and inner wall boundaries at one end. */
|
|
54
|
+
static ringCap(outer, outerSkin, inner, innerSkin, isEnd) {
|
|
55
|
+
const column = (grid) => grid.map(row => row[isEnd ? row.length - 1 : 0]);
|
|
56
|
+
const outerWire = new Wire(Skinning.capWire(outer, column(outerSkin.grid)));
|
|
57
|
+
const innerWire = new Wire(Skinning.capWire(inner, column(innerSkin.grid)));
|
|
58
|
+
const ring = FaceOps.makeFaceWithHoles(outerWire, [innerWire]);
|
|
59
|
+
return FaceOps.fixFaceOrientation(ring).getShape();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Geom_BSplineCurve } from "ocjs-fluidcad";
|
|
2
|
+
import type { BSplineCurveData } from "../../math/bspline-interpolation.js";
|
|
3
|
+
/**
|
|
4
|
+
* Plain-JS mirror of a `Geom_BSplineCurve`. `weights` is null for polynomial
|
|
5
|
+
* curves; when present it holds one weight per pole (rational curve).
|
|
6
|
+
*/
|
|
7
|
+
export interface RationalBSplineData extends BSplineCurveData {
|
|
8
|
+
weights: number[] | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Moves B-spline curves between OCC handles and plain data, so the knot/pole
|
|
12
|
+
* bookkeeping of the loft pipeline (concatenation, unification, skinning) can
|
|
13
|
+
* run on ordinary arrays instead of chains of native calls.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CurveData {
|
|
16
|
+
static read(curve: Geom_BSplineCurve): RationalBSplineData;
|
|
17
|
+
static build(data: RationalBSplineData): Geom_BSplineCurve;
|
|
18
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getOC } from "../init.js";
|
|
2
|
+
import { NCollections } from "../ncollection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Moves B-spline curves between OCC handles and plain data, so the knot/pole
|
|
5
|
+
* bookkeeping of the loft pipeline (concatenation, unification, skinning) can
|
|
6
|
+
* run on ordinary arrays instead of chains of native calls.
|
|
7
|
+
*/
|
|
8
|
+
export class CurveData {
|
|
9
|
+
static read(curve) {
|
|
10
|
+
const poles = [];
|
|
11
|
+
for (let i = 1; i <= curve.NbPoles(); i++) {
|
|
12
|
+
const pole = curve.Pole(i);
|
|
13
|
+
poles.push([pole.X(), pole.Y(), pole.Z()]);
|
|
14
|
+
pole.delete();
|
|
15
|
+
}
|
|
16
|
+
const knots = [];
|
|
17
|
+
const multiplicities = [];
|
|
18
|
+
for (let i = 1; i <= curve.NbKnots(); i++) {
|
|
19
|
+
knots.push(curve.Knot(i));
|
|
20
|
+
multiplicities.push(curve.Multiplicity(i));
|
|
21
|
+
}
|
|
22
|
+
let weights = null;
|
|
23
|
+
if (curve.IsRational()) {
|
|
24
|
+
weights = [];
|
|
25
|
+
for (let i = 1; i <= curve.NbPoles(); i++) {
|
|
26
|
+
weights.push(curve.Weight(i));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { poles, weights, knots, multiplicities, degree: curve.Degree() };
|
|
30
|
+
}
|
|
31
|
+
static build(data) {
|
|
32
|
+
const oc = getOC();
|
|
33
|
+
const [poles, disposePoles] = NCollections.toArray1Pnt(data.poles);
|
|
34
|
+
const [knots, disposeKnots] = NCollections.toArray1Double(data.knots);
|
|
35
|
+
const [multiplicities, disposeMultiplicities] = NCollections.toArray1Int(data.multiplicities);
|
|
36
|
+
try {
|
|
37
|
+
if (data.weights) {
|
|
38
|
+
const [weights, disposeWeights] = NCollections.toArray1Double(data.weights);
|
|
39
|
+
try {
|
|
40
|
+
return new oc.Geom_BSplineCurve(poles, weights, knots, multiplicities, data.degree, false, false);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
disposeWeights();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return new oc.Geom_BSplineCurve(poles, knots, multiplicities, data.degree, false);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
disposePoles();
|
|
50
|
+
disposeKnots();
|
|
51
|
+
disposeMultiplicities();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-data B-spline evaluation for the loft pipeline. Sections and guides
|
|
3
|
+
* live as pole/knot arrays between pipeline stages (see `CurveData`), and
|
|
4
|
+
* evaluating them directly in JS avoids rebuilding native curves just to
|
|
5
|
+
* sample a few points.
|
|
6
|
+
*/
|
|
7
|
+
/** Expands distinct knots + multiplicities into the flat knot sequence. */
|
|
8
|
+
export declare function flattenKnots(knots: ReadonlyArray<number>, multiplicities: ReadonlyArray<number>): number[];
|
|
9
|
+
export interface EvaluableBSpline {
|
|
10
|
+
degree: number;
|
|
11
|
+
knots: number[];
|
|
12
|
+
multiplicities: number[];
|
|
13
|
+
poles: number[][];
|
|
14
|
+
weights?: number[] | null;
|
|
15
|
+
}
|
|
16
|
+
/** Point on a (possibly rational) B-spline at parameter t. */
|
|
17
|
+
export declare function evaluateBSplinePoint(curve: EvaluableBSpline, t: number): number[];
|
|
18
|
+
/**
|
|
19
|
+
* Parameter in [low, high] whose evaluated point lies closest to `target`:
|
|
20
|
+
* dense sampling to bracket the minimum, then golden-section refinement.
|
|
21
|
+
* Works for any point-valued curve evaluation (plain-data or native).
|
|
22
|
+
*/
|
|
23
|
+
export declare function closestCurveParameter(evaluate: (t: number) => number[], target: ReadonlyArray<number>, low?: number, high?: number, samples?: number): number;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { findSpan, basisFunctions } from "../../math/bspline-interpolation.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plain-data B-spline evaluation for the loft pipeline. Sections and guides
|
|
4
|
+
* live as pole/knot arrays between pipeline stages (see `CurveData`), and
|
|
5
|
+
* evaluating them directly in JS avoids rebuilding native curves just to
|
|
6
|
+
* sample a few points.
|
|
7
|
+
*/
|
|
8
|
+
/** Expands distinct knots + multiplicities into the flat knot sequence. */
|
|
9
|
+
export function flattenKnots(knots, multiplicities) {
|
|
10
|
+
const flat = [];
|
|
11
|
+
for (let i = 0; i < knots.length; i++) {
|
|
12
|
+
for (let m = 0; m < multiplicities[i]; m++) {
|
|
13
|
+
flat.push(knots[i]);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return flat;
|
|
17
|
+
}
|
|
18
|
+
/** Point on a (possibly rational) B-spline at parameter t. */
|
|
19
|
+
export function evaluateBSplinePoint(curve, t) {
|
|
20
|
+
const flat = flattenKnots(curve.knots, curve.multiplicities);
|
|
21
|
+
const span = findSpan(curve.poles.length, curve.degree, t, flat);
|
|
22
|
+
const values = basisFunctions(span, t, curve.degree, flat);
|
|
23
|
+
const dimension = curve.poles[0].length;
|
|
24
|
+
const numerator = new Array(dimension).fill(0);
|
|
25
|
+
let denominator = 0;
|
|
26
|
+
for (let j = 0; j <= curve.degree; j++) {
|
|
27
|
+
const poleIndex = span - curve.degree + j;
|
|
28
|
+
const weight = curve.weights ? curve.weights[poleIndex] : 1;
|
|
29
|
+
const blend = values[j] * weight;
|
|
30
|
+
denominator += blend;
|
|
31
|
+
for (let d = 0; d < dimension; d++) {
|
|
32
|
+
numerator[d] += blend * curve.poles[poleIndex][d];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return numerator.map(v => v / denominator);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parameter in [low, high] whose evaluated point lies closest to `target`:
|
|
39
|
+
* dense sampling to bracket the minimum, then golden-section refinement.
|
|
40
|
+
* Works for any point-valued curve evaluation (plain-data or native).
|
|
41
|
+
*/
|
|
42
|
+
export function closestCurveParameter(evaluate, target, low = 0, high = 1, samples = 512) {
|
|
43
|
+
const distanceAt = (t) => {
|
|
44
|
+
const p = evaluate(t);
|
|
45
|
+
let sum = 0;
|
|
46
|
+
for (let d = 0; d < target.length; d++) {
|
|
47
|
+
sum += (p[d] - target[d]) * (p[d] - target[d]);
|
|
48
|
+
}
|
|
49
|
+
return sum;
|
|
50
|
+
};
|
|
51
|
+
let bestIndex = 0;
|
|
52
|
+
let bestDistance = Infinity;
|
|
53
|
+
for (let i = 0; i <= samples; i++) {
|
|
54
|
+
const d = distanceAt(low + ((high - low) * i) / samples);
|
|
55
|
+
if (d < bestDistance) {
|
|
56
|
+
bestDistance = d;
|
|
57
|
+
bestIndex = i;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let a = low + ((high - low) * Math.max(0, bestIndex - 1)) / samples;
|
|
61
|
+
let b = low + ((high - low) * Math.min(samples, bestIndex + 1)) / samples;
|
|
62
|
+
const phi = (Math.sqrt(5) - 1) / 2;
|
|
63
|
+
let mid1 = b - phi * (b - a);
|
|
64
|
+
let mid2 = a + phi * (b - a);
|
|
65
|
+
let d1 = distanceAt(mid1);
|
|
66
|
+
let d2 = distanceAt(mid2);
|
|
67
|
+
for (let iteration = 0; iteration < 40; iteration++) {
|
|
68
|
+
if (d1 <= d2) {
|
|
69
|
+
b = mid2;
|
|
70
|
+
mid2 = mid1;
|
|
71
|
+
d2 = d1;
|
|
72
|
+
mid1 = b - phi * (b - a);
|
|
73
|
+
d1 = distanceAt(mid1);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
a = mid1;
|
|
77
|
+
mid1 = mid2;
|
|
78
|
+
d1 = d2;
|
|
79
|
+
mid2 = a + phi * (b - a);
|
|
80
|
+
d2 = distanceAt(mid2);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return (a + b) / 2;
|
|
84
|
+
}
|