okgeometry-api 1.15.0 → 1.17.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/package.json CHANGED
@@ -1,54 +1,58 @@
1
- {
2
- "name": "okgeometry-api",
3
- "version": "1.15.0",
4
- "description": "Geometry engine API for AEC applications — NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist/",
16
- "src/"
17
- ],
18
- "scripts": {
19
- "build:wasm": "node -e \"require('fs').rmSync('wasm',{recursive:true,force:true})\" && cd .. && wasm-pack build --target web --out-dir okgeometry-api/wasm",
20
- "sync:wasm-bindings": "node scripts/sync-wasm-bindings.mjs",
21
- "inline-wasm": "tsx scripts/inline-wasm.ts",
22
- "build": "npm run build:wasm && npm run sync:wasm-bindings && npm run inline-wasm && tsc",
23
- "bench:boolean-heavy": "npm run build && tsx scripts/bench-boolean-heavy.ts",
24
- "bench:boolean-ab": "npm run build && tsx scripts/bench-boolean-ab.ts",
25
- "bench:boolean-batch": "npm run build && tsx scripts/bench-boolean-batch.ts",
26
- "smoke:brep-boolean": "tsx scripts/smoke-brep-boolean.ts",
27
- "prepublishOnly": "npm run build",
28
- "smoke:brep-parametric": "tsx scripts/smoke-brep-parametric.ts",
29
- "smoke:brep-from-mesh": "tsx scripts/smoke-brep-from-mesh.ts",
30
- "smoke:brep-faces": "tsx scripts/smoke-brep-faces.ts",
31
- "smoke:curve-intersections": "tsx scripts/smoke-curve-intersections.ts",
32
- "smoke:brep-intersections": "tsx scripts/smoke-brep-intersections.ts"
33
- },
34
- "keywords": [
35
- "geometry",
36
- "wasm",
37
- "nurbs",
38
- "mesh",
39
- "boolean",
40
- "cad",
41
- "aec",
42
- "3d"
43
- ],
44
- "author": "Mostafa El Ayoubi",
45
- "license": "Proprietary License",
46
- "repository": {
47
- "type": "git",
48
- "url": "https://www.orkestra.online"
49
- },
50
- "devDependencies": {
51
- "tsx": "^4.7.0",
52
- "typescript": "^5.4.0"
53
- }
54
- }
1
+ {
2
+ "name": "okgeometry-api",
3
+ "version": "1.17.0",
4
+ "description": "Geometry engine API for AEC applications - NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "src/"
17
+ ],
18
+ "scripts": {
19
+ "build:wasm": "node -e \"require('fs').rmSync('wasm',{recursive:true,force:true})\" && cd .. && wasm-pack build --target web --out-dir okgeometry-api/wasm",
20
+ "sync:wasm-bindings": "node scripts/sync-wasm-bindings.mjs",
21
+ "inline-wasm": "tsx scripts/inline-wasm.ts",
22
+ "build": "npm run build:wasm && npm run sync:wasm-bindings && npm run inline-wasm && tsc",
23
+ "bench:boolean-heavy": "npm run build && tsx scripts/bench-boolean-heavy.ts",
24
+ "bench:boolean-ab": "npm run build && tsx scripts/bench-boolean-ab.ts",
25
+ "bench:boolean-batch": "npm run build && tsx scripts/bench-boolean-batch.ts",
26
+ "bench:boolean-matrix": "tsx scripts/bench-boolean-matrix.ts",
27
+ "smoke:brep-boolean": "tsx scripts/smoke-brep-boolean.ts",
28
+ "prepublishOnly": "npm run build",
29
+ "smoke:brep-parametric": "tsx scripts/smoke-brep-parametric.ts",
30
+ "smoke:brep-from-mesh": "tsx scripts/smoke-brep-from-mesh.ts",
31
+ "smoke:brep-faces": "tsx scripts/smoke-brep-faces.ts",
32
+ "smoke:curve-intersections": "tsx scripts/smoke-curve-intersections.ts",
33
+ "smoke:brep-intersections": "tsx scripts/smoke-brep-intersections.ts",
34
+ "smoke:brep-sheet-boolean": "tsx scripts/smoke-brep-sheet-boolean.ts",
35
+ "smoke:geometry-boolean": "tsx scripts/smoke-geometry-boolean.ts",
36
+ "smoke:curve-region": "tsx scripts/smoke-curve-region.ts"
37
+ },
38
+ "keywords": [
39
+ "geometry",
40
+ "wasm",
41
+ "nurbs",
42
+ "mesh",
43
+ "boolean",
44
+ "cad",
45
+ "aec",
46
+ "3d"
47
+ ],
48
+ "author": "Mostafa El Ayoubi",
49
+ "license": "Proprietary License",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://www.orkestra.online"
53
+ },
54
+ "devDependencies": {
55
+ "tsx": "^4.7.0",
56
+ "typescript": "^5.4.0"
57
+ }
58
+ }
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;
@@ -624,12 +639,27 @@ export class Brep {
624
639
  *
625
640
  * `options.tolerance` is the absolute geometric tolerance of the fitted
626
641
  * intersection curves (default 1e-6).
642
+ *
643
+ * `options.join` (default false) JOINS the chained pieces into maximal
644
+ * curves: greedy reversal-aware endpoint chaining, exact degree elevation
645
+ * to the chain maximum, exact knot-vector concatenation (junction
646
+ * multiplicity = degree, no re-parameterization), and a per-curve
647
+ * verification gate — every joined curve is sampled densely and must stay
648
+ * within 5·tolerance of BOTH operands' surfaces, otherwise that chain's
649
+ * pieces come back unjoined (never a corrupted joined curve). A closed
650
+ * intersection loop (e.g. a plane through a cylinder) comes back as ONE
651
+ * closed curve with `pointAt(0)` equal to `pointAt(1)`.
627
652
  */
628
- intersectionCurves(other: Brep | NurbsSurface, options?: { tolerance?: number }): NurbsCurve[] {
653
+ intersectionCurves(
654
+ other: Brep | NurbsSurface,
655
+ options?: { tolerance?: number; join?: boolean },
656
+ ): NurbsCurve[] {
629
657
  ensureInit();
630
658
  const rhs = other instanceof Brep ? other : Brep.fromSurface(other);
631
659
  const tolerance = options?.tolerance ?? 1e-6;
632
- const buf = wasm.brep_intersection_curves_op(this.json, rhs.json, tolerance);
660
+ const buf = options?.join
661
+ ? wasm.brep_intersection_curves_joined_op(this.json, rhs.json, tolerance)
662
+ : wasm.brep_intersection_curves_op(this.json, rhs.json, tolerance);
633
663
  // An EMPTY buffer signals a kernel error (invalid BREP / pipeline
634
664
  // failure); "no intersections" is the non-empty `[0]` packing.
635
665
  if (buf.length < 1) {
@@ -755,14 +785,34 @@ export class Brep {
755
785
 
756
786
  // ── Parametric booleans (BREP → BREP) ──
757
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
+
758
807
  private runParametricBoolean(
759
- other: Brep,
808
+ other: Brep | Mesh,
760
809
  op: "union" | "subtract" | "intersect",
761
810
  options?: BrepParametricBooleanOptions,
762
811
  ): Brep {
763
812
  ensureInit();
813
+ const rhs = Brep.resolveParametricOperand(other);
764
814
  const tolerance = options?.tolerance ?? 1e-6;
765
- 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);
766
816
  if (!json) throw new Error(`Brep.${op}Brep failed`);
767
817
  if (json.startsWith('{"error"')) {
768
818
  const parsed = JSON.parse(json) as { error: string };
@@ -776,7 +826,30 @@ export class Brep {
776
826
  * trimmed-surface output. Result faces keep their parents' exact surfaces
777
827
  * (planes/NURBS, untessellated); intersection edges are Newton-refined SSI
778
828
  * curves stored as tolerant NURBS edges with matched pcurves on both
779
- * 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.
780
853
  *
781
854
  * `options.tolerance` is the absolute geometric tolerance of intersection
782
855
  * edges and coincident-face classification (default 1e-6).
@@ -785,17 +858,27 @@ export class Brep {
785
858
  * cylinders touching along a line/point) is not resolved into pinch
786
859
  * topology; transversal and coincident (coplanar) configurations are exact.
787
860
  */
788
- unionBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
861
+ unionBrep(other: Brep | Mesh, options?: BrepParametricBooleanOptions): Brep {
789
862
  return this.runParametricBoolean(other, "union", options);
790
863
  }
791
864
 
792
- /** PARAMETRIC boolean subtraction (this − other). See {@link unionBrep}. */
793
- 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 {
794
872
  return this.runParametricBoolean(other, "subtract", options);
795
873
  }
796
874
 
797
- /** PARAMETRIC boolean intersection. See {@link unionBrep}. */
798
- 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 {
799
882
  return this.runParametricBoolean(other, "intersect", options);
800
883
  }
801
884
 
@@ -888,6 +971,60 @@ export class Brep {
888
971
  return { negative: side(parsed.negative), positive: side(parsed.positive) };
889
972
  }
890
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
+
891
1028
  /** JSON representation (persistence). */
892
1029
  toJson(): string {
893
1030
  return this.json;