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/Brep.ts CHANGED
@@ -50,6 +50,21 @@ export interface BrepSplitSolidResult {
50
50
  positive: Brep | null;
51
51
  }
52
52
 
53
+ /**
54
+ * Both sides of a sheet split ({@link Brep.splitSheetBySolid} /
55
+ * {@link Brep.splitSheetBySheet}), each an OPEN Brep preserving the sheet's
56
+ * exact trimmed faces, pcurves and edges.
57
+ *
58
+ * `inside` follows the oriented-sheet convention ("inside" = FRONT, the side
59
+ * a sheet's normal points toward): pieces CONTAINED in the solid cutter, or
60
+ * pieces in FRONT of the sheet cutter's normal. A side is null when no
61
+ * material lands on it.
62
+ */
63
+ export interface BrepSheetSplitResult {
64
+ inside: Brep | null;
65
+ outside: Brep | null;
66
+ }
67
+
53
68
  /** Validation report for a BREP body. */
54
69
  export interface BrepValidationReport {
55
70
  isClosed: boolean;
@@ -770,14 +785,34 @@ export class Brep {
770
785
 
771
786
  // ── Parametric booleans (BREP → BREP) ──
772
787
 
788
+ /**
789
+ * Resolve a parametric-boolean operand: Breps pass through; Meshes are
790
+ * converted via {@link Brep.fromMesh} (planar-faceted meshes only) so the
791
+ * EXACT pipeline can run. Curved/dense meshes fail conversion with
792
+ * guidance to use the tessellated union()/subtract()/intersect() instead.
793
+ */
794
+ private static resolveParametricOperand(other: Brep | Mesh): Brep {
795
+ if (other instanceof Brep) return other;
796
+ try {
797
+ return Brep.fromMesh(other);
798
+ } catch (e) {
799
+ const reason = e instanceof Error ? e.message : String(e);
800
+ throw new Error(
801
+ `Parametric boolean operand mesh could not be converted to a BREP (${reason}); ` +
802
+ "use the tessellated union()/subtract()/intersect() for mesh operands instead",
803
+ );
804
+ }
805
+ }
806
+
773
807
  private runParametricBoolean(
774
- other: Brep,
808
+ other: Brep | Mesh,
775
809
  op: "union" | "subtract" | "intersect",
776
810
  options?: BrepParametricBooleanOptions,
777
811
  ): Brep {
778
812
  ensureInit();
813
+ const rhs = Brep.resolveParametricOperand(other);
779
814
  const tolerance = options?.tolerance ?? 1e-6;
780
- const json = wasm.brep_boolean_op(this.json, other.json, op, tolerance);
815
+ const json = wasm.brep_boolean_op(this.json, rhs.json, op, tolerance);
781
816
  if (!json) throw new Error(`Brep.${op}Brep failed`);
782
817
  if (json.startsWith('{"error"')) {
783
818
  const parsed = JSON.parse(json) as { error: string };
@@ -791,7 +826,30 @@ export class Brep {
791
826
  * trimmed-surface output. Result faces keep their parents' exact surfaces
792
827
  * (planes/NURBS, untessellated); intersection edges are Newton-refined SSI
793
828
  * curves stored as tolerant NURBS edges with matched pcurves on both
794
- * adjacent faces. Both operands must be CLOSED solids.
829
+ * adjacent faces.
830
+ *
831
+ * Operands may be CLOSED solids or OPEN sheets — the kernel dispatches on
832
+ * closedness, following the oriented-sheet convention used across the API
833
+ * (a sheet's "inside" is the FRONT of its normal):
834
+ * - closed × closed — full volumetric boolean.
835
+ * - sheet × solid — `subtract` keeps the sheet's pieces OUTSIDE the solid,
836
+ * `intersect` the pieces INSIDE (open trimmed sheets).
837
+ * - solid × sheet — `subtract` keeps the capped half of the solid BEHIND
838
+ * the sheet normal, `intersect` the FRONT half (closed solids).
839
+ * - sheet × sheet — a sheet's material is its FRONT half-space, so
840
+ * `union` = ∂(matA ∪ matB): this sheet's BACK-of-other pieces joined
841
+ * with the other's BACK-of-this pieces into ONE open Brep stitched at
842
+ * the shared exact intersection edges (pieces in FRONT of the other
843
+ * sheet are interior to the union material and are dropped; faces the
844
+ * other operand never cuts are kept whole, coincident overlaps keep
845
+ * this sheet's copy); `subtract` keeps this sheet's BACK pieces;
846
+ * `intersect` its FRONT pieces.
847
+ * - `union` of a solid and a sheet (either order) is NOT defined and
848
+ * throws — trim the sheet with subtract/intersect instead.
849
+ *
850
+ * A Mesh operand is converted via {@link Brep.fromMesh} (planar-faceted
851
+ * meshes only); if conversion fails, use the tessellated
852
+ * union()/subtract()/intersect() instead.
795
853
  *
796
854
  * `options.tolerance` is the absolute geometric tolerance of intersection
797
855
  * edges and coincident-face classification (default 1e-6).
@@ -800,17 +858,27 @@ export class Brep {
800
858
  * cylinders touching along a line/point) is not resolved into pinch
801
859
  * topology; transversal and coincident (coplanar) configurations are exact.
802
860
  */
803
- unionBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
861
+ unionBrep(other: Brep | Mesh, options?: BrepParametricBooleanOptions): Brep {
804
862
  return this.runParametricBoolean(other, "union", options);
805
863
  }
806
864
 
807
- /** PARAMETRIC boolean subtraction (this − other). See {@link unionBrep}. */
808
- subtractBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
865
+ /**
866
+ * PARAMETRIC boolean subtraction (this other). See {@link unionBrep} for
867
+ * the full open/closed dispatch table: sheet − solid trims the sheet to
868
+ * the OUTSIDE of the solid; solid − sheet keeps the capped half BEHIND the
869
+ * sheet normal; sheet − sheet keeps this sheet's BACK pieces.
870
+ */
871
+ subtractBrep(other: Brep | Mesh, options?: BrepParametricBooleanOptions): Brep {
809
872
  return this.runParametricBoolean(other, "subtract", options);
810
873
  }
811
874
 
812
- /** PARAMETRIC boolean intersection. See {@link unionBrep}. */
813
- intersectBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
875
+ /**
876
+ * PARAMETRIC boolean intersection. See {@link unionBrep} for the full
877
+ * open/closed dispatch table: sheet ∩ solid trims the sheet to the INSIDE
878
+ * of the solid; solid ∩ sheet keeps the capped half in FRONT of the sheet
879
+ * normal; sheet ∩ sheet keeps this sheet's FRONT pieces.
880
+ */
881
+ intersectBrep(other: Brep | Mesh, options?: BrepParametricBooleanOptions): Brep {
814
882
  return this.runParametricBoolean(other, "intersect", options);
815
883
  }
816
884
 
@@ -903,6 +971,60 @@ export class Brep {
903
971
  return { negative: side(parsed.negative), positive: side(parsed.positive) };
904
972
  }
905
973
 
974
+ /**
975
+ * PARAMETRIC split of this OPEN sheet by a CLOSED solid: the sheet's faces
976
+ * are imprinted with the exact intersection curves and every face piece is
977
+ * classified by containment in the solid. `inside` = the pieces contained
978
+ * in the solid (ON-boundary pieces count as inside), `outside` = the rest.
979
+ * Both sides are open Breps preserving the sheet's exact trimmed faces,
980
+ * pcurves and edges.
981
+ *
982
+ * The counterpart of {@link splitBySheet} with the roles swapped: use that
983
+ * one to split a closed solid BY a sheet (capped halves); use this one to
984
+ * split a sheet BY a solid (trimmed sheet pieces).
985
+ */
986
+ splitSheetBySolid(
987
+ solid: Brep,
988
+ options?: BrepParametricBooleanOptions,
989
+ ): BrepSheetSplitResult {
990
+ ensureInit();
991
+ const tolerance = options?.tolerance ?? 1e-6;
992
+ const json = wasm.brep_split_sheet_by_solid_op(this.json, solid.json, tolerance);
993
+ return Brep.parseSheetSplitResult(json, "Brep.splitSheetBySolid");
994
+ }
995
+
996
+ /**
997
+ * PARAMETRIC split of this OPEN sheet by another OPEN sheet (a sheet Brep
998
+ * or a NurbsSurface, converted via {@link Brep.fromSurface}): the mutual
999
+ * exact intersection curves are imprinted onto this sheet's faces and each
1000
+ * piece is assigned to the FRONT (`inside`) or BACK (`outside`) of the
1001
+ * other sheet's oriented normal — the same seam-vote classification as
1002
+ * {@link splitBySheet}. Faces the cutter never crosses classify wholesale
1003
+ * by their side of the cutter; coincident-overlap pieces land in `inside`.
1004
+ */
1005
+ splitSheetBySheet(
1006
+ other: Brep | NurbsSurface,
1007
+ options?: BrepParametricBooleanOptions,
1008
+ ): BrepSheetSplitResult {
1009
+ ensureInit();
1010
+ const cutter = other instanceof Brep ? other : Brep.fromSurface(other);
1011
+ const tolerance = options?.tolerance ?? 1e-6;
1012
+ const json = wasm.brep_split_sheet_by_sheet_op(this.json, cutter.json, tolerance);
1013
+ return Brep.parseSheetSplitResult(json, "Brep.splitSheetBySheet");
1014
+ }
1015
+
1016
+ private static parseSheetSplitResult(json: string, label: string): BrepSheetSplitResult {
1017
+ if (!json) throw new Error(`${label} failed`);
1018
+ if (json.startsWith('{"error"')) {
1019
+ const parsed = JSON.parse(json) as { error: string };
1020
+ throw new Error(`${label} failed: ${parsed.error}`);
1021
+ }
1022
+ const parsed = JSON.parse(json) as { inside: unknown; outside: unknown };
1023
+ const side = (v: unknown): Brep | null =>
1024
+ v == null ? null : new Brep(JSON.stringify(v));
1025
+ return { inside: side(parsed.inside), outside: side(parsed.outside) };
1026
+ }
1027
+
906
1028
  /** JSON representation (persistence). */
907
1029
  toJson(): string {
908
1030
  return this.json;
package/src/Circle.ts CHANGED
@@ -63,9 +63,12 @@ export class Circle {
63
63
  }
64
64
 
65
65
  /**
66
- * Sample the circle into evenly-spaced points.
66
+ * Sample the circle into evenly-spaced points across the full closed
67
+ * parameter range INCLUSIVE of both ends: the last point coincides with
68
+ * the first (the seam). Consumers building a closed polygon from a circle
69
+ * must drop the final duplicate (see Mesh.buildPlanarCircle).
67
70
  * @param n - Number of points to generate
68
- * @returns Array of n points around the circle
71
+ * @returns Array of n points around the circle, first == last
69
72
  */
70
73
  sample(n: number): Point[] {
71
74
  ensureInit();
@@ -139,25 +142,25 @@ export class Circle {
139
142
  * @param caps - Whether to cap the ends (default true)
140
143
  * @returns Mesh representing the extruded cylinder
141
144
  */
142
- extrude(direction: Vec3, segments = 16, caps = true): Mesh {
143
- ensureInit();
144
- const buf = wasm.extrude_circle(
145
- this.center.x, this.center.y, this.center.z,
146
- this.normal.x, this.normal.y, this.normal.z,
145
+ extrude(direction: Vec3, segments = 16, caps = true): Mesh {
146
+ ensureInit();
147
+ const buf = wasm.extrude_circle(
148
+ this.center.x, this.center.y, this.center.z,
149
+ this.normal.x, this.normal.y, this.normal.z,
147
150
  this.radius,
148
151
  direction.x, direction.y, direction.z,
149
152
  segments, caps
150
- );
151
- return Mesh.fromBuffer(buf);
152
- }
153
-
154
- /**
155
- * Extrude this closed circle into a trusted solid suitable for booleans.
156
- * @param direction - Extrusion direction and magnitude
157
- * @param segments - Number of boundary samples used for tessellation (default 32)
158
- * @returns Closed mesh solid ready for boolean operations
159
- */
160
- extrudeAsSolid(direction: Vec3, segments = 32): Mesh {
161
- return Mesh.extrudeCurveAsSolid(this, direction, segments);
162
- }
163
- }
153
+ );
154
+ return Mesh.fromBuffer(buf);
155
+ }
156
+
157
+ /**
158
+ * Extrude this closed circle into a trusted solid suitable for booleans.
159
+ * @param direction - Extrusion direction and magnitude
160
+ * @param segments - Number of boundary samples used for tessellation (default 32)
161
+ * @returns Closed mesh solid ready for boolean operations
162
+ */
163
+ extrudeAsSolid(direction: Vec3, segments = 32): Mesh {
164
+ return Mesh.extrudeCurveAsSolid(this, direction, segments);
165
+ }
166
+ }