okgeometry-api 1.11.1 → 1.15.0
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/dist/Brep.d.ts +222 -0
- package/dist/Brep.d.ts.map +1 -1
- package/dist/Brep.js +377 -0
- package/dist/Brep.js.map +1 -1
- package/dist/Mesh.d.ts +100 -2
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +258 -4
- package/dist/Mesh.js.map +1 -1
- package/dist/NurbsCurve.d.ts +78 -0
- package/dist/NurbsCurve.d.ts.map +1 -1
- package/dist/NurbsCurve.js +118 -0
- package/dist/NurbsCurve.js.map +1 -1
- package/dist/NurbsSurface.d.ts +23 -0
- package/dist/NurbsSurface.d.ts.map +1 -1
- package/dist/NurbsSurface.js +63 -15
- package/dist/NurbsSurface.js.map +1 -1
- package/dist/Polyline.d.ts +47 -0
- package/dist/Polyline.d.ts.map +1 -1
- package/dist/Polyline.js +45 -0
- package/dist/Polyline.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/wasm-base64.d.ts +1 -1
- package/dist/wasm-base64.d.ts.map +1 -1
- package/dist/wasm-base64.js +1 -1
- package/dist/wasm-base64.js.map +1 -1
- package/dist/wasm-bindings.d.ts +359 -0
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +595 -0
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +7 -3
- package/src/Brep.ts +458 -0
- package/src/Mesh.ts +300 -2
- package/src/NurbsCurve.ts +129 -0
- package/src/NurbsSurface.ts +65 -15
- package/src/Polyline.ts +145 -95
- package/src/index.ts +2 -0
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +308 -0
- package/src/wasm-bindings.js +615 -0
package/src/Mesh.ts
CHANGED
|
@@ -1277,7 +1277,12 @@ export class Mesh {
|
|
|
1277
1277
|
|
|
1278
1278
|
/**
|
|
1279
1279
|
* Build a planar arc from center, start, and end points.
|
|
1280
|
-
*
|
|
1280
|
+
* Without `sweepHint` the shortest signed sweep between start and end
|
|
1281
|
+
* around `normal` is used. With `sweepHint` (signed radians, typically the
|
|
1282
|
+
* sweep from the previous interactive update or a stored arc parameter),
|
|
1283
|
+
* the sweep candidate closest to the hint is chosen instead — this tracks
|
|
1284
|
+
* the user's winding direction continuously through 180° and up to just
|
|
1285
|
+
* under a full circle.
|
|
1281
1286
|
*/
|
|
1282
1287
|
static buildPlanarArcCenterStartEnd(
|
|
1283
1288
|
center: Point,
|
|
@@ -1285,6 +1290,7 @@ export class Mesh {
|
|
|
1285
1290
|
endPoint: Point,
|
|
1286
1291
|
normal: Vec3,
|
|
1287
1292
|
segments = 64,
|
|
1293
|
+
sweepHint?: number,
|
|
1288
1294
|
): PlanarArc {
|
|
1289
1295
|
const n = normal.normalize();
|
|
1290
1296
|
const minSegments = Math.max(2, Math.floor(segments));
|
|
@@ -1311,7 +1317,8 @@ export class Mesh {
|
|
|
1311
1317
|
const basis = Mesh.resolveArcBasis(center, radius, n);
|
|
1312
1318
|
const startAngle = Mesh.angleOnBasis(startVec, basis.uAxis, basis.vAxis);
|
|
1313
1319
|
const endAngleRaw = Mesh.angleOnBasis(endVec.scale(radius / endLen), basis.uAxis, basis.vAxis);
|
|
1314
|
-
const
|
|
1320
|
+
const shortestSweep = Mesh.normalizeSignedAngle(endAngleRaw - startAngle);
|
|
1321
|
+
const sweepAngle = Mesh.resolveSweepWithHint(shortestSweep, sweepHint);
|
|
1315
1322
|
if (Math.abs(sweepAngle) < 1e-9) {
|
|
1316
1323
|
return {
|
|
1317
1324
|
points: [],
|
|
@@ -1440,6 +1447,93 @@ export class Mesh {
|
|
|
1440
1447
|
};
|
|
1441
1448
|
}
|
|
1442
1449
|
|
|
1450
|
+
/**
|
|
1451
|
+
* Build a planar arc through three points: `startPoint`, a point the arc
|
|
1452
|
+
* passes through, and `endPoint`. All points are projected to the plane
|
|
1453
|
+
* through `startPoint` orthogonal to `normal`; the circle is the
|
|
1454
|
+
* circumcircle of the projected points and the sweep direction is the one
|
|
1455
|
+
* that passes through `throughPoint` (any sweep up to a full circle).
|
|
1456
|
+
* Returns null when the points are (near) collinear or coincident.
|
|
1457
|
+
*/
|
|
1458
|
+
static buildPlanarArcThreePoint(
|
|
1459
|
+
startPoint: Point,
|
|
1460
|
+
throughPoint: Point,
|
|
1461
|
+
endPoint: Point,
|
|
1462
|
+
normal: Vec3,
|
|
1463
|
+
segments = 64,
|
|
1464
|
+
): PlanarArc | null {
|
|
1465
|
+
const n = normal.normalize();
|
|
1466
|
+
const minSegments = Math.max(2, Math.floor(segments));
|
|
1467
|
+
const a = Mesh.projectPointToPlane(startPoint, startPoint, n);
|
|
1468
|
+
const b = Mesh.projectPointToPlane(throughPoint, startPoint, n);
|
|
1469
|
+
const c = Mesh.projectPointToPlane(endPoint, startPoint, n);
|
|
1470
|
+
|
|
1471
|
+
const ab = b.sub(a);
|
|
1472
|
+
const ac = c.sub(a);
|
|
1473
|
+
const scale = Math.max(ab.length(), ac.length());
|
|
1474
|
+
const cross = ab.cross(ac);
|
|
1475
|
+
const crossLen = cross.length();
|
|
1476
|
+
if (scale < 1e-12 || crossLen < 1e-9 * scale * scale) {
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Circumcenter: A + ((|ab|²·ac − |ac|²·ab) × (ab × ac)) / (2·|ab × ac|²)
|
|
1481
|
+
const numerator = ac.scale(ab.dot(ab)).sub(ab.scale(ac.dot(ac)));
|
|
1482
|
+
const center = a.add(numerator.cross(cross).scale(1 / (2 * cross.dot(cross))));
|
|
1483
|
+
const radius = a.sub(center).length();
|
|
1484
|
+
if (radius < 1e-12) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const basis = Mesh.resolveArcBasis(center, radius, n);
|
|
1489
|
+
const twoPi = Math.PI * 2;
|
|
1490
|
+
const startAngle = Mesh.angleOnBasis(a.sub(center), basis.uAxis, basis.vAxis);
|
|
1491
|
+
const throughAngle = Mesh.angleOnBasis(b.sub(center), basis.uAxis, basis.vAxis);
|
|
1492
|
+
const endAngleRaw = Mesh.angleOnBasis(c.sub(center), basis.uAxis, basis.vAxis);
|
|
1493
|
+
|
|
1494
|
+
const ccwSweep = ((endAngleRaw - startAngle) % twoPi + twoPi) % twoPi;
|
|
1495
|
+
const ccwThrough = ((throughAngle - startAngle) % twoPi + twoPi) % twoPi;
|
|
1496
|
+
if (ccwSweep < 1e-9 || twoPi - ccwSweep < 1e-9) {
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
const sweepAngle = ccwThrough <= ccwSweep ? ccwSweep : ccwSweep - twoPi;
|
|
1500
|
+
const endAngle = startAngle + sweepAngle;
|
|
1501
|
+
const arc = new Arc(center, radius, startAngle, endAngle, n);
|
|
1502
|
+
return {
|
|
1503
|
+
points: arc.sample(minSegments),
|
|
1504
|
+
center,
|
|
1505
|
+
radius,
|
|
1506
|
+
startAngle,
|
|
1507
|
+
endAngle,
|
|
1508
|
+
sweepAngle,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** Largest supported arc sweep — just under a full circle keeps arcs open. */
|
|
1513
|
+
private static readonly MAX_ARC_SWEEP = Math.PI * 2 - 1e-4;
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* Pick the sweep equivalent (shortest, +2π, or −2π) closest to `hint`,
|
|
1517
|
+
* clamped to just under a full circle. Without a hint the shortest sweep
|
|
1518
|
+
* is kept.
|
|
1519
|
+
*/
|
|
1520
|
+
private static resolveSweepWithHint(shortestSweep: number, hint?: number): number {
|
|
1521
|
+
if (hint === undefined || !Number.isFinite(hint)) {
|
|
1522
|
+
return shortestSweep;
|
|
1523
|
+
}
|
|
1524
|
+
const twoPi = Math.PI * 2;
|
|
1525
|
+
let best = shortestSweep;
|
|
1526
|
+
let bestDistance = Math.abs(shortestSweep - hint);
|
|
1527
|
+
for (const candidate of [shortestSweep + twoPi, shortestSweep - twoPi]) {
|
|
1528
|
+
const distance = Math.abs(candidate - hint);
|
|
1529
|
+
if (distance < bestDistance) {
|
|
1530
|
+
best = candidate;
|
|
1531
|
+
bestDistance = distance;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return Math.max(-Mesh.MAX_ARC_SWEEP, Math.min(Mesh.MAX_ARC_SWEEP, best));
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1443
1537
|
private static resolveArcBasis(
|
|
1444
1538
|
center: Point,
|
|
1445
1539
|
radius: number,
|
|
@@ -2901,6 +2995,179 @@ export class Mesh {
|
|
|
2901
2995
|
return { inside, outside };
|
|
2902
2996
|
}
|
|
2903
2997
|
|
|
2998
|
+
/**
|
|
2999
|
+
* Compute all transversal crossing points of a polyline with this mesh.
|
|
3000
|
+
* Works on OPEN meshes (sheets) — unlike `clipPolyline`, the mesh is NOT
|
|
3001
|
+
* required to be a closed volume.
|
|
3002
|
+
*
|
|
3003
|
+
* Hits are deduplicated (shared-edge / shared-vertex crossings merge into
|
|
3004
|
+
* one, keeping the smallest triangle index) and sorted by
|
|
3005
|
+
* (segmentIndex, segmentT). Coplanar sliding overlaps yield no hits.
|
|
3006
|
+
* Segment `i` runs from point `i` to point `(i + 1) % n`; open polylines
|
|
3007
|
+
* have n − 1 segments, closed ones n (the wrap segment).
|
|
3008
|
+
*
|
|
3009
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
3010
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
3011
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
3012
|
+
* @returns Crossings with the 3D point, segment index/parameter, and
|
|
3013
|
+
* triangle index
|
|
3014
|
+
*/
|
|
3015
|
+
intersectPolyline(
|
|
3016
|
+
polyline: Polyline | Point[],
|
|
3017
|
+
closed?: boolean,
|
|
3018
|
+
): { point: Point; segmentIndex: number; segmentT: number; triangleIndex: number }[] {
|
|
3019
|
+
ensureInit();
|
|
3020
|
+
const { flat, isClosed } = Mesh.flattenPolylineArg(polyline, closed, "Mesh.intersectPolyline");
|
|
3021
|
+
const buf = wasm.mesh_polyline_intersection_points(
|
|
3022
|
+
this._vertexCount,
|
|
3023
|
+
this._buffer,
|
|
3024
|
+
flat,
|
|
3025
|
+
isClosed,
|
|
3026
|
+
);
|
|
3027
|
+
if (buf.length === 0) {
|
|
3028
|
+
throw new Error(
|
|
3029
|
+
"Mesh.intersectPolyline failed — the mesh and polyline must be non-degenerate.",
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
const count = buf[0];
|
|
3033
|
+
const hits: { point: Point; segmentIndex: number; segmentT: number; triangleIndex: number }[] = [];
|
|
3034
|
+
for (let i = 0; i < count; i++) {
|
|
3035
|
+
const off = 1 + i * 6;
|
|
3036
|
+
hits.push({
|
|
3037
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
3038
|
+
segmentIndex: buf[off + 3],
|
|
3039
|
+
segmentT: buf[off + 4],
|
|
3040
|
+
triangleIndex: buf[off + 5],
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
return hits;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
/**
|
|
3047
|
+
* Split a polyline into pieces at every crossing with this mesh (open
|
|
3048
|
+
* sheets allowed). Hit points become the shared endpoints of adjacent
|
|
3049
|
+
* pieces; a closed loop is opened at the first crossing (N crossings → N
|
|
3050
|
+
* pieces), an open polyline yields N + 1 pieces. With no crossings the
|
|
3051
|
+
* original polyline comes back as one piece. Pieces are NOT classified —
|
|
3052
|
+
* use `clipPolyline` (closed volume, containment) or
|
|
3053
|
+
* `clipPolylineBySheet` (oriented sheet, signed side) for classification.
|
|
3054
|
+
*
|
|
3055
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
3056
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
3057
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
3058
|
+
* @returns Polyline pieces in order along the input
|
|
3059
|
+
*/
|
|
3060
|
+
splitPolyline(polyline: Polyline | Point[], closed?: boolean): Polyline[] {
|
|
3061
|
+
ensureInit();
|
|
3062
|
+
const { flat, isClosed } = Mesh.flattenPolylineArg(polyline, closed, "Mesh.splitPolyline");
|
|
3063
|
+
const buf = wasm.mesh_split_polyline(this._vertexCount, this._buffer, flat, isClosed);
|
|
3064
|
+
if (buf.length === 0) {
|
|
3065
|
+
throw new Error(
|
|
3066
|
+
"Mesh.splitPolyline failed — the mesh and polyline must be non-degenerate.",
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
return parsePolylineBuf(buf).map(pts => new Polyline(pts));
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/**
|
|
3073
|
+
* Clip a polyline by this mesh treated as an OPEN oriented sheet: the
|
|
3074
|
+
* polyline is split at every crossing and each piece is classified by the
|
|
3075
|
+
* SIGNED SIDE of the sheet — `inside` = in FRONT of the sheet normal,
|
|
3076
|
+
* `outside` = behind it (the mesh's stored winding defines the normals).
|
|
3077
|
+
*
|
|
3078
|
+
* This is side classification, NOT containment — unlike `clipPolyline`,
|
|
3079
|
+
* which requires a CLOSED volume: the sheet may be open, and pieces far
|
|
3080
|
+
* from it still classify deterministically by the nearest facet's normal.
|
|
3081
|
+
* Pieces lying ON the sheet default to `inside`. Adjacent same-side pieces
|
|
3082
|
+
* (e.g. from a touch, not a crossing) merge back into one run; closed
|
|
3083
|
+
* loops merge the run spanning the seam.
|
|
3084
|
+
*
|
|
3085
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
3086
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
3087
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
3088
|
+
*/
|
|
3089
|
+
clipPolylineBySheet(
|
|
3090
|
+
polyline: Polyline | Point[],
|
|
3091
|
+
closed?: boolean,
|
|
3092
|
+
): { inside: Polyline[]; outside: Polyline[] } {
|
|
3093
|
+
ensureInit();
|
|
3094
|
+
const { flat, isClosed } = Mesh.flattenPolylineArg(
|
|
3095
|
+
polyline,
|
|
3096
|
+
closed,
|
|
3097
|
+
"Mesh.clipPolylineBySheet",
|
|
3098
|
+
);
|
|
3099
|
+
const buf = wasm.mesh_clip_polyline_by_sheet(this._vertexCount, this._buffer, flat, isClosed);
|
|
3100
|
+
if (buf.length === 0) {
|
|
3101
|
+
throw new Error(
|
|
3102
|
+
"Mesh.clipPolylineBySheet failed — the sheet and polyline must be non-degenerate.",
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
// Two concatenated polyline groups: inside first, then outside — same
|
|
3106
|
+
// packing as clipPolyline.
|
|
3107
|
+
let idx = 0;
|
|
3108
|
+
const readGroup = (): Polyline[] => {
|
|
3109
|
+
const group: Polyline[] = [];
|
|
3110
|
+
const count = buf[idx++];
|
|
3111
|
+
for (let i = 0; i < count; i++) {
|
|
3112
|
+
const n = buf[idx++];
|
|
3113
|
+
const points: Point[] = [];
|
|
3114
|
+
for (let k = 0; k < n; k++) {
|
|
3115
|
+
points.push(new Point(buf[idx], buf[idx + 1], buf[idx + 2]));
|
|
3116
|
+
idx += 3;
|
|
3117
|
+
}
|
|
3118
|
+
if (points.length >= 2) group.push(new Polyline(points));
|
|
3119
|
+
}
|
|
3120
|
+
return group;
|
|
3121
|
+
};
|
|
3122
|
+
const inside = readGroup();
|
|
3123
|
+
const outside = readGroup();
|
|
3124
|
+
return { inside, outside };
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Flatten a Polyline-or-points argument into WASM coords + closed flag.
|
|
3129
|
+
*
|
|
3130
|
+
* Closed inputs following the duplicated-seam convention (terminal point
|
|
3131
|
+
* repeating the first) have the duplicate STRIPPED before crossing the
|
|
3132
|
+
* boundary — the kernel's closed-polyline encoding is seam-free (the wrap
|
|
3133
|
+
* segment is implicit), and a duplicated seam would add a zero-length wrap
|
|
3134
|
+
* segment / doubled seam vertex to split pieces.
|
|
3135
|
+
*/
|
|
3136
|
+
private static flattenPolylineArg(
|
|
3137
|
+
polyline: Polyline | Point[],
|
|
3138
|
+
closed: boolean | undefined,
|
|
3139
|
+
context: string,
|
|
3140
|
+
): { flat: Float64Array; isClosed: boolean } {
|
|
3141
|
+
let pts = Array.isArray(polyline) ? polyline : polyline.points;
|
|
3142
|
+
if (pts.length < 2) {
|
|
3143
|
+
throw new Error(`${context} needs at least 2 polyline points`);
|
|
3144
|
+
}
|
|
3145
|
+
const isClosed = closed ?? (!Array.isArray(polyline) && polyline.isClosed());
|
|
3146
|
+
if (isClosed && pts.length > 2) {
|
|
3147
|
+
// Scale-relative epsilon for the duplicate-seam test.
|
|
3148
|
+
let maxAbs = 0;
|
|
3149
|
+
for (const p of pts) {
|
|
3150
|
+
maxAbs = Math.max(maxAbs, Math.abs(p.x), Math.abs(p.y), Math.abs(p.z));
|
|
3151
|
+
}
|
|
3152
|
+
const eps = (1 + maxAbs) * 1e-12;
|
|
3153
|
+
const first = pts[0];
|
|
3154
|
+
const last = pts[pts.length - 1];
|
|
3155
|
+
const dx = first.x - last.x;
|
|
3156
|
+
const dy = first.y - last.y;
|
|
3157
|
+
const dz = first.z - last.z;
|
|
3158
|
+
if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= eps) {
|
|
3159
|
+
pts = pts.slice(0, -1);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const flat = new Float64Array(pts.length * 3);
|
|
3163
|
+
for (let i = 0; i < pts.length; i++) {
|
|
3164
|
+
flat[i * 3] = pts[i].x;
|
|
3165
|
+
flat[i * 3 + 1] = pts[i].y;
|
|
3166
|
+
flat[i * 3 + 2] = pts[i].z;
|
|
3167
|
+
}
|
|
3168
|
+
return { flat, isClosed };
|
|
3169
|
+
}
|
|
3170
|
+
|
|
2904
3171
|
/**
|
|
2905
3172
|
* Apply a 4x4 transformation matrix.
|
|
2906
3173
|
* @param matrix - Row-major flat array of 16 numbers
|
|
@@ -3116,6 +3383,37 @@ export class Mesh {
|
|
|
3116
3383
|
return new Point(r[0], r[1], r[2]);
|
|
3117
3384
|
}
|
|
3118
3385
|
|
|
3386
|
+
/**
|
|
3387
|
+
* Boundary loops of the coplanar edge-connected face group containing
|
|
3388
|
+
* faceIndex. Each loop is a closed polygon (winding preserved, collinear
|
|
3389
|
+
* subdivision points collapsed); the closing edge back to the first point
|
|
3390
|
+
* is implicit.
|
|
3391
|
+
*/
|
|
3392
|
+
getFaceBoundaryLoops(faceIndex: number): Point[][] {
|
|
3393
|
+
ensureInit();
|
|
3394
|
+
if (!Number.isFinite(faceIndex) || faceIndex < 0) return [];
|
|
3395
|
+
const r = wasm.mesh_get_face_boundary_loops(
|
|
3396
|
+
this._vertexCount,
|
|
3397
|
+
this._buffer,
|
|
3398
|
+
Math.floor(faceIndex),
|
|
3399
|
+
);
|
|
3400
|
+
if (!r || r.length < 1) return [];
|
|
3401
|
+
const loopCount = Math.floor(r[0]);
|
|
3402
|
+
const loops: Point[][] = [];
|
|
3403
|
+
let offset = 1;
|
|
3404
|
+
for (let l = 0; l < loopCount && offset < r.length; l++) {
|
|
3405
|
+
const pointCount = Math.floor(r[offset]);
|
|
3406
|
+
offset += 1;
|
|
3407
|
+
const loop: Point[] = [];
|
|
3408
|
+
for (let i = 0; i < pointCount && offset + 2 < r.length; i++) {
|
|
3409
|
+
loop.push(new Point(r[offset], r[offset + 1], r[offset + 2]));
|
|
3410
|
+
offset += 3;
|
|
3411
|
+
}
|
|
3412
|
+
if (loop.length >= 3) loops.push(loop);
|
|
3413
|
+
}
|
|
3414
|
+
return loops;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3119
3417
|
/**
|
|
3120
3418
|
* Unique edge count for this triangulated mesh.
|
|
3121
3419
|
*/
|
package/src/NurbsCurve.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { Polyline } from "./Polyline.js";
|
|
|
7
7
|
import { PolyCurve } from "./PolyCurve.js";
|
|
8
8
|
import type { Line } from "./Line.js";
|
|
9
9
|
import type { Arc } from "./Arc.js";
|
|
10
|
+
import type { NurbsSurface } from "./NurbsSurface.js";
|
|
11
|
+
import type { Brep } from "./Brep.js";
|
|
10
12
|
import type { CurveOffsetOptions, RotationAxis } from "./types.js";
|
|
11
13
|
import * as wasm from "./wasm-bindings.js";
|
|
12
14
|
|
|
@@ -110,6 +112,133 @@ export class NurbsCurve {
|
|
|
110
112
|
return pts;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Find all intersections with another NURBS curve, with exact parameters.
|
|
117
|
+
*
|
|
118
|
+
* Parameters are normalized to [0, 1] (the same parameterization as
|
|
119
|
+
* `pointAt`), so `this.pointAt(paramA)` and `other.pointAt(paramB)` both
|
|
120
|
+
* reproduce `point` up to the convergence tolerance. Results are sorted by
|
|
121
|
+
* `paramA`. Coincident (overlapping) curve runs yield NO points — identical
|
|
122
|
+
* curves return an empty array.
|
|
123
|
+
*
|
|
124
|
+
* @param other - Other curve to intersect with
|
|
125
|
+
* @returns Intersections with the 3D point and the parameter on each curve
|
|
126
|
+
*/
|
|
127
|
+
intersectCurveParams(other: NurbsCurve): { point: Point; paramA: number; paramB: number }[] {
|
|
128
|
+
ensureInit();
|
|
129
|
+
const buf = wasm.nurbs_curve_curve_intersect_params(this._data, other._data);
|
|
130
|
+
if (buf.length === 0) return [];
|
|
131
|
+
const count = buf[0];
|
|
132
|
+
const hits: { point: Point; paramA: number; paramB: number }[] = [];
|
|
133
|
+
for (let i = 0; i < count; i++) {
|
|
134
|
+
const off = 1 + i * 5;
|
|
135
|
+
hits.push({
|
|
136
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
137
|
+
paramA: buf[off + 3],
|
|
138
|
+
paramB: buf[off + 4],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return hits;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find all isolated intersections with a NURBS surface, with exact
|
|
146
|
+
* parameters.
|
|
147
|
+
*
|
|
148
|
+
* All parameters are normalized to [0, 1]: `this.pointAt(curveParam)` and
|
|
149
|
+
* `surface.evaluate(u, v)` both reproduce `point` up to the convergence
|
|
150
|
+
* tolerance. Results are sorted by `curveParam`. Curve segments lying ON
|
|
151
|
+
* the surface (coincident runs) yield no points — a line inside a planar
|
|
152
|
+
* surface returns an empty array.
|
|
153
|
+
*
|
|
154
|
+
* @param surface - Surface to intersect with
|
|
155
|
+
* @returns Intersections with the 3D point, curve parameter, and surface (u, v)
|
|
156
|
+
*/
|
|
157
|
+
intersectSurfaceExact(
|
|
158
|
+
surface: NurbsSurface,
|
|
159
|
+
): { point: Point; curveParam: number; u: number; v: number }[] {
|
|
160
|
+
ensureInit();
|
|
161
|
+
const buf = wasm.nurbs_curve_surface_intersect_exact(this._data, surface.data);
|
|
162
|
+
if (buf.length === 0) return [];
|
|
163
|
+
const count = buf[0];
|
|
164
|
+
const hits: { point: Point; curveParam: number; u: number; v: number }[] = [];
|
|
165
|
+
for (let i = 0; i < count; i++) {
|
|
166
|
+
const off = 1 + i * 6;
|
|
167
|
+
hits.push({
|
|
168
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
169
|
+
curveParam: buf[off + 3],
|
|
170
|
+
u: buf[off + 4],
|
|
171
|
+
v: buf[off + 5],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return hits;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find intersection points with a NURBS surface.
|
|
179
|
+
* Point-only convenience wrapper around `intersectSurfaceExact`.
|
|
180
|
+
* @param surface - Surface to intersect with
|
|
181
|
+
* @returns Array of intersection points, sorted along the curve
|
|
182
|
+
*/
|
|
183
|
+
intersectSurface(surface: NurbsSurface): Point[] {
|
|
184
|
+
return this.intersectSurfaceExact(surface).map(hit => hit.point);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Boolean-intersection clip of this curve against a CLOSED solid Brep —
|
|
189
|
+
* delegate for {@link Brep.clipCurve}: the curve is split EXACTLY (knot
|
|
190
|
+
* insertion) at every boundary crossing and each piece is classified by
|
|
191
|
+
* containment, so every returned piece lies exactly on this curve. Throws
|
|
192
|
+
* when the Brep is not a closed solid.
|
|
193
|
+
*
|
|
194
|
+
* @param brep - Closed solid BREP to clip against
|
|
195
|
+
* @returns Exact sub-curves inside and outside the solid
|
|
196
|
+
*/
|
|
197
|
+
clipByBrep(brep: Brep): { inside: NurbsCurve[]; outside: NurbsCurve[] } {
|
|
198
|
+
return brep.clipCurve(this);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Split this curve at multiple normalized parameters t in [0, 1].
|
|
203
|
+
*
|
|
204
|
+
* Splitting is exact (knot insertion), so every returned piece lies exactly
|
|
205
|
+
* on this curve. Parameters are sorted, deduplicated, and clamped
|
|
206
|
+
* internally. Closed curves are opened at the FIRST parameter (N interior
|
|
207
|
+
* parameters yield N pieces, the seam-spanning piece rejoined); open curves
|
|
208
|
+
* yield N + 1 pieces. With no valid parameters the (clamped) curve comes
|
|
209
|
+
* back as the single piece.
|
|
210
|
+
*
|
|
211
|
+
* @param params - Normalized split parameters in [0, 1]
|
|
212
|
+
* @param closed - Explicit topology override. `true` forces closed
|
|
213
|
+
* (seam-rejoin) semantics; `false` forces open semantics — an open curve
|
|
214
|
+
* whose endpoints merely coincide (a C-shape drawn back onto its start)
|
|
215
|
+
* keeps its N + 1 pieces. Omit to infer closedness geometrically from the
|
|
216
|
+
* endpoint distance (the historical behavior).
|
|
217
|
+
* @returns Curve pieces in parameter order
|
|
218
|
+
*/
|
|
219
|
+
splitAtParams(params: number[], closed?: boolean): NurbsCurve[] {
|
|
220
|
+
ensureInit();
|
|
221
|
+
const closedHint = closed === undefined ? -1 : closed ? 1 : 0;
|
|
222
|
+
const buf = wasm.nurbs_curve_split_at_params(
|
|
223
|
+
this._data,
|
|
224
|
+
new Float64Array(params),
|
|
225
|
+
closedHint
|
|
226
|
+
);
|
|
227
|
+
if (buf.length === 0) {
|
|
228
|
+
throw new Error("NurbsCurve.splitAtParams failed — malformed curve data");
|
|
229
|
+
}
|
|
230
|
+
// Packing: [curveCount, (bufLen, curveData…)·curveCount]
|
|
231
|
+
const count = buf[0];
|
|
232
|
+
const pieces: NurbsCurve[] = [];
|
|
233
|
+
let idx = 1;
|
|
234
|
+
for (let i = 0; i < count; i++) {
|
|
235
|
+
const len = buf[idx++];
|
|
236
|
+
pieces.push(NurbsCurve.fromData(buf.subarray(idx, idx + len)));
|
|
237
|
+
idx += len;
|
|
238
|
+
}
|
|
239
|
+
return pieces;
|
|
240
|
+
}
|
|
241
|
+
|
|
113
242
|
/**
|
|
114
243
|
* Translate this curve by an offset vector.
|
|
115
244
|
* @param offset - Translation vector
|
package/src/NurbsSurface.ts
CHANGED
|
@@ -147,6 +147,47 @@ export class NurbsSurface implements Surface {
|
|
|
147
147
|
return parsePolylineBuf(buf).map(pts => new Polyline(pts));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* EXACT intersection curves with another NURBS surface.
|
|
152
|
+
*
|
|
153
|
+
* Unlike {@link intersectSurface} (tessellation-based, returns polylines at
|
|
154
|
+
* a fixed sampling resolution), this runs the parametric boolean's
|
|
155
|
+
* surface–surface intersection pipeline — seeded tri-tri detection,
|
|
156
|
+
* chaining, and Newton/tangent-plane relaxation of every point onto the
|
|
157
|
+
* true intersection — and fits each chain to an interpolating NURBS curve,
|
|
158
|
+
* densified until the fitted curve stays within `tolerance` of the exact
|
|
159
|
+
* intersection.
|
|
160
|
+
*
|
|
161
|
+
* Closed intersection loops (e.g. cylinder × plane) come back as ONE closed
|
|
162
|
+
* curve. Coincident (overlapping) surfaces return an empty array — their
|
|
163
|
+
* overlap is a 2D region, not a curve set.
|
|
164
|
+
*
|
|
165
|
+
* @param other - Other surface to intersect with
|
|
166
|
+
* @param tolerance - Absolute geometric tolerance of the fitted curves
|
|
167
|
+
* (model units; floored relative to the combined model scale)
|
|
168
|
+
* @returns Exact fitted NURBS intersection curves
|
|
169
|
+
*/
|
|
170
|
+
intersectSurfaceCurves(other: NurbsSurface, tolerance = 1e-6): NurbsCurve[] {
|
|
171
|
+
ensureInit();
|
|
172
|
+
const buf = wasm.nurbs_surface_surface_intersect_curves(this._data, other._data, tolerance);
|
|
173
|
+
return NurbsSurface.parseCurveList(buf);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
|
|
177
|
+
private static parseCurveList(buf: Float64Array): NurbsCurve[] {
|
|
178
|
+
if (buf.length < 1) return [];
|
|
179
|
+
const out: NurbsCurve[] = [];
|
|
180
|
+
const count = Number(buf[0]);
|
|
181
|
+
let offset = 1;
|
|
182
|
+
for (let k = 0; k < count && offset < buf.length; k++) {
|
|
183
|
+
const len = Number(buf[offset]);
|
|
184
|
+
offset += 1;
|
|
185
|
+
out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
|
|
186
|
+
offset += len;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
/**
|
|
151
192
|
* Convert any supported curve type to a NurbsCurve.
|
|
152
193
|
* @param c - Curve to convert
|
|
@@ -176,11 +217,19 @@ export class NurbsSurface implements Surface {
|
|
|
176
217
|
// Step 0: Convert all curves to NurbsCurve
|
|
177
218
|
const nurbsCurves = curves.map(c => NurbsSurface.toNurbs(c));
|
|
178
219
|
|
|
179
|
-
// Step 1: Make curves compatible (unified knot vectors,
|
|
220
|
+
// Step 1: Make curves compatible (same degree, unified knot vectors,
|
|
221
|
+
// same CP count). Mixed degrees are fine — the kernel degree-elevates
|
|
222
|
+
// lower-degree curves to the max degree exactly (a 2-point line lofts
|
|
223
|
+
// with a cubic).
|
|
180
224
|
let loftCurves = nurbsCurves;
|
|
181
225
|
if (nurbsCurves.length >= 2) {
|
|
182
|
-
const
|
|
183
|
-
const
|
|
226
|
+
const ref = nurbsCurves[0];
|
|
227
|
+
const matchesRef = (c: NurbsCurve) =>
|
|
228
|
+
c.degree === ref.degree &&
|
|
229
|
+
c.controlPoints.length === ref.controlPoints.length &&
|
|
230
|
+
c.knots.length === ref.knots.length &&
|
|
231
|
+
c.knots.every((k, i) => Math.abs(k - (ref.knots[i] as number)) < 1e-12);
|
|
232
|
+
const allSame = nurbsCurves.every(matchesRef);
|
|
184
233
|
if (!allSame) {
|
|
185
234
|
const compatParts: number[] = [nurbsCurves.length];
|
|
186
235
|
for (const c of nurbsCurves) {
|
|
@@ -190,19 +239,20 @@ export class NurbsSurface implements Surface {
|
|
|
190
239
|
for (const k of c.knots) { compatParts.push(k); }
|
|
191
240
|
}
|
|
192
241
|
const compatBuf = wasm.make_nurbs_curves_compatible(new Float64Array(compatParts));
|
|
193
|
-
if (compatBuf.length
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
242
|
+
if (compatBuf.length === 0) {
|
|
243
|
+
throw new Error("NurbsSurface loft: profile curves could not be made compatible");
|
|
244
|
+
}
|
|
245
|
+
const count = Number(compatBuf[0] ?? 0);
|
|
246
|
+
const decoded: NurbsCurve[] = [];
|
|
247
|
+
let off = 1;
|
|
248
|
+
for (let i = 0; i < count; i++) {
|
|
249
|
+
const deg = Number(compatBuf[off] ?? 0);
|
|
250
|
+
const n = Number(compatBuf[off + 1] ?? 0);
|
|
251
|
+
const curveLen = 2 + n * 3 + n + (n + deg + 1);
|
|
252
|
+
decoded.push(NurbsCurve.fromData(compatBuf.slice(off, off + curveLen)));
|
|
253
|
+
off += curveLen;
|
|
205
254
|
}
|
|
255
|
+
loftCurves = decoded;
|
|
206
256
|
}
|
|
207
257
|
}
|
|
208
258
|
|