okgeometry-api 1.2.0 → 1.4.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.
Files changed (65) hide show
  1. package/dist/Line.d.ts +10 -1
  2. package/dist/Line.d.ts.map +1 -1
  3. package/dist/Line.js +11 -0
  4. package/dist/Line.js.map +1 -1
  5. package/dist/Mesh.d.ts +123 -9
  6. package/dist/Mesh.d.ts.map +1 -1
  7. package/dist/Mesh.js +401 -26
  8. package/dist/Mesh.js.map +1 -1
  9. package/dist/MeshSurface.d.ts +32 -0
  10. package/dist/MeshSurface.d.ts.map +1 -0
  11. package/dist/MeshSurface.js +51 -0
  12. package/dist/MeshSurface.js.map +1 -0
  13. package/dist/NurbsCurve.d.ts +50 -2
  14. package/dist/NurbsCurve.d.ts.map +1 -1
  15. package/dist/NurbsCurve.js +76 -2
  16. package/dist/NurbsCurve.js.map +1 -1
  17. package/dist/NurbsSurface.d.ts +9 -1
  18. package/dist/NurbsSurface.d.ts.map +1 -1
  19. package/dist/NurbsSurface.js +12 -3
  20. package/dist/NurbsSurface.js.map +1 -1
  21. package/dist/PolyCurve.d.ts +21 -3
  22. package/dist/PolyCurve.d.ts.map +1 -1
  23. package/dist/PolyCurve.js +82 -38
  24. package/dist/PolyCurve.js.map +1 -1
  25. package/dist/Polygon.d.ts +13 -2
  26. package/dist/Polygon.d.ts.map +1 -1
  27. package/dist/Polygon.js +21 -3
  28. package/dist/Polygon.js.map +1 -1
  29. package/dist/Polyline.d.ts +19 -2
  30. package/dist/Polyline.d.ts.map +1 -1
  31. package/dist/Polyline.js +38 -6
  32. package/dist/Polyline.js.map +1 -1
  33. package/dist/Surface.d.ts +17 -0
  34. package/dist/Surface.d.ts.map +1 -0
  35. package/dist/Surface.js +2 -0
  36. package/dist/Surface.js.map +1 -0
  37. package/dist/index.d.ts +4 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/types.d.ts +13 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/wasm-base64.d.ts +1 -1
  44. package/dist/wasm-base64.d.ts.map +1 -1
  45. package/dist/wasm-base64.js +1 -1
  46. package/dist/wasm-base64.js.map +1 -1
  47. package/dist/wasm-bindings.d.ts +132 -2
  48. package/dist/wasm-bindings.d.ts.map +1 -1
  49. package/dist/wasm-bindings.js +207 -2
  50. package/dist/wasm-bindings.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/Line.ts +38 -20
  53. package/src/Mesh.ts +909 -464
  54. package/src/MeshSurface.ts +72 -0
  55. package/src/NurbsCurve.ts +103 -3
  56. package/src/NurbsSurface.ts +28 -13
  57. package/src/PolyCurve.ts +157 -85
  58. package/src/Polygon.ts +34 -4
  59. package/src/Polyline.ts +74 -24
  60. package/src/Surface.ts +18 -0
  61. package/src/index.ts +5 -0
  62. package/src/types.ts +15 -0
  63. package/src/wasm-base64.ts +1 -1
  64. package/src/wasm-bindings.d.ts +101 -2
  65. package/src/wasm-bindings.js +218 -2
package/dist/Mesh.js CHANGED
@@ -7,6 +7,7 @@ import { Line } from "./Line.js";
7
7
  import { Circle } from "./Circle.js";
8
8
  import { Arc } from "./Arc.js";
9
9
  import { Polygon } from "./Polygon.js";
10
+ import { MeshSurface } from "./MeshSurface.js";
10
11
  import { NurbsCurve } from "./NurbsCurve.js";
11
12
  import { PolyCurve } from "./PolyCurve.js";
12
13
  import { pointsToCoords, parsePolylineBuffer as parsePolylineBuf } from "./BufferCodec.js";
