okgeometry-api 1.16.0 → 1.17.1

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/src/Mesh.ts CHANGED
@@ -1268,9 +1268,15 @@ export class Mesh {
1268
1268
  return { points: [], radius: 0 };
1269
1269
  }
1270
1270
 
1271
+ // Circle.sample(k) spans the full closed parameter range inclusively, so
1272
+ // its last point repeats the first (the seam). A planar circle POLYGON
1273
+ // must be `segments` unique vertices with an implicit closing edge —
1274
+ // sample one extra point and drop the duplicated seam.
1271
1275
  const circle = new Circle(center, radius, n);
1276
+ const samples = circle.sample(Math.floor(segments) + 1);
1277
+ samples.pop();
1272
1278
  return {
1273
- points: circle.sample(Math.floor(segments)),
1279
+ points: samples,
1274
1280
  radius,
1275
1281
  };
1276
1282
  }
@@ -2203,8 +2209,9 @@ export class Mesh {
2203
2209
  const topology = `A=${aClosed ? "closed" : "open"}, B=${bClosed ? "closed" : "open"}`;
2204
2210
  if (operation === "union") {
2205
2211
  return new Error(
2206
- `Boolean union requires both inputs to be closed volumes (${topology}). `
2207
- + "Close or cap the open mesh before calling union().",
2212
+ `Boolean union of a closed solid and an open sheet is not defined (${topology}). `
2213
+ + "Trim the sheet with subtract/intersect, or close it into a solid first. "
2214
+ + "Union supports closed x closed (volumetric) and open x open (sheet trim + join).",
2208
2215
  );
2209
2216
  }
2210
2217
 
@@ -2229,9 +2236,75 @@ export class Mesh {
2229
2236
  return Mesh.mergeMeshComponents(operation === "subtraction" ? split.outside : split.inside);
2230
2237
  }
2231
2238
 
2239
+ if (!aClosed && !bClosed && operation === "union") {
2240
+ return this.sheetUnion(other, options);
2241
+ }
2242
+
2232
2243
  throw Mesh.createUnsupportedOpenMeshBooleanError(operation, aClosed, bClosed);
2233
2244
  }
2234
2245
 
