okgeometry-api 0.5.8 → 1.0.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/Mesh.d.ts +21 -3
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +114 -41
- package/dist/Mesh.js.map +1 -1
- package/dist/mesh-boolean.pool.d.ts +2 -2
- package/dist/mesh-boolean.pool.d.ts.map +1 -1
- package/dist/mesh-boolean.pool.js +8 -4
- package/dist/mesh-boolean.pool.js.map +1 -1
- package/dist/mesh-boolean.protocol.d.ts +5 -4
- package/dist/mesh-boolean.protocol.d.ts.map +1 -1
- package/dist/mesh-boolean.protocol.js.map +1 -1
- package/dist/mesh-boolean.worker.d.ts.map +1 -1
- package/dist/mesh-boolean.worker.js +7 -3
- package/dist/mesh-boolean.worker.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/package.json +9 -6
- package/src/Mesh.ts +219 -115
- package/src/mesh-boolean.pool.ts +51 -41
- package/src/mesh-boolean.protocol.ts +32 -31
- package/src/mesh-boolean.worker.ts +11 -7
- package/src/wasm-base64.ts +1 -1
- package/wasm/README.md +2 -0
- package/wasm/okgeometrycore.d.ts +81 -36
- package/wasm/okgeometrycore.js +145 -60
- package/wasm/okgeometrycore_bg.wasm +0 -0
- package/wasm/okgeometrycore_bg.wasm.d.ts +6 -3
- package/wasm/package.json +1 -1
package/dist/wasm-base64.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasm-base64.js","sourceRoot":"","sources":["../src/wasm-base64.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,MAAM,CAAC,MAAM,QAAQ,GAAG,
|
|
1
|
+
{"version":3,"file":"wasm-base64.js","sourceRoot":"","sources":["../src/wasm-base64.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,MAAM,CAAC,MAAM,QAAQ,GAAG,028yDAA028yD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okgeometry-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Geometry engine API for AEC applications — NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
"inline-wasm": "tsx scripts/inline-wasm.ts",
|
|
21
21
|
"build": "npm run inline-wasm && tsc",
|
|
22
22
|
"bench:boolean-manifold-baseline": "npm run build && tsx scripts/bench-boolean-manifold-baseline.ts",
|
|
23
|
-
"bench:boolean-
|
|
24
|
-
"bench:boolean-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
23
|
+
"bench:boolean-replacement-heavy": "npm run build && tsx scripts/bench-boolean-replacement-heavy.ts",
|
|
24
|
+
"bench:boolean-deterministic": "npm run build && tsx scripts/bench-boolean-deterministic.ts",
|
|
25
|
+
"bench:boolean-ab": "npm run build && tsx scripts/bench-boolean-ab.ts",
|
|
26
|
+
"bench:subtraction-bunny-parity": "npm run build && tsx scripts/bench-subtraction-bunny-parity.ts",
|
|
27
|
+
"bench:boolean-replacement": "npm run build && tsx scripts/gate-boolean-replacement.ts",
|
|
28
|
+
"gate:boolean-replacement": "npm run build && tsx scripts/gate-boolean-replacement.ts",
|
|
29
|
+
"gate:boolean-replacement-vs-manifold": "npm run bench:boolean-manifold-baseline && npm run gate:boolean-replacement",
|
|
27
30
|
"prepublishOnly": "npm run build"
|
|
28
31
|
},
|
|
29
32
|
"keywords": [
|
|
@@ -40,7 +43,7 @@
|
|
|
40
43
|
"license": "Proprietary License",
|
|
41
44
|
"repository": {
|
|
42
45
|
"type": "git",
|
|
43
|
-
"url": "https://
|
|
46
|
+
"url": "https://www.orkestra.online"
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"manifold-3d": "^3.3.2",
|
package/src/Mesh.ts
CHANGED
|
@@ -18,21 +18,20 @@ import {
|
|
|
18
18
|
runMeshBooleanInWorkerPool,
|
|
19
19
|
} from "./mesh-boolean.pool.js";
|
|
20
20
|
import { MeshBooleanExecutionError } from "./mesh-boolean.protocol.js";
|
|
21
|
-
import type {
|
|
22
|
-
MeshBooleanAsyncOptions,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import
|
|
21
|
+
import type {
|
|
22
|
+
MeshBooleanAsyncOptions,
|
|
23
|
+
MeshBooleanLimits,
|
|
24
|
+
MeshBooleanOptions,
|
|
25
|
+
} from "./mesh-boolean.protocol.js";
|
|
26
|
+
import * as wasm from "../wasm/okgeometrycore_bg.js";
|
|
27
|
+
import { mesh_topology_metrics } from "../wasm/okgeometrycore.js";
|
|
28
28
|
|
|
29
29
|
export { MeshBooleanExecutionError };
|
|
30
|
-
export type {
|
|
31
|
-
MeshBooleanAsyncOptions,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
MeshBooleanErrorCode,
|
|
30
|
+
export type {
|
|
31
|
+
MeshBooleanAsyncOptions,
|
|
32
|
+
MeshBooleanLimits,
|
|
33
|
+
MeshBooleanOptions,
|
|
34
|
+
MeshBooleanErrorCode,
|
|
36
35
|
MeshBooleanErrorPayload,
|
|
37
36
|
MeshBooleanProgressEvent,
|
|
38
37
|
} from "./mesh-boolean.protocol.js";
|
|
@@ -72,27 +71,33 @@ interface RawMeshBounds {
|
|
|
72
71
|
*
|
|
73
72
|
* Buffer format: [vertexCount, x1,y1,z1, ..., i1,i2,i3, ...]
|
|
74
73
|
*/
|
|
75
|
-
export class Mesh {
|
|
76
|
-
private _buffer: Float64Array;
|
|
77
|
-
private _vertexCount: number;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
private
|
|
89
|
-
private
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
export class Mesh {
|
|
75
|
+
private _buffer: Float64Array;
|
|
76
|
+
private _vertexCount: number;
|
|
77
|
+
private _trustedBooleanInput: boolean;
|
|
78
|
+
|
|
79
|
+
private static readonly DEFAULT_BOOLEAN_LIMITS: MeshBooleanLimits = {
|
|
80
|
+
maxInputFacesPerMesh: 120_000,
|
|
81
|
+
maxCombinedInputFaces: 180_000,
|
|
82
|
+
maxFaceProduct: 500_000_000,
|
|
83
|
+
};
|
|
84
|
+
private static readonly TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD = 20_000;
|
|
85
|
+
|
|
86
|
+
// Lazy caches
|
|
87
|
+
private _positionBuffer: Float32Array | null = null;
|
|
88
|
+
private _indexBuffer: Uint32Array | null = null;
|
|
89
|
+
private _vertices: Point[] | null = null;
|
|
90
|
+
private _faces: number[][] | null = null;
|
|
91
|
+
private _edgeVertexPairs: Array<[number, number]> | null = null;
|
|
92
|
+
private _topologyMetricsCache: { boundaryEdges: number; nonManifoldEdges: number } | null = null;
|
|
93
|
+
private _isClosedVolumeCache: boolean | null = null;
|
|
94
|
+
private _rawBoundsCache: RawMeshBounds | null | undefined = undefined;
|
|
95
|
+
|
|
96
|
+
private constructor(buffer: Float64Array, trustedBooleanInput = false) {
|
|
97
|
+
this._buffer = buffer;
|
|
98
|
+
this._vertexCount = buffer.length > 0 ? buffer[0] : 0;
|
|
99
|
+
this._trustedBooleanInput = trustedBooleanInput;
|
|
100
|
+
}
|
|
96
101
|
|
|
97
102
|
/**
|
|
98
103
|
* Configure default size for the shared boolean worker pool.
|
|
@@ -119,10 +124,14 @@ export class Mesh {
|
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
private static computeRawBounds(mesh: Mesh): RawMeshBounds | null {
|
|
123
|
-
if (mesh.
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
private static computeRawBounds(mesh: Mesh): RawMeshBounds | null {
|
|
128
|
+
if (mesh._rawBoundsCache !== undefined) return mesh._rawBoundsCache;
|
|
129
|
+
if (mesh._vertexCount <= 0) {
|
|
130
|
+
mesh._rawBoundsCache = null;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = mesh._buffer;
|
|
126
135
|
const limit = 1 + mesh._vertexCount * 3;
|
|
127
136
|
|
|
128
137
|
let minX = data[1];
|
|
@@ -144,8 +153,10 @@ export class Mesh {
|
|
|
144
153
|
if (z > maxZ) maxZ = z;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
const bounds = { minX, minY, minZ, maxX, maxY, maxZ };
|
|
157
|
+
mesh._rawBoundsCache = bounds;
|
|
158
|
+
return bounds;
|
|
159
|
+
}
|
|
149
160
|
|
|
150
161
|
private static boundsOverlap(a: RawMeshBounds | null, b: RawMeshBounds | null, eps = 1e-9): boolean {
|
|
151
162
|
if (!a || !b) return false;
|
|
@@ -171,13 +182,15 @@ export class Mesh {
|
|
|
171
182
|
return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
private static cloneMesh(mesh: Mesh): Mesh {
|
|
175
|
-
return Mesh.fromBuffer(new Float64Array(mesh._buffer)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
private static cloneMesh(mesh: Mesh): Mesh {
|
|
186
|
+
return Mesh.fromBuffer(new Float64Array(mesh._buffer), {
|
|
187
|
+
trustedBooleanInput: mesh._trustedBooleanInput,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private static emptyMesh(): Mesh {
|
|
192
|
+
return Mesh.fromTrustedBuffer(new Float64Array(0));
|
|
193
|
+
}
|
|
181
194
|
|
|
182
195
|
// ── GPU-ready buffers ──────────────────────────────────────────
|
|
183
196
|
|
|
@@ -217,10 +230,12 @@ export class Mesh {
|
|
|
217
230
|
return this._vertexCount;
|
|
218
231
|
}
|
|
219
232
|
|
|
220
|
-
/** Number of triangular faces in this mesh */
|
|
221
|
-
get faceCount(): number {
|
|
222
|
-
|
|
223
|
-
|
|
233
|
+
/** Number of triangular faces in this mesh */
|
|
234
|
+
get faceCount(): number {
|
|
235
|
+
const start = 1 + this._vertexCount * 3;
|
|
236
|
+
if (this._buffer.length <= start) return 0;
|
|
237
|
+
return Math.floor((this._buffer.length - start) / 3);
|
|
238
|
+
}
|
|
224
239
|
|
|
225
240
|
// ── High-level accessors (lazy) ────────────────────────────────
|
|
226
241
|
|
|
@@ -269,9 +284,20 @@ export class Mesh {
|
|
|
269
284
|
* @param buffer - Float64Array in mesh buffer format
|
|
270
285
|
* @returns New Mesh instance
|
|
271
286
|
*/
|
|
272
|
-
static fromBuffer(
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
static fromBuffer(
|
|
288
|
+
buffer: Float64Array,
|
|
289
|
+
options?: { trustedBooleanInput?: boolean },
|
|
290
|
+
): Mesh {
|
|
291
|
+
return new Mesh(buffer, options?.trustedBooleanInput ?? false);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private static fromTrustedBuffer(buffer: Float64Array): Mesh {
|
|
295
|
+
return new Mesh(buffer, true);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
get trustedBooleanInput(): boolean {
|
|
299
|
+
return this._trustedBooleanInput;
|
|
300
|
+
}
|
|
275
301
|
|
|
276
302
|
/**
|
|
277
303
|
* Build an axis-aligned rectangle on a plane basis from opposite corners.
|
|
@@ -554,11 +580,11 @@ export class Mesh {
|
|
|
554
580
|
* @param pts - Ordered boundary points defining a closed polygon (minimum 3)
|
|
555
581
|
* @returns New Mesh triangulating the polygon interior
|
|
556
582
|
*/
|
|
557
|
-
static patchFromPoints(pts: Point[]): Mesh {
|
|
558
|
-
ensureInit();
|
|
559
|
-
const coords = pointsToCoords(pts);
|
|
560
|
-
const buf = wasm.mesh_patch_from_points(coords);
|
|
561
|
-
return
|
|
583
|
+
static patchFromPoints(pts: Point[]): Mesh {
|
|
584
|
+
ensureInit();
|
|
585
|
+
const coords = pointsToCoords(pts);
|
|
586
|
+
const buf = wasm.mesh_patch_from_points(coords);
|
|
587
|
+
return Mesh.fromTrustedBuffer(buf);
|
|
562
588
|
}
|
|
563
589
|
|
|
564
590
|
/**
|
|
@@ -570,7 +596,7 @@ export class Mesh {
|
|
|
570
596
|
*/
|
|
571
597
|
static createBox(width: number, height: number, depth: number): Mesh {
|
|
572
598
|
ensureInit();
|
|
573
|
-
return
|
|
599
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_create_box(width, height, depth));
|
|
574
600
|
}
|
|
575
601
|
|
|
576
602
|
/**
|
|
@@ -582,7 +608,7 @@ export class Mesh {
|
|
|
582
608
|
*/
|
|
583
609
|
static createSphere(radius: number, segments: number, rings: number): Mesh {
|
|
584
610
|
ensureInit();
|
|
585
|
-
return
|
|
611
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_create_sphere(radius, segments, rings));
|
|
586
612
|
}
|
|
587
613
|
|
|
588
614
|
/**
|
|
@@ -594,7 +620,7 @@ export class Mesh {
|
|
|
594
620
|
*/
|
|
595
621
|
static createCylinder(radius: number, height: number, segments: number): Mesh {
|
|
596
622
|
ensureInit();
|
|
597
|
-
return
|
|
623
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_create_cylinder(radius, height, segments));
|
|
598
624
|
}
|
|
599
625
|
|
|
600
626
|
/**
|
|
@@ -606,7 +632,7 @@ export class Mesh {
|
|
|
606
632
|
*/
|
|
607
633
|
static createPrism(radius: number, height: number, sides: number): Mesh {
|
|
608
634
|
ensureInit();
|
|
609
|
-
return
|
|
635
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_create_prism(radius, height, sides));
|
|
610
636
|
}
|
|
611
637
|
|
|
612
638
|
/**
|
|
@@ -618,7 +644,7 @@ export class Mesh {
|
|
|
618
644
|
*/
|
|
619
645
|
static createCone(radius: number, height: number, segments: number): Mesh {
|
|
620
646
|
ensureInit();
|
|
621
|
-
return
|
|
647
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_create_cone(radius, height, segments));
|
|
622
648
|
}
|
|
623
649
|
|
|
624
650
|
/**
|
|
@@ -628,7 +654,7 @@ export class Mesh {
|
|
|
628
654
|
*/
|
|
629
655
|
static fromOBJ(objString: string): Mesh {
|
|
630
656
|
ensureInit();
|
|
631
|
-
return
|
|
657
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_import_obj(objString));
|
|
632
658
|
}
|
|
633
659
|
|
|
634
660
|
/**
|
|
@@ -656,7 +682,7 @@ export class Mesh {
|
|
|
656
682
|
data[off + 5] = c.normal?.z ?? 0;
|
|
657
683
|
data[off + 6] = c.radius;
|
|
658
684
|
}
|
|
659
|
-
return
|
|
685
|
+
return Mesh.fromTrustedBuffer(wasm.loft_circles(data, segments, caps));
|
|
660
686
|
}
|
|
661
687
|
|
|
662
688
|
/**
|
|
@@ -676,7 +702,7 @@ export class Mesh {
|
|
|
676
702
|
parts.push(p.x, p.y, p.z);
|
|
677
703
|
}
|
|
678
704
|
}
|
|
679
|
-
return
|
|
705
|
+
return Mesh.fromTrustedBuffer(wasm.loft_polylines(new Float64Array(parts), segments, caps));
|
|
680
706
|
}
|
|
681
707
|
|
|
682
708
|
/**
|
|
@@ -688,7 +714,7 @@ export class Mesh {
|
|
|
688
714
|
*/
|
|
689
715
|
static sweep(profilePoints: Point[], pathPoints: Point[], caps = false): Mesh {
|
|
690
716
|
ensureInit();
|
|
691
|
-
return
|
|
717
|
+
return Mesh.fromTrustedBuffer(wasm.sweep_polylines(pointsToCoords(profilePoints), pointsToCoords(pathPoints), caps));
|
|
692
718
|
}
|
|
693
719
|
|
|
694
720
|
/**
|
|
@@ -713,7 +739,7 @@ export class Mesh {
|
|
|
713
739
|
height,
|
|
714
740
|
closed,
|
|
715
741
|
);
|
|
716
|
-
return Mesh.
|
|
742
|
+
return Mesh.fromTrustedBuffer(buf);
|
|
717
743
|
}
|
|
718
744
|
|
|
719
745
|
/**
|
|
@@ -758,7 +784,7 @@ export class Mesh {
|
|
|
758
784
|
ensureInit();
|
|
759
785
|
const profileData = Mesh.encodeCurve(profile);
|
|
760
786
|
const pathData = Mesh.encodeCurve(path);
|
|
761
|
-
return
|
|
787
|
+
return Mesh.fromTrustedBuffer(wasm.sweep_curves(profileData, pathData, segments, segments, caps));
|
|
762
788
|
}
|
|
763
789
|
|
|
764
790
|
/** Encode a curve into the WASM format for sweep_curves. */
|
|
@@ -836,7 +862,7 @@ export class Mesh {
|
|
|
836
862
|
private static mergeMeshes(meshes: Mesh[]): Mesh {
|
|
837
863
|
ensureInit();
|
|
838
864
|
const packed = Mesh.packMeshes(meshes);
|
|
839
|
-
return Mesh.
|
|
865
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_merge(packed));
|
|
840
866
|
}
|
|
841
867
|
|
|
842
868
|
/**
|
|
@@ -928,7 +954,7 @@ export class Mesh {
|
|
|
928
954
|
*/
|
|
929
955
|
translate(offset: Vec3): Mesh {
|
|
930
956
|
ensureInit();
|
|
931
|
-
return
|
|
957
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_translate(this._vertexCount, this._buffer, offset.x, offset.y, offset.z));
|
|
932
958
|
}
|
|
933
959
|
|
|
934
960
|
/**
|
|
@@ -946,7 +972,7 @@ export class Mesh {
|
|
|
946
972
|
.rotate(dir, angleRadians)
|
|
947
973
|
.translate(new Vec3(c.x, c.y, c.z));
|
|
948
974
|
}
|
|
949
|
-
return
|
|
975
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_rotate(this._vertexCount, this._buffer, axis.x, axis.y, axis.z, angleRadians));
|
|
950
976
|
}
|
|
951
977
|
|
|
952
978
|
/**
|
|
@@ -956,7 +982,7 @@ export class Mesh {
|
|
|
956
982
|
*/
|
|
957
983
|
scale(factor: number): Mesh {
|
|
958
984
|
ensureInit();
|
|
959
|
-
return
|
|
985
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_scale(this._vertexCount, this._buffer, factor, factor, factor));
|
|
960
986
|
}
|
|
961
987
|
|
|
962
988
|
/**
|
|
@@ -968,7 +994,7 @@ export class Mesh {
|
|
|
968
994
|
*/
|
|
969
995
|
scaleXYZ(sx: number, sy: number, sz: number): Mesh {
|
|
970
996
|
ensureInit();
|
|
971
|
-
return
|
|
997
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_scale(this._vertexCount, this._buffer, sx, sy, sz));
|
|
972
998
|
}
|
|
973
999
|
|
|
974
1000
|
private runBoolean(
|
|
@@ -1025,15 +1051,24 @@ export class Mesh {
|
|
|
1025
1051
|
if (!Number.isFinite(vertexCount) || vertexCount < 0) {
|
|
1026
1052
|
throw new Error(`Boolean ${operation} failed and returned a corrupt mesh buffer.`);
|
|
1027
1053
|
}
|
|
1028
|
-
return Mesh.
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
operation: "union" | "subtraction" | "intersection",
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1054
|
+
return Mesh.fromTrustedBuffer(result);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
static encodeBooleanOperationToken(
|
|
1058
|
+
operation: "union" | "subtraction" | "intersection",
|
|
1059
|
+
a: Mesh,
|
|
1060
|
+
b: Mesh,
|
|
1061
|
+
options?: MeshBooleanOptions,
|
|
1062
|
+
): string {
|
|
1063
|
+
const tokens: string[] = [operation];
|
|
1064
|
+
if (a._trustedBooleanInput && b._trustedBooleanInput) {
|
|
1065
|
+
tokens.push("trustedInput");
|
|
1066
|
+
}
|
|
1067
|
+
if (options?.debugForceFaceID) {
|
|
1068
|
+
tokens.push("forceFaceID");
|
|
1069
|
+
}
|
|
1070
|
+
return tokens.join("@");
|
|
1071
|
+
}
|
|
1037
1072
|
|
|
1038
1073
|
// ── Booleans ───────────────────────────────────────────────────
|
|
1039
1074
|
|
|
@@ -1043,9 +1078,9 @@ export class Mesh {
|
|
|
1043
1078
|
* @param options - Optional safety overrides
|
|
1044
1079
|
* @returns New mesh containing volume of both inputs
|
|
1045
1080
|
*/
|
|
1046
|
-
union(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1047
|
-
ensureInit();
|
|
1048
|
-
const operationToken = Mesh.
|
|
1081
|
+
union(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1082
|
+
ensureInit();
|
|
1083
|
+
const operationToken = Mesh.encodeBooleanOperationToken("union", this, other, options);
|
|
1049
1084
|
return this.runBoolean(
|
|
1050
1085
|
other,
|
|
1051
1086
|
"union",
|
|
@@ -1066,9 +1101,9 @@ export class Mesh {
|
|
|
1066
1101
|
* @param options - Optional safety overrides
|
|
1067
1102
|
* @returns New mesh with other's volume removed from this
|
|
1068
1103
|
*/
|
|
1069
|
-
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1070
|
-
ensureInit();
|
|
1071
|
-
const operationToken = Mesh.
|
|
1104
|
+
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1105
|
+
ensureInit();
|
|
1106
|
+
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
|
|
1072
1107
|
return this.runBoolean(
|
|
1073
1108
|
other,
|
|
1074
1109
|
"subtraction",
|
|
@@ -1089,9 +1124,9 @@ export class Mesh {
|
|
|
1089
1124
|
* @param options - Optional safety overrides
|
|
1090
1125
|
* @returns New mesh containing only the overlapping volume
|
|
1091
1126
|
*/
|
|
1092
|
-
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1093
|
-
ensureInit();
|
|
1094
|
-
const operationToken = Mesh.
|
|
1127
|
+
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1128
|
+
ensureInit();
|
|
1129
|
+
const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
|
|
1095
1130
|
return this.runBoolean(
|
|
1096
1131
|
other,
|
|
1097
1132
|
"intersection",
|
|
@@ -1111,8 +1146,15 @@ export class Mesh {
|
|
|
1111
1146
|
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
1112
1147
|
*/
|
|
1113
1148
|
async unionAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
1114
|
-
const result = await runMeshBooleanInWorkerPool(
|
|
1115
|
-
|
|
1149
|
+
const result = await runMeshBooleanInWorkerPool(
|
|
1150
|
+
"union",
|
|
1151
|
+
this._buffer,
|
|
1152
|
+
other._buffer,
|
|
1153
|
+
this._trustedBooleanInput,
|
|
1154
|
+
other._trustedBooleanInput,
|
|
1155
|
+
options,
|
|
1156
|
+
);
|
|
1157
|
+
return Mesh.fromTrustedBuffer(result);
|
|
1116
1158
|
}
|
|
1117
1159
|
|
|
1118
1160
|
/**
|
|
@@ -1120,8 +1162,15 @@ export class Mesh {
|
|
|
1120
1162
|
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
1121
1163
|
*/
|
|
1122
1164
|
async subtractAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
1123
|
-
const result = await runMeshBooleanInWorkerPool(
|
|
1124
|
-
|
|
1165
|
+
const result = await runMeshBooleanInWorkerPool(
|
|
1166
|
+
"subtraction",
|
|
1167
|
+
this._buffer,
|
|
1168
|
+
other._buffer,
|
|
1169
|
+
this._trustedBooleanInput,
|
|
1170
|
+
other._trustedBooleanInput,
|
|
1171
|
+
options,
|
|
1172
|
+
);
|
|
1173
|
+
return Mesh.fromTrustedBuffer(result);
|
|
1125
1174
|
}
|
|
1126
1175
|
|
|
1127
1176
|
/**
|
|
@@ -1129,8 +1178,15 @@ export class Mesh {
|
|
|
1129
1178
|
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
1130
1179
|
*/
|
|
1131
1180
|
async intersectAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
1132
|
-
const result = await runMeshBooleanInWorkerPool(
|
|
1133
|
-
|
|
1181
|
+
const result = await runMeshBooleanInWorkerPool(
|
|
1182
|
+
"intersection",
|
|
1183
|
+
this._buffer,
|
|
1184
|
+
other._buffer,
|
|
1185
|
+
this._trustedBooleanInput,
|
|
1186
|
+
other._trustedBooleanInput,
|
|
1187
|
+
options,
|
|
1188
|
+
);
|
|
1189
|
+
return Mesh.fromTrustedBuffer(result);
|
|
1134
1190
|
}
|
|
1135
1191
|
|
|
1136
1192
|
// ── Intersection queries ───────────────────────────────────────
|
|
@@ -1176,7 +1232,7 @@ export class Mesh {
|
|
|
1176
1232
|
*/
|
|
1177
1233
|
applyMatrix(matrix: number[]): Mesh {
|
|
1178
1234
|
ensureInit();
|
|
1179
|
-
return
|
|
1235
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_apply_matrix(this._vertexCount, this._buffer, new Float64Array(matrix)));
|
|
1180
1236
|
}
|
|
1181
1237
|
|
|
1182
1238
|
/**
|
|
@@ -1408,21 +1464,68 @@ export class Mesh {
|
|
|
1408
1464
|
extrudeFace(faceIndex: number, distance: number): Mesh {
|
|
1409
1465
|
ensureInit();
|
|
1410
1466
|
if (!Number.isFinite(faceIndex) || faceIndex < 0) {
|
|
1411
|
-
return Mesh.fromBuffer(new Float64Array(this._buffer)
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1467
|
+
return Mesh.fromBuffer(new Float64Array(this._buffer), {
|
|
1468
|
+
trustedBooleanInput: this._trustedBooleanInput,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
return Mesh.fromTrustedBuffer(
|
|
1472
|
+
wasm.mesh_extrude_face(this._vertexCount, this._buffer, Math.floor(faceIndex), distance),
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Check if this triangulated mesh represents a closed volume.
|
|
1478
|
+
* Returns true when no welded topological boundary edges are found.
|
|
1479
|
+
*/
|
|
1480
|
+
isClosedVolume(): boolean {
|
|
1481
|
+
if (this._isClosedVolumeCache !== null) {
|
|
1482
|
+
return this._isClosedVolumeCache;
|
|
1483
|
+
}
|
|
1484
|
+
if (this._topologyMetricsCache) {
|
|
1485
|
+
const closedFromMetrics = this._topologyMetricsCache.boundaryEdges === 0;
|
|
1486
|
+
this._isClosedVolumeCache = closedFromMetrics;
|
|
1487
|
+
return closedFromMetrics;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
ensureInit();
|
|
1491
|
+
if (this.faceCount >= Mesh.TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD) {
|
|
1492
|
+
const metrics = this.topologyMetrics();
|
|
1493
|
+
const closedFromMetrics = metrics.boundaryEdges === 0;
|
|
1494
|
+
this._isClosedVolumeCache = closedFromMetrics;
|
|
1495
|
+
return closedFromMetrics;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const closed = wasm.mesh_is_closed_volume(this._vertexCount, this._buffer);
|
|
1499
|
+
this._isClosedVolumeCache = closed;
|
|
1500
|
+
return closed;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Return welded-edge topology metrics for this triangulated mesh.
|
|
1505
|
+
* `boundaryEdges`: edges referenced by exactly one triangle.
|
|
1506
|
+
* `nonManifoldEdges`: edges referenced by more than two triangles.
|
|
1507
|
+
*/
|
|
1508
|
+
topologyMetrics(): { boundaryEdges: number; nonManifoldEdges: number } {
|
|
1509
|
+
if (this._topologyMetricsCache) {
|
|
1510
|
+
return {
|
|
1511
|
+
boundaryEdges: this._topologyMetricsCache.boundaryEdges,
|
|
1512
|
+
nonManifoldEdges: this._topologyMetricsCache.nonManifoldEdges,
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
ensureInit();
|
|
1517
|
+
const metrics = mesh_topology_metrics(this._vertexCount, this._buffer);
|
|
1518
|
+
const boundaryEdges = Math.floor(metrics[0] ?? 0);
|
|
1519
|
+
const nonManifoldEdges = Math.floor(metrics[1] ?? 0);
|
|
1520
|
+
this._topologyMetricsCache = { boundaryEdges, nonManifoldEdges };
|
|
1521
|
+
if (this._isClosedVolumeCache === null) {
|
|
1522
|
+
this._isClosedVolumeCache = boundaryEdges === 0;
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
boundaryEdges,
|
|
1526
|
+
nonManifoldEdges,
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1426
1529
|
|
|
1427
1530
|
/**
|
|
1428
1531
|
* Odd/even point containment test against a closed mesh.
|
|
@@ -1489,3 +1592,4 @@ export class Mesh {
|
|
|
1489
1592
|
|
|
1490
1593
|
|
|
1491
1594
|
|
|
1595
|
+
|