@@ -34,6 +35,10 @@ export class Mesh {
34
35
  this._indexBuffer = null;
35
36
  this._vertices = null;
36
37
  this._faces = null;
38
+ this._planarFaces = null;
39
+ this._planarFacesByTriangleIndex = null;
40
+ this._surfaces = null;
41
+ this._surfacesByTriangleIndex = null;
37
42
  this._edgeVertexPairs = null;
38
43
  this._topologyMetricsCache = null;
39
44
  this._isClosedVolumeCache = null;
@@ -117,6 +122,55 @@ export class Mesh {
117
122
  const diag = Math.hypot(dx, dy, dz);
118
123
  return Number.isFinite(diag) && diag > 0 ? diag : 1;
119
124
  }
125
+ static computeSignedVolumeFromBuffer(buffer) {
126
+ const vertexCount = Math.max(0, Math.floor(buffer[0] ?? 0));
127
+ if (vertexCount <= 0)
128
+ return 0;
129
+ const indexStart = 1 + vertexCount * 3;
130
+ let signedVolume = 0;
131
+ for (let i = indexStart; i + 2 < buffer.length; i += 3) {
132
+ const ia = Math.floor(buffer[i] ?? -1);
133
+ const ib = Math.floor(buffer[i + 1] ?? -1);
134
+ const ic = Math.floor(buffer[i + 2] ?? -1);
135
+ if (ia < 0 || ib < 0 || ic < 0 || ia >= vertexCount || ib >= vertexCount || ic >= vertexCount)
136
+ continue;
137
+ const ax = buffer[1 + ia * 3];
138
+ const ay = buffer[1 + ia * 3 + 1];
139
+ const az = buffer[1 + ia * 3 + 2];
140
+ const bx = buffer[1 + ib * 3];
141
+ const by = buffer[1 + ib * 3 + 1];
142
+ const bz = buffer[1 + ib * 3 + 2];
143
+ const cx = buffer[1 + ic * 3];
144
+ const cy = buffer[1 + ic * 3 + 1];
145
+ const cz = buffer[1 + ic * 3 + 2];
146
+ signedVolume += ax * (by * cz - bz * cy)
147
+ - ay * (bx * cz - bz * cx)
148
+ + az * (bx * cy - by * cx);
149
+ }
150
+ return signedVolume / 6;
151
+ }
152
+ static reverseTriangleWinding(buffer) {
153
+ const flipped = new Float64Array(buffer);
154
+ const vertexCount = Math.max(0, Math.floor(flipped[0] ?? 0));
155
+ const indexStart = 1 + vertexCount * 3;
156
+ for (let i = indexStart; i + 2 < flipped.length; i += 3) {
157
+ const tmp = flipped[i + 1];
158
+ flipped[i + 1] = flipped[i + 2];
159
+ flipped[i + 2] = tmp;
160
+ }
161
+ return flipped;
162
+ }
163
+ static normalizeClosedVolumeOrientation(mesh) {
164
+ if (mesh.faceCount === 0 || mesh._vertexCount <= 0)
165
+ return mesh;
166
+ const topology = mesh.topologyMetrics();
167
+ if (topology.boundaryEdges !== 0 || topology.nonManifoldEdges !== 0)
168
+ return mesh;
169
+ const signedVolume = Mesh.computeSignedVolumeFromBuffer(mesh._buffer);
170
+ if (!Number.isFinite(signedVolume) || signedVolume >= 0)
171
+ return mesh;
172
+ return new Mesh(Mesh.reverseTriangleWinding(mesh._buffer), mesh._trustedBooleanInput);
173
+ }
120
174
  static booleanContactTolerance(a, b) {
121
175
  const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
122
176
  return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
@@ -532,7 +586,7 @@ export class Mesh {
532
586
  get vertexCount() {
533
587
  return this._vertexCount;
534
588
  }
535
- /** Number of triangular faces in this mesh */
589
+ /** Number of triangular faces in this mesh. Use `planarFaceCount` for logical planar faces. */
536
590
  get faceCount() {
537
591
  const start = 1 + this._vertexCount * 3;
538
592
  if (this._buffer.length <= start)
@@ -558,6 +612,7 @@ export class Mesh {
558
612
  /**
559
613
  * Get all faces as arrays of vertex indices.
560
614
  * Each face is [i0, i1, i2] for the three triangle vertices.
615
+ * For coplanar edge-connected logical faces, use `planarFaces`.
561
616
  * Lazy-computed and cached.
562
617
  */
563
618
  get faces() {
@@ -571,6 +626,42 @@ export class Mesh {
571
626
  }
572
627
  return this._faces;
573
628
  }
629
+ /**
630
+ * Alias for triangle faces, mirroring `faces`.
631
+ * Use `planarFaces` when you want logical mesh faces such as the 6 faces of a box.
632
+ */
633
+ get triangles() {
634
+ return this.faces;
635
+ }
636
+ /**
637
+ * Number of coplanar edge-connected planar faces in this mesh.
638
+ * Examples: a box has 6, an L-shaped extrusion has 8.
639
+ */
640
+ get planarFaceCount() {
641
+ return this.planarFaces.length;
642
+ }
643
+ /**
644
+ * Logical mesh faces built by grouping coplanar edge-connected triangles.
645
+ * Cached after the first query to keep repeated face access cheap.
646
+ */
647
+ get planarFaces() {
648
+ this.ensurePlanarFaceCache();
649
+ return this._planarFaces ?? [];
650
+ }
651
+ /**
652
+ * Number of logical planar surfaces in this mesh.
653
+ * Mirrors `planarFaceCount`, but returns wrapper objects with surface queries.
654
+ */
655
+ get surfaceCount() {
656
+ return this.surfaces.length;
657
+ }
658
+ /**
659
+ * Logical planar mesh surfaces with `evaluate(u, v)` and `normal(u, v)` methods.
660
+ */
661
+ get surfaces() {
662
+ this.ensureSurfaceCache();
663
+ return this._surfaces ?? [];
664
+ }
574
665
  /** Raw WASM buffer (for advanced use / re-passing to WASM) */
575
666
  get rawBuffer() {
576
667
  return this._buffer;
@@ -585,11 +676,111 @@ export class Mesh {
585
676
  return new Mesh(buffer, options?.trustedBooleanInput ?? false);
586
677
  }
587
678
  static fromTrustedBuffer(buffer) {
588
- return new Mesh(buffer, true);
679
+ return Mesh.normalizeClosedVolumeOrientation(new Mesh(buffer, true));
589
680
  }
590
681
  get trustedBooleanInput() {
591
682
  return this._trustedBooleanInput;
592
683
  }
684
+ getTriangleVertexIndices(faceIndex) {
685
+ if (!Number.isFinite(faceIndex) || faceIndex < 0 || faceIndex >= this.faceCount)
686
+ return null;
687
+ const indexBuffer = this.indexBuffer;
688
+ const offset = Math.floor(faceIndex) * 3;
689
+ if (offset + 2 >= indexBuffer.length)
690
+ return null;
691
+ return [indexBuffer[offset], indexBuffer[offset + 1], indexBuffer[offset + 2]];
692
+ }
693
+ computeTriangleAreaByIndex(faceIndex) {
694
+ const tri = this.getTriangleVertexIndices(faceIndex);
695
+ if (!tri)
696
+ return 0;
697
+ const [i0, i1, i2] = tri;
698
+ const base = 1;
699
+ const off0 = base + i0 * 3;
700
+ const off1 = base + i1 * 3;
701
+ const off2 = base + i2 * 3;
702
+ const ax = this._buffer[off0] ?? 0;
703
+ const ay = this._buffer[off0 + 1] ?? 0;
704
+ const az = this._buffer[off0 + 2] ?? 0;
705
+ const bx = this._buffer[off1] ?? 0;
706
+ const by = this._buffer[off1 + 1] ?? 0;
707
+ const bz = this._buffer[off1 + 2] ?? 0;
708
+ const cx = this._buffer[off2] ?? 0;
709
+ const cy = this._buffer[off2 + 1] ?? 0;
710
+ const cz = this._buffer[off2 + 2] ?? 0;
711
+ const abx = bx - ax;
712
+ const aby = by - ay;
713
+ const abz = bz - az;
714
+ const acx = cx - ax;
715
+ const acy = cy - ay;
716
+ const acz = cz - az;
717
+ const crossX = aby * acz - abz * acy;
718
+ const crossY = abz * acx - abx * acz;
719
+ const crossZ = abx * acy - aby * acx;
720
+ return 0.5 * Math.sqrt(crossX * crossX + crossY * crossY + crossZ * crossZ);
721
+ }
722
+ ensurePlanarFaceCache() {
723
+ if (this._planarFaces && this._planarFacesByTriangleIndex)
724
+ return;
725
+ ensureInit();
726
+ const raw = wasm.mesh_build_coplanar_connected_face_groups(this._vertexCount, this._buffer);
727
+ const declaredGroupCount = Math.max(0, Math.floor(raw[0] ?? 0));
728
+ const planarFaces = [];
729
+ const planarFacesByTriangleIndex = new Array(this.faceCount).fill(null);
730
+ let offset = 1;
731
+ for (let groupIndex = 0; groupIndex < declaredGroupCount && offset < raw.length; groupIndex++) {
732
+ const declaredTriangleCount = Math.max(0, Math.floor(raw[offset] ?? 0));
733
+ offset += 1;
734
+ const triangleIndices = [];
735
+ let area = 0;
736
+ for (let i = 0; i < declaredTriangleCount && offset < raw.length; i++, offset++) {
737
+ const triangleIndex = Math.floor(raw[offset] ?? -1);
738
+ if (triangleIndex < 0 || triangleIndex >= this.faceCount)
739
+ continue;
740
+ triangleIndices.push(triangleIndex);
741
+ area += this.computeTriangleAreaByIndex(triangleIndex);
742
+ }
743
+ if (offset + 5 >= raw.length)
744
+ break;
745
+ const centroid = new Point(raw[offset] ?? 0, raw[offset + 1] ?? 0, raw[offset + 2] ?? 0);
746
+ const normal = new Vec3(raw[offset + 3] ?? 0, raw[offset + 4] ?? 0, raw[offset + 5] ?? 0).normalize();
747
+ offset += 6;
748
+ if (triangleIndices.length === 0)
749
+ continue;
750
+ const planarFace = {
751
+ index: planarFaces.length,
752
+ seedTriangleIndex: triangleIndices[0],
753
+ triangleIndices,
754
+ triangleCount: triangleIndices.length,
755
+ centroid,
756
+ normal,
757
+ area,
758
+ };
759
+ planarFaces.push(planarFace);
760
+ for (const triangleIndex of triangleIndices) {
761
+ planarFacesByTriangleIndex[triangleIndex] = planarFace;
762
+ }
763
+ }
764
+ this._planarFaces = planarFaces;
765
+ this._planarFacesByTriangleIndex = planarFacesByTriangleIndex;
766
+ this._surfaces = null;
767
+ this._surfacesByTriangleIndex = null;
768
+ }
769
+ ensureSurfaceCache() {
770
+ if (this._surfaces && this._surfacesByTriangleIndex)
771
+ return;
772
+ const surfaces = this.planarFaces.map(face => new MeshSurface(this, face));
773
+ const surfacesByTriangleIndex = new Array(this.faceCount).fill(null);
774
+ for (const surface of surfaces) {
775
+ for (const triangleIndex of surface.triangleIndices) {
776
+ if (triangleIndex >= 0 && triangleIndex < surfacesByTriangleIndex.length) {
777
+ surfacesByTriangleIndex[triangleIndex] = surface;
778
+ }
779
+ }
780
+ }
781
+ this._surfaces = surfaces;
782
+ this._surfacesByTriangleIndex = surfacesByTriangleIndex;
783
+ }
593
784
  debugSummary() {
594
785
  const bounds = Mesh.toDebugBounds(this.getBounds());
595
786
  const topology = this.topologyMetrics();
@@ -933,6 +1124,31 @@ export class Mesh {
933
1124
  }
934
1125
  return Mesh.fromTrustedBuffer(wasm.loft_polylines(new Float64Array(parts), segments, caps));
935
1126
  }
1127
+ /**
1128
+ * Loft through multiple raw profiles with kernel-managed compatibility:
1129
+ * profiles are resampled to a common point count (uniform arc length), and
1130
+ * closed profiles get seam alignment + winding direction matching so the
1131
+ * loft does not twist.
1132
+ *
1133
+ * @param profiles - Array of point arrays defining cross-sections. Closed
1134
+ * profiles must NOT repeat the first point at the end.
1135
+ * @param closed - Whether profiles are closed loops (tube) or open rows (sheet)
1136
+ * @param caps - Cap first/last profiles (closed profiles only) to produce a
1137
+ * watertight solid ready for booleans (default false)
1138
+ * @returns New Mesh representing the lofted surface
1139
+ */
1140
+ static loftProfiles(profiles, closed, caps = false) {
1141
+ ensureInit();
1142
+ // Format: [count1, x,y,z,..., count2, x,y,z,...]
1143
+ const parts = [];
1144
+ for (const profile of profiles) {
1145
+ parts.push(profile.length);
1146
+ for (const p of profile) {
1147
+ parts.push(p.x, p.y, p.z);
1148
+ }
1149
+ }
1150
+ return Mesh.fromTrustedBuffer(wasm.loft_profiles(new Float64Array(parts), closed, caps));
1151
+ }
936
1152
  /**
937
1153
  * Sweep a profile polyline along a path polyline.
938
1154
  * @param profilePoints - Profile cross-section points
@@ -1298,6 +1514,34 @@ export class Mesh {
1298
1514
  }
1299
1515
  return Mesh.fromTrustedBuffer(result);
1300
1516
  }
1517
+ static mergeMeshComponents(meshes) {
1518
+ if (meshes.length === 0)
1519
+ return Mesh.emptyMesh();
1520
+ if (meshes.length === 1)
1521
+ return Mesh.cloneMesh(meshes[0]);
1522
+ return Mesh.mergeMeshes(meshes);
1523
+ }
1524
+ static createUnsupportedOpenMeshBooleanError(operation, aClosed, bClosed) {
1525
+ const topology = `A=${aClosed ? "closed" : "open"}, B=${bClosed ? "closed" : "open"}`;
1526
+ if (operation === "union") {
1527
+ return new Error(`Boolean union requires both inputs to be closed volumes (${topology}). `
1528
+ + "Close or cap the open mesh before calling union().");
1529
+ }
1530
+ return new Error(`Boolean ${operation} with open meshes is only supported when A is open and B is closed `
1531
+ + `(surface trim mode). Got ${topology}. Use Mesh.split(...) and choose outside/inside explicitly `
1532
+ + "for other open-mesh combinations.");
1533
+ }
1534
+ resolveOpenMeshBooleanFallback(other, operation, options) {
1535
+ const aClosed = this.isClosedVolume();
1536
+ const bClosed = other.isClosedVolume();
1537
+ if (aClosed && bClosed)
1538
+ return null;
1539
+ if (!aClosed && bClosed && (operation === "subtraction" || operation === "intersection")) {
1540
+ const split = this.splitWithMesh(other, options);
1541
+ return Mesh.mergeMeshComponents(operation === "subtraction" ? split.outside : split.inside);
1542
+ }
1543
+ throw Mesh.createUnsupportedOpenMeshBooleanError(operation, aClosed, bClosed);
1544
+ }
1301
1545
  static encodeBooleanOperationToken(operation, a, b, options) {
1302
1546
  const tokens = [operation];
1303
1547
  if (a._trustedBooleanInput && b._trustedBooleanInput) {
@@ -1321,36 +1565,54 @@ export class Mesh {
1321
1565
  // ── Booleans ───────────────────────────────────────────────────
1322
1566
  /**
1323
1567
  * Compute boolean union with another mesh.
1568
+ * Requires both inputs to be closed volumes.
1324
1569
  * @param other - Mesh to union with
1325
1570
  * @param options - Optional safety overrides
1326
1571
  * @returns New mesh containing volume of both inputs
1327
1572
  */
1328
1573
  union(other, options) {
1329
1574
  ensureInit();
1330
- const operationToken = Mesh.encodeBooleanOperationToken("union", this, other, options);
1331
- return this.runBoolean(other, "union", () => wasm.mesh_boolean_operation(this._vertexCount, this._buffer, other._vertexCount, other._buffer, operationToken), options);
1575
+ const lhs = Mesh.normalizeClosedVolumeOrientation(this);
1576
+ const rhs = Mesh.normalizeClosedVolumeOrientation(other);
1577
+ lhs.resolveOpenMeshBooleanFallback(rhs, "union", options);
1578
+ const operationToken = Mesh.encodeBooleanOperationToken("union", lhs, rhs, options);
1579
+ return lhs.runBoolean(rhs, "union", () => wasm.mesh_boolean_operation(lhs._vertexCount, lhs._buffer, rhs._vertexCount, rhs._buffer, operationToken), options);
1332
1580
  }
1333
1581
  /**
1334
1582
  * Compute boolean subtraction with another mesh.
1583
+ * If this mesh is open and `other` is a closed cutter, this trims the open
1584
+ * host via `split()` and returns the outside surface pieces merged together.
1335
1585
  * @param other - Mesh to subtract
1336
1586
  * @param options - Optional safety overrides
1337
1587
  * @returns New mesh with other's volume removed from this
1338
1588
  */
1339
1589
  subtract(other, options) {
1340
1590
  ensureInit();
1341
- const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
1342
- return this.runBoolean(other, "subtraction", () => wasm.mesh_boolean_operation(this._vertexCount, this._buffer, other._vertexCount, other._buffer, operationToken), options);
1591
+ const lhs = Mesh.normalizeClosedVolumeOrientation(this);
1592
+ const rhs = Mesh.normalizeClosedVolumeOrientation(other);
1593
+ const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "subtraction", options);
1594
+ if (fallback)
1595
+ return fallback;
1596
+ const operationToken = Mesh.encodeBooleanOperationToken("subtraction", lhs, rhs, options);
1597
+ return lhs.runBoolean(rhs, "subtraction", () => wasm.mesh_boolean_operation(lhs._vertexCount, lhs._buffer, rhs._vertexCount, rhs._buffer, operationToken), options);
1343
1598
  }
1344
1599
  /**
1345
1600
  * Compute boolean intersection with another mesh.
1601
+ * If this mesh is open and `other` is a closed cutter, this trims the open
1602
+ * host via `split()` and returns the inside surface pieces merged together.
1346
1603
  * @param other - Mesh to intersect with
1347
1604
  * @param options - Optional safety overrides
1348
1605
  * @returns New mesh containing only the overlapping volume
1349
1606
  */
1350
1607
  intersect(other, options) {
1351
1608
  ensureInit();
1352
- const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
1353
- return this.runBoolean(other, "intersection", () => wasm.mesh_boolean_operation(this._vertexCount, this._buffer, other._vertexCount, other._buffer, operationToken), options);
1609
+ const lhs = Mesh.normalizeClosedVolumeOrientation(this);
1610
+ const rhs = Mesh.normalizeClosedVolumeOrientation(other);
1611
+ const fallback = lhs.resolveOpenMeshBooleanFallback(rhs, "intersection", options);
1612
+ if (fallback)
1613
+ return fallback;
1614
+ const operationToken = Mesh.encodeBooleanOperationToken("intersection", lhs, rhs, options);
1615
+ return lhs.runBoolean(rhs, "intersection", () => wasm.mesh_boolean_operation(lhs._vertexCount, lhs._buffer, rhs._vertexCount, rhs._buffer, operationToken), options);
1354
1616
  }
1355
1617
  splitWithMesh(other, options) {
1356
1618
  ensureInit();
@@ -1672,6 +1934,53 @@ export class Mesh {
1672
1934
  }
1673
1935
  return out;
1674
1936
  }
1937
+ /**
1938
+ * Find the edge-connected smooth face group containing a triangle: flood
1939
+ * fill crossing only edges whose dihedral angle is below `maxAngleDeg`.
1940
+ * On a tessellated curved surface (cylinder wall, lofted patch) this
1941
+ * returns the whole smooth region (`isPlanar: false`); on a flat face it
1942
+ * matches the coplanar group (`isPlanar: true`).
1943
+ */
1944
+ getSmoothFaceGroup(faceIndex, maxAngleDeg = 30) {
1945
+ ensureInit();
1946
+ if (!Number.isFinite(faceIndex) || faceIndex < 0) {
1947
+ return { faceIndices: [], isPlanar: true };
1948
+ }
1949
+ const r = wasm.mesh_get_smooth_face_group(this._vertexCount, this._buffer, Math.floor(faceIndex), maxAngleDeg);
1950
+ const count = Math.max(0, Math.floor(r[0] ?? 0));
1951
+ const isPlanar = (r[1] ?? 1) !== 0;
1952
+ const faceIndices = [];
1953
+ for (let i = 0; i < count; i++) {
1954
+ faceIndices.push(Math.floor(r[2 + i] ?? 0));
1955
+ }
1956
+ return { faceIndices, isPlanar };
1957
+ }
1958
+ /**
1959
+ * Display-ready render buffers computed in one kernel pass: creased
1960
+ * (crease-split) vertex normals plus feature edge segments. Replaces
1961
+ * per-mesh THREE.toCreasedNormals + EdgesGeometry in app layers.
1962
+ *
1963
+ * `positions`/`normals` are non-indexed, three floats per vertex, in the
1964
+ * mesh's triangle order (raycast face indices stay valid). `featureEdges`
1965
+ * holds two endpoints (six floats) per feature edge segment: boundary
1966
+ * edges, non-manifold edges, and edges whose dihedral angle exceeds
1967
+ * `creaseAngleDeg`.
1968
+ */
1969
+ buildRenderBuffers(creaseAngleDeg = 30) {
1970
+ ensureInit();
1971
+ const r = wasm.mesh_build_render_buffers(this._vertexCount, this._buffer, creaseAngleDeg);
1972
+ const vertexCount = Math.max(0, Math.floor(r[0] ?? 0));
1973
+ const posStart = 1;
1974
+ const norStart = posStart + vertexCount * 3;
1975
+ const edgeCountIndex = norStart + vertexCount * 3;
1976
+ const edgeCount = Math.max(0, Math.floor(r[edgeCountIndex] ?? 0));
1977
+ const edgeStart = edgeCountIndex + 1;
1978
+ return {
1979
+ positions: r.subarray(posStart, posStart + vertexCount * 3),
1980
+ normals: r.subarray(norStart, norStart + vertexCount * 3),
1981
+ featureEdges: r.subarray(edgeStart, edgeStart + edgeCount * 6),
1982
+ };
1983
+ }
1675
1984
  /**
1676
1985
  * Coplanar connected face region and its projection bounds on a plane basis.
1677
1986
  * Returns null for invalid inputs or empty regions.
@@ -1841,6 +2150,88 @@ export class Mesh {
1841
2150
  nonManifoldEdges,
1842
2151
  };
1843
2152
  }
2153
+ /**
2154
+ * Get a logical planar face by its planar-face index.
2155
+ */
2156
+ getPlanarFace(index) {
2157
+ if (!Number.isFinite(index) || index < 0)
2158
+ return null;
2159
+ const planarFaces = this.planarFaces;
2160
+ return planarFaces[Math.floor(index)] ?? null;
2161
+ }
2162
+ /**
2163
+ * Get a logical planar surface wrapper by its planar-face index.
2164
+ */
2165
+ getSurface(index) {
2166
+ if (!Number.isFinite(index) || index < 0)
2167
+ return null;
2168
+ const surfaces = this.surfaces;
2169
+ return surfaces[Math.floor(index)] ?? null;
2170
+ }
2171
+ /**
2172
+ * Get the logical planar face that contains a triangle face index.
2173
+ */
2174
+ getPlanarFaceByTriangleIndex(triangleIndex) {
2175
+ if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
2176
+ return null;
2177
+ this.ensurePlanarFaceCache();
2178
+ return this._planarFacesByTriangleIndex?.[Math.floor(triangleIndex)] ?? null;
2179
+ }
2180
+ /**
2181
+ * Get the logical planar surface wrapper that contains a triangle face index.
2182
+ */
2183
+ getSurfaceByTriangleIndex(triangleIndex) {
2184
+ if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
2185
+ return null;
2186
+ this.ensureSurfaceCache();
2187
+ return this._surfacesByTriangleIndex?.[Math.floor(triangleIndex)] ?? null;
2188
+ }
2189
+ /**
2190
+ * Find the best matching logical planar face by normal and optional centroid proximity.
2191
+ */
2192
+ findPlanarFaceByNormal(targetNormal, nearPoint) {
2193
+ const planarFaces = this.planarFaces;
2194
+ if (planarFaces.length === 0)
2195
+ return null;
2196
+ const normalizedTarget = targetNormal.normalize();
2197
+ if (normalizedTarget.length() < 1e-12)
2198
+ return null;
2199
+ const alignmentEps = 1e-9;
2200
+ const distanceEps = 1e-9;
2201
+ let best = null;
2202
+ let bestAlignment = -Infinity;
2203
+ let bestDistance = Number.POSITIVE_INFINITY;
2204
+ let bestArea = -Infinity;
2205
+ for (const planarFace of planarFaces) {
2206
+ const alignment = planarFace.normal.dot(normalizedTarget);
2207
+ const distance = nearPoint ? planarFace.centroid.distanceTo(nearPoint) : 0;
2208
+ const betterAlignment = alignment > bestAlignment + alignmentEps;
2209
+ const similarAlignment = Math.abs(alignment - bestAlignment) <= alignmentEps;
2210
+ const betterDistance = nearPoint !== undefined && distance < bestDistance - distanceEps;
2211
+ const similarDistance = Math.abs(distance - bestDistance) <= distanceEps;
2212
+ const betterArea = planarFace.area > bestArea + distanceEps;
2213
+ if (best === null
2214
+ || betterAlignment
2215
+ || (similarAlignment && betterDistance)
2216
+ || (similarAlignment && (!nearPoint || similarDistance) && betterArea)) {
2217
+ best = planarFace;
2218
+ bestAlignment = alignment;
2219
+ bestDistance = distance;
2220
+ bestArea = planarFace.area;
2221
+ }
2222
+ }
2223
+ return best;
2224
+ }
2225
+ /**
2226
+ * Find the best matching logical planar surface wrapper by normal
2227
+ * similarity and optional point proximity.
2228
+ */
2229
+ findSurfaceByNormal(targetNormal, nearPoint) {
2230
+ const face = this.findPlanarFaceByNormal(targetNormal, nearPoint);
2231
+ if (!face)
2232
+ return null;
2233
+ return this.getSurface(face.index);
2234
+ }
1844
2235
  /**
1845
2236
  * Odd/even point containment test against a closed mesh.
1846
2237
  * Uses majority vote across multiple ray directions for robustness.
@@ -1853,30 +2244,14 @@ export class Mesh {
1853
2244
  * Find the coplanar + edge-connected face group containing a triangle.
1854
2245
  */
1855
2246
  findFaceByTriangleIndex(triangleIndex) {
1856
- if (!Number.isFinite(triangleIndex) || triangleIndex < 0)
1857
- return null;
1858
- ensureInit();
1859
- const r = wasm.mesh_find_face_group_by_triangle_index(this._vertexCount, this._buffer, Math.floor(triangleIndex));
1860
- if (!r || r.length < 6)
1861
- return null;
1862
- return {
1863
- centroid: new Point(r[0], r[1], r[2]),
1864
- normal: new Vec3(r[3], r[4], r[5]),
1865
- };
2247
+ return this.getPlanarFaceByTriangleIndex(triangleIndex);
1866
2248
  }
1867
2249
  /**
1868
2250
  * Find the best matching coplanar + edge-connected face group by normal
1869
2251
  * similarity and optional point proximity.
1870
2252
  */
1871
2253
  findFaceByNormal(targetNormal, nearPoint) {
1872
- ensureInit();
1873
- const r = wasm.mesh_find_face_group_by_normal(this._vertexCount, this._buffer, targetNormal.x, targetNormal.y, targetNormal.z, nearPoint?.x ?? 0, nearPoint?.y ?? 0, nearPoint?.z ?? 0, nearPoint !== undefined);
1874
- if (!r || r.length < 6)
1875
- return null;
1876
- return {
1877
- centroid: new Point(r[0], r[1], r[2]),
1878
- normal: new Vec3(r[3], r[4], r[5]),
1879
- };
2254
+ return this.findPlanarFaceByNormal(targetNormal, nearPoint);
1880
2255
  }
1881
2256
  // ── Export ──────────────────────────────────────────────────────
1882
2257
  /**