2246
+ /**
2247
+ * Union of two OPEN oriented sheets: mutual trim + join, mirroring the
2248
+ * parametric sheet x sheet union (`Brep.unionBrep`) exactly. A sheet's
2249
+ * material is its FRONT half-space, so the union boundary keeps this
2250
+ * sheet's pieces BEHIND the other (pieces in front of the other sheet are
2251
+ * interior to the union material and are dropped) and the other's pieces
2252
+ * BEHIND this one, welded into one crack-free mesh at the shared
2253
+ * intersection seam. Regions the other operand never cuts are kept
2254
+ * wholesale (union never loses non-conflicting material);
2255
+ * coincident-overlap regions keep THIS sheet's copy only.
2256
+ *
2257
+ * Called by `union()` when both operands are open.
2258
+ */
2259
+ private sheetUnion(other: Mesh, options?: MeshBooleanOptions): Mesh {
2260
+ if (this.faceCount === 0) return Mesh.cloneMesh(other);
2261
+ if (other.faceCount === 0) return Mesh.cloneMesh(this);
2262
+
2263
+ const boundsA = Mesh.computeRawBounds(this);
2264
+ const boundsB = Mesh.computeRawBounds(other);
2265
+ const contactTol = Mesh.booleanContactTolerance(boundsA, boundsB);
2266
+ if (!Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
2267
+ return Mesh.mergeMeshes([this, other]);
2268
+ }
2269
+
2270
+ if (!options?.allowUnsafe) {
2271
+ const limits = Mesh.resolveBooleanLimits(options?.limits);
2272
+ const faceCountA = this.faceCount;
2273
+ const faceCountB = other.faceCount;
2274
+ const maxInputFaces = Math.max(faceCountA, faceCountB);
2275
+ const combinedInputFaces = faceCountA + faceCountB;
2276
+ const faceProduct = faceCountA * faceCountB;
2277
+ if (
2278
+ maxInputFaces > limits.maxInputFacesPerMesh
2279
+ || combinedInputFaces > limits.maxCombinedInputFaces
2280
+ || faceProduct > limits.maxFaceProduct
2281
+ ) {
2282
+ throw new Error(
2283
+ `Boolean union blocked by safety limits `
2284
+ + `(A faces=${faceCountA}, B faces=${faceCountB}, faceProduct=${faceProduct}). `
2285
+ + "Simplify inputs, run in a Worker, or pass allowUnsafe: true to force execution.",
2286
+ );
2287
+ }
2288
+ }
2289
+
2290
+ const token = Mesh.encodeBooleanSplitToken(this, other, options);
2291
+ const result = wasm.mesh_sheet_union(
2292
+ this._vertexCount,
2293
+ this._buffer,
2294
+ other._vertexCount,
2295
+ other._buffer,
2296
+ token,
2297
+ );
2298
+ if (result.length === 0) {
2299
+ throw new Error("Boolean union of open sheets failed and returned an invalid mesh buffer.");
2300
+ }
2301
+ const vertexCount = result[0];
2302
+ if (!Number.isFinite(vertexCount) || vertexCount < 0) {
2303
+ throw new Error("Boolean union of open sheets failed and returned a corrupt mesh buffer.");
2304
+ }
2305
+ return Mesh.fromTrustedBuffer(result);
2306
+ }
2307
+
2235
2308
  static encodeBooleanOperationToken(
2236
2309
  operation: MeshBooleanOperation,
2237
2310
  a: Mesh,
@@ -2267,7 +2340,13 @@ export class Mesh {
2267
2340
 
2268
2341
  /**
2269
2342
  * Compute boolean union with another mesh.
2270
- * Requires both inputs to be closed volumes.
2343
+ *
2344
+ * Closed x closed inputs run the volumetric CSG union. Open x open inputs
2345
+ * run the SHEET union (mutual trim + join): a sheet's material is its
2346
+ * FRONT half-space, so the union keeps this sheet's pieces BEHIND the
2347
+ * other plus the other's pieces BEHIND this one, welded at the shared
2348
+ * seam, with uncut regions kept wholesale. A closed solid combined
2349
+ * with an open sheet has no defined union and throws.
2271
2350
  * @param other - Mesh to union with
2272
2351
  * @param options - Optional safety overrides
2273
2352
  * @returns New mesh containing volume of both inputs
@@ -2276,7 +2355,8 @@ export class Mesh {
2276
2355
  ensureInit();
2277
2356
  const lhs = Mesh.normalizeClosedVolumeOrientation(this);
2278
2357
  const rhs = Mesh.normalizeClosedVolumeOrientation(other);
2279
- lhs.resolveOpenMeshBooleanFallback(rhs, "union", options);
2358
+ const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "union", options);
2359
+ if (fallback) return fallback;
2280
2360
  const operationToken = Mesh.encodeBooleanOperationToken("union", lhs, rhs, options);
2281
2361
  return lhs.runBoolean(
2282
2362
  rhs,
package/src/NurbsCurve.ts CHANGED
@@ -239,6 +239,116 @@ export class NurbsCurve {
239
239
  return pieces;
240
240
  }
241
241
 
242
+ /**
243
+ * 2D REGION boolean between this curve and another, both CLOSED and
244
+ * COPLANAR (Rhino CurveBoolean). Closed planar curves bound regions, so
245
+ * union / subtract / intersect are well defined: the curves are projected
246
+ * onto a common plane (fitted from this curve, coplanarity verified),
247
+ * tessellated to polygons, arranged in 2D, classified by nonzero winding,
248
+ * and the result loops emitted.
249
+ *
250
+ * The result is a list of closed POLYLINE (degree-1 NURBS) curves —
251
+ * Rhino-parity fidelity, so curved input boundaries are approximated to the
252
+ * tessellation tolerance. Tessellation samples include every distinct
253
+ * interior knot value, so degree-1 polygon corners and C0 kinks are
254
+ * preserved EXACTLY, and consecutive collinear points are collapsed so
255
+ * straight runs come back as single segments. OUTER boundaries are
256
+ * counter-clockwise (positive signed area in the fitted plane); HOLE
257
+ * boundaries are clockwise. CCW is measured about a DETERMINISTIC plane
258
+ * normal: the normal fitted from THIS curve, flipped so its dominant
259
+ * world-axis component is positive (for curves in the world XY plane the
260
+ * reference normal is +Z, so outer loops are CCW viewed from +Z). The
261
+ * emitted orientation is therefore independent of either curve's drawing
262
+ * direction. Callers reconstruct the region topology by nesting (a CW loop
263
+ * inside a CCW loop is that region's hole).
264
+ *
265
+ * A valid but empty result (e.g. a disjoint intersect) returns `[]`. Throws
266
+ * on non-coplanar or OPEN input (closure is verified kernel-side: the
267
+ * curves' endpoints must coincide within a scale-relative tolerance).
268
+ *
269
+ * @param other - The other closed coplanar curve
270
+ * @param op - 0 = union, 1 = subtract (this − other), 2 = intersect
271
+ * @returns Result region boundary loops (outer CCW, holes CW)
272
+ */
273
+ private regionBoolean(other: NurbsCurve, op: number): NurbsCurve[] {
274
+ ensureInit();
275
+ const buf = wasm.curve_region_boolean(this._data, other._data, op);
276
+ // An EMPTY buffer signals an error (non-coplanar / malformed); a valid
277
+ // empty result is the non-empty [0] packing.
278
+ if (buf.length === 0) {
279
+ throw new Error(
280
+ "NurbsCurve region boolean failed — curves must be closed and coplanar",
281
+ );
282
+ }
283
+ return NurbsCurve.decodeCurveList(buf);
284
+ }
285
+
286
+ /**
287
+ * Region UNION with another closed coplanar curve. See {@link regionBoolean}.
288
+ * @param other - The other closed coplanar curve
289
+ * @returns Boundary loops of (inside-this OR inside-other), outer CCW / holes CW
290
+ */
291
+ regionUnion(other: NurbsCurve): NurbsCurve[] {
292
+ return this.regionBoolean(other, 0);
293
+ }
294
+
295
+ /**
296
+ * Region SUBTRACT (this − other) of a closed coplanar curve. Order matters.
297
+ * See {@link regionBoolean}.
298
+ * @param other - The curve subtracted from this
299
+ * @returns Boundary loops of (inside-this AND NOT inside-other)
300
+ */
301
+ regionSubtract(other: NurbsCurve): NurbsCurve[] {
302
+ return this.regionBoolean(other, 1);
303
+ }
304
+
305
+ /**
306
+ * Region INTERSECT with another closed coplanar curve. See
307
+ * {@link regionBoolean}.
308
+ * @param other - The other closed coplanar curve
309
+ * @returns Boundary loops of (inside-this AND inside-other)
310
+ */
311
+ regionIntersect(other: NurbsCurve): NurbsCurve[] {
312
+ return this.regionBoolean(other, 2);
313
+ }
314
+
315
+ /**
316
+ * Region SPLIT: partition this curve and another closed coplanar curve into
317
+ * ALL region boundaries (Rhino "region split"). The arrangement partitions
318
+ * the plane into the pieces inside-this-only, inside-other-only and
319
+ * inside-both; every bounded region's outer and hole loops come back as
320
+ * their own closed curves (outer CCW, holes CW). See {@link regionBoolean}
321
+ * for the fidelity and orientation contract.
322
+ *
323
+ * @param other - The other closed coplanar curve
324
+ * @returns All region boundary loops
325
+ */
326
+ regionSplit(other: NurbsCurve): NurbsCurve[] {
327
+ ensureInit();
328
+ const buf = wasm.curve_region_split(this._data, other._data);
329
+ if (buf.length === 0) {
330
+ throw new Error(
331
+ "NurbsCurve region split failed — curves must be closed and coplanar",
332
+ );
333
+ }
334
+ return NurbsCurve.decodeCurveList(buf);
335
+ }
336
+
337
+ /** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
338
+ private static decodeCurveList(buf: Float64Array): NurbsCurve[] {
339
+ if (buf.length < 1) return [];
340
+ const out: NurbsCurve[] = [];
341
+ let offset = 1;
342
+ const count = Number(buf[0]);
343
+ for (let k = 0; k < count && offset < buf.length; k++) {
344
+ const len = Number(buf[offset]);
345
+ offset += 1;
346
+ out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
347
+ offset += len;
348
+ }
349
+ return out;
350
+ }
351
+
242
352
  /**
243
353
  * Translate this curve by an offset vector.
244
354
  * @param offset - Translation vector
package/src/index.ts CHANGED
@@ -1,79 +1,88 @@
1
- // Core initialization
2
- export { init, isInitialized } from "./engine.js";
3
-
4
- // Shared types and utilities
5
- export type {
6
- SweepableCurve,
7
- LoftableCurve,
8
- RotationAxis,
9
- CurveSegment,
10
- CurveOffsetJoinStyle,
11
- CurveOffsetOptions,
12
- } from "./types.js";
13
- export { CurveTypeCode, SegmentTypeCode } from "./types.js";
14
- export {
15
- pointsToCoords,
16
- coordsToPoints,
17
- parsePolylineBuffer,
18
- parseIntersectionPoints,
19
- } from "./BufferCodec.js";
20
-
21
- // Primitive types
22
- export { Vec3 } from "./Vec3.js";
23
- export { Point } from "./Point.js";
24
- export { Plane } from "./Plane.js";
25
- export { Ray } from "./Ray.js";
26
-
27
- // Curve types
28
- export { Line } from "./Line.js";
29
- export { Circle } from "./Circle.js";
30
- export { Arc } from "./Arc.js";
31
- export { Polyline } from "./Polyline.js";
32
- export { Polygon } from "./Polygon.js";
33
- export { PolyCurve } from "./PolyCurve.js";
34
- export { NurbsCurve } from "./NurbsCurve.js";
35
- export { NurbsSurface } from "./NurbsSurface.js";
36
- export { MeshSurface } from "./MeshSurface.js";
37
- export type { Surface } from "./Surface.js";
38
-
39
- // Boundary representation
40
- export { Brep } from "./Brep.js";
41
- export type {
42
- BrepInfo,
43
- BrepValidationReport,
44
- BrepBooleanOptions,
45
- BrepParametricBooleanOptions,
46
- MeshToBrepConversionOptions,
47
- BrepSplitSolidResult,
48
- } from "./Brep.js";
49
-
50
- // Mesh and operations
51
- export { Mesh, MeshBooleanExecutionError } from "./Mesh.js";
52
- export type {
53
- MeshBooleanOperation,
54
- MeshBooleanDebugProbeId,
55
- MeshBooleanDebugOptions,
56
- MeshBooleanDebugProbe,
57
- MeshBooleanDebugProbeSummary,
58
- MeshBooleanDebugReport,
59
- MeshBooleanReproOperand,
60
- MeshBooleanReproResult,
61
- MeshBooleanReproError,
62
- MeshBooleanReproPayload,
63
- MeshBooleanReproOptions,
64
- MeshSplitResult,
65
- MeshPlaneSplitResult,
66
- MeshPlaneSplitOptions,
67
- MeshCurveSplitOptions,
68
- MeshPlanarFace,
69
- MeshDebugBounds,
70
- MeshDebugRayHit,
71
- MeshDebugSummary,
72
- MeshBooleanLimits,
73
- MeshBooleanOptions,
74
- MeshBooleanAsyncOptions,
75
- MeshBooleanErrorCode,
76
- MeshBooleanErrorPayload,
77
- MeshBooleanProgressEvent,
78
- } from "./Mesh.js";
79
- export { intersect } from "./Geometry.js";
1
+ // Core initialization
2
+ export { init, isInitialized } from "./engine.js";
3
+
4
+ // Shared types and utilities
5
+ export type {
6
+ SweepableCurve,
7
+ LoftableCurve,
8
+ RotationAxis,
9
+ CurveSegment,
10
+ CurveOffsetJoinStyle,
11
+ CurveOffsetOptions,
12
+ } from "./types.js";
13
+ export { CurveTypeCode, SegmentTypeCode } from "./types.js";
14
+ export {
15
+ pointsToCoords,
16
+ coordsToPoints,
17
+ parsePolylineBuffer,
18
+ parseIntersectionPoints,
19
+ } from "./BufferCodec.js";
20
+
21
+ // Primitive types
22
+ export { Vec3 } from "./Vec3.js";
23
+ export { Point } from "./Point.js";
24
+ export { Plane } from "./Plane.js";
25
+ export { Ray } from "./Ray.js";
26
+
27
+ // Curve types
28
+ export { Line } from "./Line.js";
29
+ export { Circle } from "./Circle.js";
30
+ export { Arc } from "./Arc.js";
31
+ export { Polyline } from "./Polyline.js";
32
+ export { Polygon } from "./Polygon.js";
33
+ export { PolyCurve } from "./PolyCurve.js";
34
+ export { NurbsCurve } from "./NurbsCurve.js";
35
+ export { NurbsSurface } from "./NurbsSurface.js";
36
+ export { MeshSurface } from "./MeshSurface.js";
37
+ export type { Surface } from "./Surface.js";
38
+
39
+ // Boundary representation
40
+ export { Brep } from "./Brep.js";
41
+ export type {
42
+ BrepInfo,
43
+ BrepValidationReport,
44
+ BrepBooleanOptions,
45
+ BrepParametricBooleanOptions,
46
+ MeshToBrepConversionOptions,
47
+ BrepSplitSolidResult,
48
+ } from "./Brep.js";
49
+
50
+ // Mesh and operations
51
+ export { Mesh, MeshBooleanExecutionError } from "./Mesh.js";
52
+ export type {
53
+ MeshBooleanOperation,
54
+ MeshBooleanDebugProbeId,
55
+ MeshBooleanDebugOptions,
56
+ MeshBooleanDebugProbe,
57
+ MeshBooleanDebugProbeSummary,
58
+ MeshBooleanDebugReport,
59
+ MeshBooleanReproOperand,
60
+ MeshBooleanReproResult,
61
+ MeshBooleanReproError,
62
+ MeshBooleanReproPayload,
63
+ MeshBooleanReproOptions,
64
+ MeshSplitResult,
65
+ MeshPlaneSplitResult,
66
+ MeshPlaneSplitOptions,
67
+ MeshCurveSplitOptions,
68
+ MeshPlanarFace,
69
+ MeshDebugBounds,
70
+ MeshDebugRayHit,
71
+ MeshDebugSummary,
72
+ MeshBooleanLimits,
73
+ MeshBooleanOptions,
74
+ MeshBooleanAsyncOptions,
75
+ MeshBooleanErrorCode,
76
+ MeshBooleanErrorPayload,
77
+ MeshBooleanProgressEvent,
78
+ } from "./Mesh.js";
79
+ export { intersect } from "./Geometry.js";
80
+
81
+ // Universal geometry boolean dispatcher
82
+ export { geometryBoolean, geometrySplit } from "./GeometryBoolean.js";
83
+ export type {
84
+ BooleanOpKind,
85
+ BooleanOperand,
86
+ GeometryBooleanOptions,
87
+ GeometryBooleanResult,
88
+ } from "./GeometryBoolean.js";