okgeometry-api 1.1.22 → 1.2.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 +52 -0
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +260 -0
- package/dist/Mesh.js.map +1 -1
- package/dist/NurbsCurve.d.ts.map +1 -1
- package/dist/NurbsCurve.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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/dist/wasm-bindings.d.ts +46 -7
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +86 -7
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +1 -5
- package/src/Mesh.ts +872 -483
- package/src/NurbsCurve.ts +43 -44
- package/src/index.ts +4 -0
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +18 -7
- package/src/wasm-bindings.js +90 -7
package/src/Mesh.ts
CHANGED
|
@@ -17,24 +17,24 @@ import {
|
|
|
17
17
|
disposeMeshBooleanWorkerPools,
|
|
18
18
|
runMeshBooleanInWorkerPool,
|
|
19
19
|
} from "./mesh-boolean.pool.js";
|
|
20
|
-
import { MeshBooleanExecutionError } from "./mesh-boolean.protocol.js";
|
|
21
|
-
import type {
|
|
22
|
-
MeshBooleanAsyncOptions,
|
|
23
|
-
MeshBooleanLimits,
|
|
24
|
-
MeshBooleanOptions,
|
|
25
|
-
} from "./mesh-boolean.protocol.js";
|
|
26
|
-
import * as wasm from "./wasm-bindings.js";
|
|
27
|
-
import { mesh_topology_metrics, mesh_topology_metrics_raw } from "./wasm-bindings.js";
|
|
20
|
+
import { MeshBooleanExecutionError } from "./mesh-boolean.protocol.js";
|
|
21
|
+
import type {
|
|
22
|
+
MeshBooleanAsyncOptions,
|
|
23
|
+
MeshBooleanLimits,
|
|
24
|
+
MeshBooleanOptions,
|
|
25
|
+
} from "./mesh-boolean.protocol.js";
|
|
26
|
+
import * as wasm from "./wasm-bindings.js";
|
|
27
|
+
import { mesh_topology_metrics, mesh_topology_metrics_raw } from "./wasm-bindings.js";
|
|
28
28
|
|
|
29
29
|
export { MeshBooleanExecutionError };
|
|
30
|
-
export type {
|
|
31
|
-
MeshBooleanAsyncOptions,
|
|
32
|
-
MeshBooleanLimits,
|
|
33
|
-
MeshBooleanOptions,
|
|
34
|
-
MeshBooleanErrorCode,
|
|
35
|
-
MeshBooleanErrorPayload,
|
|
36
|
-
MeshBooleanProgressEvent,
|
|
37
|
-
} from "./mesh-boolean.protocol.js";
|
|
30
|
+
export type {
|
|
31
|
+
MeshBooleanAsyncOptions,
|
|
32
|
+
MeshBooleanLimits,
|
|
33
|
+
MeshBooleanOptions,
|
|
34
|
+
MeshBooleanErrorCode,
|
|
35
|
+
MeshBooleanErrorPayload,
|
|
36
|
+
MeshBooleanProgressEvent,
|
|
37
|
+
} from "./mesh-boolean.protocol.js";
|
|
38
38
|
|
|
39
39
|
export interface PlanarRectangle {
|
|
40
40
|
corners: [Point, Point, Point, Point];
|
|
@@ -113,86 +113,117 @@ export interface MeshBooleanDebugOptions {
|
|
|
113
113
|
probes?: MeshBooleanDebugProbeId[];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export interface MeshBooleanDebugReport {
|
|
117
|
-
operation: MeshBooleanOperation;
|
|
118
|
-
inputA: MeshDebugSummary;
|
|
119
|
-
inputB: MeshDebugSummary;
|
|
120
|
-
result: Mesh;
|
|
121
|
-
resultSummary: MeshDebugSummary;
|
|
122
|
-
probes: MeshBooleanDebugProbe[];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface MeshBooleanDebugProbeSummary {
|
|
126
|
-
label: string;
|
|
127
|
-
origin: { x: number; y: number; z: number };
|
|
128
|
-
direction: { x: number; y: number; z: number };
|
|
129
|
-
inputAFirst: {
|
|
130
|
-
point: { x: number; y: number; z: number };
|
|
131
|
-
normal: { x: number; y: number; z: number };
|
|
132
|
-
faceIndex: number;
|
|
133
|
-
distance: number;
|
|
134
|
-
} | null;
|
|
135
|
-
inputBFirst: {
|
|
136
|
-
point: { x: number; y: number; z: number };
|
|
137
|
-
normal: { x: number; y: number; z: number };
|
|
138
|
-
faceIndex: number;
|
|
139
|
-
distance: number;
|
|
140
|
-
} | null;
|
|
141
|
-
resultFirst: {
|
|
142
|
-
point: { x: number; y: number; z: number };
|
|
143
|
-
normal: { x: number; y: number; z: number };
|
|
144
|
-
faceIndex: number;
|
|
145
|
-
distance: number;
|
|
146
|
-
} | null;
|
|
147
|
-
inputAHits: number;
|
|
148
|
-
inputBHits: number;
|
|
149
|
-
resultHits: number;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface MeshBooleanReproOperand {
|
|
153
|
-
trustedBooleanInput: boolean;
|
|
154
|
-
summary: MeshDebugSummary;
|
|
155
|
-
buffer: number[];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export interface MeshBooleanReproResult {
|
|
159
|
-
summary: MeshDebugSummary;
|
|
160
|
-
buffer: number[];
|
|
161
|
-
probeSummary?: MeshBooleanDebugProbeSummary[];
|
|
116
|
+
export interface MeshBooleanDebugReport {
|
|
117
|
+
operation: MeshBooleanOperation;
|
|
118
|
+
inputA: MeshDebugSummary;
|
|
119
|
+
inputB: MeshDebugSummary;
|
|
120
|
+
result: Mesh;
|
|
121
|
+
resultSummary: MeshDebugSummary;
|
|
122
|
+
probes: MeshBooleanDebugProbe[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface MeshBooleanDebugProbeSummary {
|
|
126
|
+
label: string;
|
|
127
|
+
origin: { x: number; y: number; z: number };
|
|
128
|
+
direction: { x: number; y: number; z: number };
|
|
129
|
+
inputAFirst: {
|
|
130
|
+
point: { x: number; y: number; z: number };
|
|
131
|
+
normal: { x: number; y: number; z: number };
|
|
132
|
+
faceIndex: number;
|
|
133
|
+
distance: number;
|
|
134
|
+
} | null;
|
|
135
|
+
inputBFirst: {
|
|
136
|
+
point: { x: number; y: number; z: number };
|
|
137
|
+
normal: { x: number; y: number; z: number };
|
|
138
|
+
faceIndex: number;
|
|
139
|
+
distance: number;
|
|
140
|
+
} | null;
|
|
141
|
+
resultFirst: {
|
|
142
|
+
point: { x: number; y: number; z: number };
|
|
143
|
+
normal: { x: number; y: number; z: number };
|
|
144
|
+
faceIndex: number;
|
|
145
|
+
distance: number;
|
|
146
|
+
} | null;
|
|
147
|
+
inputAHits: number;
|
|
148
|
+
inputBHits: number;
|
|
149
|
+
resultHits: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface MeshBooleanReproOperand {
|
|
153
|
+
trustedBooleanInput: boolean;
|
|
154
|
+
summary: MeshDebugSummary;
|
|
155
|
+
buffer: number[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface MeshBooleanReproResult {
|
|
159
|
+
summary: MeshDebugSummary;
|
|
160
|
+
buffer: number[];
|
|
161
|
+
probeSummary?: MeshBooleanDebugProbeSummary[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface MeshBooleanReproError {
|
|
165
|
+
name: string;
|
|
166
|
+
message: string;
|
|
167
|
+
stack?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface MeshBooleanReproPayload {
|
|
171
|
+
version: 1;
|
|
172
|
+
operation: MeshBooleanOperation;
|
|
173
|
+
options: MeshBooleanOptions | null;
|
|
174
|
+
inputA: MeshBooleanReproOperand;
|
|
175
|
+
inputB: MeshBooleanReproOperand;
|
|
176
|
+
result?: MeshBooleanReproResult;
|
|
177
|
+
error?: MeshBooleanReproError;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface MeshBooleanReproOptions {
|
|
181
|
+
includeResult?: boolean;
|
|
182
|
+
debugOptions?: MeshBooleanDebugOptions;
|
|
162
183
|
}
|
|
163
184
|
|
|
164
|
-
export interface
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
export interface MeshSplitResult {
|
|
186
|
+
outside: Mesh[];
|
|
187
|
+
inside: Mesh[];
|
|
188
|
+
all: Mesh[];
|
|
189
|
+
classification?: "volume" | "surface";
|
|
190
|
+
negative?: Mesh[];
|
|
191
|
+
positive?: Mesh[];
|
|
168
192
|
}
|
|
169
193
|
|
|
170
|
-
export interface
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
options: MeshBooleanOptions | null;
|
|
174
|
-
inputA: MeshBooleanReproOperand;
|
|
175
|
-
inputB: MeshBooleanReproOperand;
|
|
176
|
-
result?: MeshBooleanReproResult;
|
|
177
|
-
error?: MeshBooleanReproError;
|
|
194
|
+
export interface MeshPlaneSplitResult extends MeshSplitResult {
|
|
195
|
+
negative: Mesh[];
|
|
196
|
+
positive: Mesh[];
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
export interface
|
|
181
|
-
|
|
182
|
-
debugOptions?: MeshBooleanDebugOptions;
|
|
199
|
+
export interface MeshPlaneSplitOptions extends MeshBooleanOptions {
|
|
200
|
+
planePaddingScale?: number;
|
|
183
201
|
}
|
|
184
202
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
export interface MeshCurveSplitOptions extends MeshBooleanOptions {
|
|
204
|
+
/**
|
|
205
|
+
* Explicit cutter extrusion direction. If omitted, a through-cut is generated
|
|
206
|
+
* automatically along the detected curve normal across the host mesh.
|
|
207
|
+
*/
|
|
208
|
+
direction?: Vec3;
|
|
209
|
+
/**
|
|
210
|
+
* Tessellation density for curved closed profiles such as circles, polycurves,
|
|
211
|
+
* and NURBS curves.
|
|
212
|
+
*/
|
|
213
|
+
curveSegments?: number;
|
|
189
214
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
|
|
216
|
+
function shouldFallbackFromWorkerFailure(error: unknown): boolean {
|
|
217
|
+
if (!error || typeof error !== "object") return false;
|
|
218
|
+
const code = "code" in error ? error.code : undefined;
|
|
219
|
+
return code === "worker_unavailable" || code === "worker_crashed" || code === "worker_protocol";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Buffer-backed triangle mesh with GPU-ready accessors.
|
|
224
|
+
* All geometry lives in a Float64Array from WASM.
|
|
225
|
+
*
|
|
226
|
+
* Buffer format: [vertexCount, x1,y1,z1, ..., i1,i2,i3, ...]
|
|
196
227
|
*/
|
|
197
228
|
export class Mesh {
|
|
198
229
|
private _buffer: Float64Array;
|
|
@@ -549,11 +580,11 @@ export class Mesh {
|
|
|
549
580
|
};
|
|
550
581
|
}
|
|
551
582
|
|
|
552
|
-
private static summarizeDebugProbe(probe: MeshBooleanDebugProbe): MeshBooleanDebugProbeSummary {
|
|
553
|
-
return {
|
|
554
|
-
label: probe.label,
|
|
555
|
-
origin: {
|
|
556
|
-
x: Mesh.roundDebugScalar(probe.origin.x),
|
|
583
|
+
private static summarizeDebugProbe(probe: MeshBooleanDebugProbe): MeshBooleanDebugProbeSummary {
|
|
584
|
+
return {
|
|
585
|
+
label: probe.label,
|
|
586
|
+
origin: {
|
|
587
|
+
x: Mesh.roundDebugScalar(probe.origin.x),
|
|
557
588
|
y: Mesh.roundDebugScalar(probe.origin.y),
|
|
558
589
|
z: Mesh.roundDebugScalar(probe.origin.z),
|
|
559
590
|
},
|
|
@@ -567,84 +598,278 @@ export class Mesh {
|
|
|
567
598
|
resultFirst: Mesh.summarizeDebugHit(probe.resultHits[0]),
|
|
568
599
|
inputAHits: probe.inputAHits.length,
|
|
569
600
|
inputBHits: probe.inputBHits.length,
|
|
570
|
-
resultHits: probe.resultHits.length,
|
|
571
|
-
};
|
|
601
|
+
resultHits: probe.resultHits.length,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private static serializeBooleanOptions(options?: MeshBooleanOptions): MeshBooleanOptions | null {
|
|
606
|
+
return options == null ? null : { ...options };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private static serializeBooleanError(error: unknown): MeshBooleanReproError {
|
|
610
|
+
if (error instanceof Error) {
|
|
611
|
+
return {
|
|
612
|
+
name: error.name,
|
|
613
|
+
message: error.message,
|
|
614
|
+
stack: error.stack,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
name: typeof error,
|
|
619
|
+
message: typeof error === "string" ? error : JSON.stringify(error),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private static exportBooleanReproOperand(mesh: Mesh): MeshBooleanReproOperand {
|
|
624
|
+
return {
|
|
625
|
+
trustedBooleanInput: mesh._trustedBooleanInput,
|
|
626
|
+
summary: mesh.debugSummary(),
|
|
627
|
+
buffer: Array.from(mesh._buffer),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private static createBooleanReproPayload(
|
|
632
|
+
inputA: Mesh,
|
|
633
|
+
inputB: Mesh,
|
|
634
|
+
operation: MeshBooleanOperation,
|
|
635
|
+
options?: MeshBooleanOptions,
|
|
636
|
+
result?: Mesh,
|
|
637
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
638
|
+
error?: unknown,
|
|
639
|
+
): MeshBooleanReproPayload {
|
|
640
|
+
const payload: MeshBooleanReproPayload = {
|
|
641
|
+
version: 1,
|
|
642
|
+
operation,
|
|
643
|
+
options: Mesh.serializeBooleanOptions(options),
|
|
644
|
+
inputA: Mesh.exportBooleanReproOperand(inputA),
|
|
645
|
+
inputB: Mesh.exportBooleanReproOperand(inputB),
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
if (result) {
|
|
649
|
+
payload.result = {
|
|
650
|
+
summary: result.debugSummary(),
|
|
651
|
+
buffer: Array.from(result._buffer),
|
|
652
|
+
};
|
|
653
|
+
if (debugOptions) {
|
|
654
|
+
try {
|
|
655
|
+
const report = Mesh.createBooleanDebugReport(inputA, inputB, result, operation, debugOptions);
|
|
656
|
+
payload.result.probeSummary = report.probes.map((probe) => Mesh.summarizeDebugProbe(probe));
|
|
657
|
+
} catch {
|
|
658
|
+
// Keep the repro payload usable even when extra debug probes fail.
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (error !== undefined) {
|
|
664
|
+
payload.error = Mesh.serializeBooleanError(error);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return payload;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private static cloneMesh(mesh: Mesh): Mesh {
|
|
671
|
+
return Mesh.fromBuffer(new Float64Array(mesh._buffer), {
|
|
672
|
+
trustedBooleanInput: mesh._trustedBooleanInput,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private static emptyMesh(): Mesh {
|
|
677
|
+
return Mesh.fromTrustedBuffer(new Float64Array(0));
|
|
572
678
|
}
|
|
573
679
|
|
|
574
|
-
private static
|
|
575
|
-
|
|
680
|
+
private static createSplitResult(
|
|
681
|
+
outside: Mesh[],
|
|
682
|
+
inside: Mesh[],
|
|
683
|
+
classification: "volume" | "surface" = "volume",
|
|
684
|
+
): MeshSplitResult {
|
|
685
|
+
const result: MeshSplitResult = {
|
|
686
|
+
outside,
|
|
687
|
+
inside,
|
|
688
|
+
all: outside.concat(inside),
|
|
689
|
+
classification,
|
|
690
|
+
};
|
|
691
|
+
if (classification === "surface") {
|
|
692
|
+
result.negative = outside;
|
|
693
|
+
result.positive = inside;
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
576
696
|
}
|
|
577
697
|
|
|
578
|
-
private static
|
|
579
|
-
|
|
580
|
-
return {
|
|
581
|
-
name: error.name,
|
|
582
|
-
message: error.message,
|
|
583
|
-
stack: error.stack,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
return {
|
|
587
|
-
name: typeof error,
|
|
588
|
-
message: typeof error === "string" ? error : JSON.stringify(error),
|
|
589
|
-
};
|
|
698
|
+
private static createSurfaceSplitResult(negative: Mesh[], positive: Mesh[]): MeshSplitResult {
|
|
699
|
+
return Mesh.createSplitResult(negative, positive, "surface");
|
|
590
700
|
}
|
|
591
701
|
|
|
592
|
-
private static
|
|
702
|
+
private static createPlaneSplitResult(
|
|
703
|
+
negative: Mesh[],
|
|
704
|
+
positive: Mesh[],
|
|
705
|
+
classification: "volume" | "surface" = "surface",
|
|
706
|
+
): MeshPlaneSplitResult {
|
|
593
707
|
return {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
708
|
+
...Mesh.createSplitResult(negative, positive, classification),
|
|
709
|
+
negative,
|
|
710
|
+
positive,
|
|
597
711
|
};
|
|
598
712
|
}
|
|
599
713
|
|
|
600
|
-
private static
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
): MeshBooleanReproPayload {
|
|
609
|
-
const payload: MeshBooleanReproPayload = {
|
|
610
|
-
version: 1,
|
|
611
|
-
operation,
|
|
612
|
-
options: Mesh.serializeBooleanOptions(options),
|
|
613
|
-
inputA: Mesh.exportBooleanReproOperand(inputA),
|
|
614
|
-
inputB: Mesh.exportBooleanReproOperand(inputB),
|
|
615
|
-
};
|
|
714
|
+
private static parseSplitResultBuffer(buffer: Float64Array): MeshSplitResult {
|
|
715
|
+
if (buffer.length < 2) {
|
|
716
|
+
throw new Error("Mesh split failed and returned a corrupt split buffer.");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const outsideCount = Math.max(0, Math.floor(buffer[0] ?? 0));
|
|
720
|
+
const insideCount = Math.max(0, Math.floor(buffer[1] ?? 0));
|
|
721
|
+
let offset = 2;
|
|
616
722
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
try {
|
|
624
|
-
const report = Mesh.createBooleanDebugReport(inputA, inputB, result, operation, debugOptions);
|
|
625
|
-
payload.result.probeSummary = report.probes.map((probe) => Mesh.summarizeDebugProbe(probe));
|
|
626
|
-
} catch {
|
|
627
|
-
// Keep the repro payload usable even when extra debug probes fail.
|
|
723
|
+
const decodeGroup = (count: number): Mesh[] => {
|
|
724
|
+
const meshes: Mesh[] = [];
|
|
725
|
+
for (let i = 0; i < count; i++) {
|
|
726
|
+
const rawLen = Math.floor(buffer[offset++] ?? -1);
|
|
727
|
+
if (rawLen < 0 || offset + rawLen > buffer.length) {
|
|
728
|
+
throw new Error("Mesh split failed and returned an invalid component buffer.");
|
|
628
729
|
}
|
|
730
|
+
meshes.push(Mesh.fromTrustedBuffer(buffer.slice(offset, offset + rawLen)));
|
|
731
|
+
offset += rawLen;
|
|
629
732
|
}
|
|
733
|
+
return meshes;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const outside = decodeGroup(outsideCount);
|
|
737
|
+
const inside = decodeGroup(insideCount);
|
|
738
|
+
if (offset !== buffer.length) {
|
|
739
|
+
throw new Error("Mesh split failed and returned trailing buffer data.");
|
|
630
740
|
}
|
|
741
|
+
return Mesh.createSplitResult(outside, inside);
|
|
742
|
+
}
|
|
631
743
|
|
|
632
|
-
|
|
633
|
-
|
|
744
|
+
private static orthonormalPlaneFrame(plane: Plane): { uAxis: Vec3; vAxis: Vec3; normal: Vec3 } {
|
|
745
|
+
const normal = plane.normal.normalize();
|
|
746
|
+
let uAxis = plane.getXAxis().normalize();
|
|
747
|
+
if (uAxis.length() < 1e-9) {
|
|
748
|
+
uAxis = Math.abs(normal.x) > 0.9 ? Vec3.Y : Vec3.X;
|
|
749
|
+
uAxis = uAxis.sub(normal.scale(uAxis.dot(normal))).normalize();
|
|
634
750
|
}
|
|
751
|
+
const vAxis = normal.cross(uAxis).normalize();
|
|
752
|
+
return { uAxis, vAxis, normal };
|
|
753
|
+
}
|
|
635
754
|
|
|
636
|
-
|
|
755
|
+
private static createOrientedBox(
|
|
756
|
+
center: Point,
|
|
757
|
+
uAxis: Vec3,
|
|
758
|
+
vAxis: Vec3,
|
|
759
|
+
wAxis: Vec3,
|
|
760
|
+
halfU: number,
|
|
761
|
+
halfV: number,
|
|
762
|
+
halfW: number,
|
|
763
|
+
): Mesh {
|
|
764
|
+
const axes = [uAxis.normalize(), vAxis.normalize(), wAxis.normalize()] as const;
|
|
765
|
+
const [u, v, w] = axes;
|
|
766
|
+
const corners = [
|
|
767
|
+
center.add(u.scale(-halfU)).add(v.scale(-halfV)).add(w.scale(-halfW)),
|
|
768
|
+
center.add(u.scale(halfU)).add(v.scale(-halfV)).add(w.scale(-halfW)),
|
|
769
|
+
center.add(u.scale(halfU)).add(v.scale(halfV)).add(w.scale(-halfW)),
|
|
770
|
+
center.add(u.scale(-halfU)).add(v.scale(halfV)).add(w.scale(-halfW)),
|
|
771
|
+
center.add(u.scale(-halfU)).add(v.scale(-halfV)).add(w.scale(halfW)),
|
|
772
|
+
center.add(u.scale(halfU)).add(v.scale(-halfV)).add(w.scale(halfW)),
|
|
773
|
+
center.add(u.scale(halfU)).add(v.scale(halfV)).add(w.scale(halfW)),
|
|
774
|
+
center.add(u.scale(-halfU)).add(v.scale(halfV)).add(w.scale(halfW)),
|
|
775
|
+
];
|
|
776
|
+
const indices = [
|
|
777
|
+
0, 2, 1, 0, 3, 2,
|
|
778
|
+
4, 5, 6, 4, 6, 7,
|
|
779
|
+
0, 1, 5, 0, 5, 4,
|
|
780
|
+
3, 7, 6, 3, 6, 2,
|
|
781
|
+
0, 4, 7, 0, 7, 3,
|
|
782
|
+
1, 2, 6, 1, 6, 5,
|
|
783
|
+
];
|
|
784
|
+
const raw = new Float64Array(1 + corners.length * 3 + indices.length);
|
|
785
|
+
raw[0] = corners.length;
|
|
786
|
+
let offset = 1;
|
|
787
|
+
for (const corner of corners) {
|
|
788
|
+
raw[offset++] = corner.x;
|
|
789
|
+
raw[offset++] = corner.y;
|
|
790
|
+
raw[offset++] = corner.z;
|
|
791
|
+
}
|
|
792
|
+
for (const index of indices) {
|
|
793
|
+
raw[offset++] = index;
|
|
794
|
+
}
|
|
795
|
+
return Mesh.fromBuffer(raw, { trustedBooleanInput: true });
|
|
637
796
|
}
|
|
638
797
|
|
|
639
|
-
private static
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
798
|
+
private static classifyPlaneDistances(mesh: Mesh, plane: Plane): { min: number; max: number } {
|
|
799
|
+
if (mesh.vertexCount <= 0) return { min: 0, max: 0 };
|
|
800
|
+
let min = Number.POSITIVE_INFINITY;
|
|
801
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
802
|
+
for (const vertex of mesh.vertices) {
|
|
803
|
+
const distance = plane.distanceTo(vertex);
|
|
804
|
+
if (distance < min) min = distance;
|
|
805
|
+
if (distance > max) max = distance;
|
|
806
|
+
}
|
|
807
|
+
return { min, max };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private static createPlaneHalfSpaceCutter(
|
|
811
|
+
host: Mesh,
|
|
812
|
+
plane: Plane,
|
|
813
|
+
options?: MeshPlaneSplitOptions,
|
|
814
|
+
): Mesh {
|
|
815
|
+
const bounds = Mesh.toDebugBounds(host.getBounds());
|
|
816
|
+
const scale = Number.isFinite(options?.planePaddingScale)
|
|
817
|
+
? Math.max(1.1, options?.planePaddingScale ?? 2.5)
|
|
818
|
+
: 2.5;
|
|
819
|
+
const radius = Math.max(1e-6, bounds.diagonal * 0.5);
|
|
820
|
+
const halfU = radius * scale;
|
|
821
|
+
const halfV = radius * scale;
|
|
822
|
+
const halfW = radius * scale;
|
|
823
|
+
const { uAxis, vAxis, normal } = Mesh.orthonormalPlaneFrame(plane);
|
|
824
|
+
const projectedCenter = plane.projectPoint(bounds.center);
|
|
825
|
+
const boxCenter = projectedCenter.add(normal.scale(halfW));
|
|
826
|
+
return Mesh.createOrientedBox(boxCenter, uAxis, vAxis, normal, halfU, halfV, halfW);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private static detectClosedCurveNormal(curve: SweepableCurve, points: Point[]): Vec3 {
|
|
830
|
+
if (curve instanceof Circle || curve instanceof Arc) {
|
|
831
|
+
return curve.normal.normalize();
|
|
832
|
+
}
|
|
833
|
+
const normal = Mesh.computePlanarCurveNormal(points, true).normalize();
|
|
834
|
+
if (!Number.isFinite(normal.x) || !Number.isFinite(normal.y) || !Number.isFinite(normal.z) || normal.length() < 1e-9) {
|
|
835
|
+
throw new Error("Mesh.split(curve) requires a planar closed curve with a valid normal.");
|
|
836
|
+
}
|
|
837
|
+
return normal;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private static createCurveSplitCutter(
|
|
841
|
+
host: Mesh,
|
|
842
|
+
curve: SweepableCurve,
|
|
843
|
+
options?: MeshCurveSplitOptions,
|
|
844
|
+
): Mesh {
|
|
845
|
+
const segments = Number.isFinite(options?.curveSegments)
|
|
846
|
+
? Math.max(3, Math.floor(options?.curveSegments ?? 32))
|
|
847
|
+
: 32;
|
|
848
|
+
const points = Mesh.getClosedCurveLoopPoints(curve, segments);
|
|
849
|
+
const normal = Mesh.detectClosedCurveNormal(curve, points);
|
|
850
|
+
|
|
851
|
+
let extrusionDirection = options?.direction;
|
|
852
|
+
let basePoints = points;
|
|
853
|
+
if (extrusionDirection) {
|
|
854
|
+
if (!Number.isFinite(extrusionDirection.length()) || extrusionDirection.length() < 1e-9) {
|
|
855
|
+
throw new Error("Mesh.split(curve) requires a non-zero direction when direction is provided.");
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
858
|
+
const bounds = Mesh.toDebugBounds(host.getBounds());
|
|
859
|
+
const margin = Math.max(1e-6, bounds.diagonal * 0.25);
|
|
860
|
+
const distances = host.vertices.map((vertex) => normal.dot(vertex.sub(points[0])));
|
|
861
|
+
const minDistance = Math.min(...distances);
|
|
862
|
+
const maxDistance = Math.max(...distances);
|
|
863
|
+
const halfDepth = Math.max(Math.abs(minDistance), Math.abs(maxDistance)) + margin;
|
|
864
|
+
extrusionDirection = normal.scale(halfDepth * 2);
|
|
865
|
+
basePoints = points.map((point) => point.add(normal.scale(-halfDepth)));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const height = extrusionDirection.length();
|
|
869
|
+
const unit = extrusionDirection.normalize();
|
|
870
|
+
const prepared = Mesh.prepareBooleanCutterCurve(basePoints, true, unit, height);
|
|
871
|
+
return Mesh.extrudePlanarCurve(prepared.points, unit, prepared.height, true);
|
|
643
872
|
}
|
|
644
|
-
|
|
645
|
-
private static emptyMesh(): Mesh {
|
|
646
|
-
return Mesh.fromTrustedBuffer(new Float64Array(0));
|
|
647
|
-
}
|
|
648
873
|
|
|
649
874
|
// ── GPU-ready buffers ──────────────────────────────────────────
|
|
650
875
|
|
|
@@ -1198,91 +1423,91 @@ export class Mesh {
|
|
|
1198
1423
|
* Extrude a planar curve along a direction.
|
|
1199
1424
|
* Open curves return an uncapped polysurface; closed curves are capped solids.
|
|
1200
1425
|
*/
|
|
1201
|
-
static extrudePlanarCurve(points: Point[], normal: Vec3, height: number, closed: boolean): Mesh {
|
|
1202
|
-
ensureInit();
|
|
1203
|
-
const buf = wasm.mesh_extrude_planar_curve(
|
|
1204
|
-
pointsToCoords(points),
|
|
1205
|
-
normal.x, normal.y, normal.z,
|
|
1426
|
+
static extrudePlanarCurve(points: Point[], normal: Vec3, height: number, closed: boolean): Mesh {
|
|
1427
|
+
ensureInit();
|
|
1428
|
+
const buf = wasm.mesh_extrude_planar_curve(
|
|
1429
|
+
pointsToCoords(points),
|
|
1430
|
+
normal.x, normal.y, normal.z,
|
|
1206
1431
|
height,
|
|
1207
1432
|
closed,
|
|
1208
|
-
);
|
|
1209
|
-
return Mesh.fromTrustedBuffer(buf);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
private static normalizeClosedCurvePoints(points: Point[], eps = 1e-10): Point[] {
|
|
1213
|
-
const normalized: Point[] = [];
|
|
1214
|
-
for (const point of points) {
|
|
1215
|
-
if (normalized.length > 0 && normalized[normalized.length - 1].equals(point, eps)) continue;
|
|
1216
|
-
normalized.push(point);
|
|
1217
|
-
}
|
|
1218
|
-
if (normalized.length > 1 && normalized[0].equals(normalized[normalized.length - 1], eps)) {
|
|
1219
|
-
normalized.pop();
|
|
1220
|
-
}
|
|
1221
|
-
return normalized;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
private static getClosedCurveLoopPoints(curve: SweepableCurve, segments: number): Point[] {
|
|
1225
|
-
const closureEps = 1e-6;
|
|
1226
|
-
const sampleCount = Number.isFinite(segments) ? Math.max(3, Math.floor(segments)) : 32;
|
|
1227
|
-
|
|
1228
|
-
if (curve instanceof Circle) {
|
|
1229
|
-
return Mesh.normalizeClosedCurvePoints(curve.sample(sampleCount), closureEps);
|
|
1230
|
-
}
|
|
1231
|
-
if (curve instanceof Polygon || curve instanceof Polyline) {
|
|
1232
|
-
if (!curve.isClosed(closureEps)) {
|
|
1233
|
-
throw new Error("Mesh.extrudeCurveAsSolid() requires a closed polyline or polygon.");
|
|
1234
|
-
}
|
|
1235
|
-
return Mesh.normalizeClosedCurvePoints(curve.points, closureEps);
|
|
1236
|
-
}
|
|
1237
|
-
if (curve instanceof PolyCurve) {
|
|
1238
|
-
if (!curve.isClosed(closureEps)) {
|
|
1239
|
-
throw new Error("Mesh.extrudeCurveAsSolid() requires a closed polycurve.");
|
|
1240
|
-
}
|
|
1241
|
-
return Mesh.normalizeClosedCurvePoints(curve.sample(Math.max(2, sampleCount)), closureEps);
|
|
1242
|
-
}
|
|
1243
|
-
if (curve instanceof NurbsCurve) {
|
|
1244
|
-
if (!curve.isClosed(closureEps)) {
|
|
1245
|
-
throw new Error("Mesh.extrudeCurveAsSolid() requires a closed NURBS curve.");
|
|
1246
|
-
}
|
|
1247
|
-
return Mesh.normalizeClosedCurvePoints(curve.sample(sampleCount), closureEps);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
throw new Error(
|
|
1251
|
-
"Mesh.extrudeCurveAsSolid() requires a closed curve "
|
|
1252
|
-
+ "(Circle, closed Polyline/Polygon, closed PolyCurve, or closed NurbsCurve).",
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
/**
|
|
1257
|
-
* Extrude a closed planar curve into a trusted closed solid suitable for booleans.
|
|
1258
|
-
* Curved inputs are sampled first, so `segments` controls tessellation density
|
|
1259
|
-
* for circles, polycurves, and NURBS curves.
|
|
1260
|
-
*/
|
|
1261
|
-
static extrudeCurveAsSolid(curve: SweepableCurve, direction: Vec3, segments = 32): Mesh {
|
|
1262
|
-
ensureInit();
|
|
1263
|
-
|
|
1264
|
-
const height = direction.length();
|
|
1265
|
-
if (!Number.isFinite(height) || height < 1e-10) {
|
|
1266
|
-
throw new Error("Mesh.extrudeCurveAsSolid() requires a non-zero extrusion direction.");
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
const points = Mesh.getClosedCurveLoopPoints(curve, segments);
|
|
1270
|
-
if (points.length < 3) {
|
|
1271
|
-
throw new Error("Mesh.extrudeCurveAsSolid() requires at least 3 unique boundary points.");
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
const normal = direction.scale(1 / height);
|
|
1275
|
-
const mesh = Mesh.extrudePlanarCurve(points, normal, height, true);
|
|
1276
|
-
if (mesh.vertexCount === 0) {
|
|
1277
|
-
throw new Error("Mesh.extrudeCurveAsSolid() failed. Ensure the curve is planar and closed.");
|
|
1278
|
-
}
|
|
1279
|
-
return mesh;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
/**
|
|
1283
|
-
* Shift a closed cutter profile slightly opposite to travel direction and
|
|
1284
|
-
* compensate height so the distal end remains at the user-intended depth.
|
|
1285
|
-
*/
|
|
1433
|
+
);
|
|
1434
|
+
return Mesh.fromTrustedBuffer(buf);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
private static normalizeClosedCurvePoints(points: Point[], eps = 1e-10): Point[] {
|
|
1438
|
+
const normalized: Point[] = [];
|
|
1439
|
+
for (const point of points) {
|
|
1440
|
+
if (normalized.length > 0 && normalized[normalized.length - 1].equals(point, eps)) continue;
|
|
1441
|
+
normalized.push(point);
|
|
1442
|
+
}
|
|
1443
|
+
if (normalized.length > 1 && normalized[0].equals(normalized[normalized.length - 1], eps)) {
|
|
1444
|
+
normalized.pop();
|
|
1445
|
+
}
|
|
1446
|
+
return normalized;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private static getClosedCurveLoopPoints(curve: SweepableCurve, segments: number): Point[] {
|
|
1450
|
+
const closureEps = 1e-6;
|
|
1451
|
+
const sampleCount = Number.isFinite(segments) ? Math.max(3, Math.floor(segments)) : 32;
|
|
1452
|
+
|
|
1453
|
+
if (curve instanceof Circle) {
|
|
1454
|
+
return Mesh.normalizeClosedCurvePoints(curve.sample(sampleCount), closureEps);
|
|
1455
|
+
}
|
|
1456
|
+
if (curve instanceof Polygon || curve instanceof Polyline) {
|
|
1457
|
+
if (!curve.isClosed(closureEps)) {
|
|
1458
|
+
throw new Error("Mesh.extrudeCurveAsSolid() requires a closed polyline or polygon.");
|
|
1459
|
+
}
|
|
1460
|
+
return Mesh.normalizeClosedCurvePoints(curve.points, closureEps);
|
|
1461
|
+
}
|
|
1462
|
+
if (curve instanceof PolyCurve) {
|
|
1463
|
+
if (!curve.isClosed(closureEps)) {
|
|
1464
|
+
throw new Error("Mesh.extrudeCurveAsSolid() requires a closed polycurve.");
|
|
1465
|
+
}
|
|
1466
|
+
return Mesh.normalizeClosedCurvePoints(curve.sample(Math.max(2, sampleCount)), closureEps);
|
|
1467
|
+
}
|
|
1468
|
+
if (curve instanceof NurbsCurve) {
|
|
1469
|
+
if (!curve.isClosed(closureEps)) {
|
|
1470
|
+
throw new Error("Mesh.extrudeCurveAsSolid() requires a closed NURBS curve.");
|
|
1471
|
+
}
|
|
1472
|
+
return Mesh.normalizeClosedCurvePoints(curve.sample(sampleCount), closureEps);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
"Mesh.extrudeCurveAsSolid() requires a closed curve "
|
|
1477
|
+
+ "(Circle, closed Polyline/Polygon, closed PolyCurve, or closed NurbsCurve).",
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Extrude a closed planar curve into a trusted closed solid suitable for booleans.
|
|
1483
|
+
* Curved inputs are sampled first, so `segments` controls tessellation density
|
|
1484
|
+
* for circles, polycurves, and NURBS curves.
|
|
1485
|
+
*/
|
|
1486
|
+
static extrudeCurveAsSolid(curve: SweepableCurve, direction: Vec3, segments = 32): Mesh {
|
|
1487
|
+
ensureInit();
|
|
1488
|
+
|
|
1489
|
+
const height = direction.length();
|
|
1490
|
+
if (!Number.isFinite(height) || height < 1e-10) {
|
|
1491
|
+
throw new Error("Mesh.extrudeCurveAsSolid() requires a non-zero extrusion direction.");
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const points = Mesh.getClosedCurveLoopPoints(curve, segments);
|
|
1495
|
+
if (points.length < 3) {
|
|
1496
|
+
throw new Error("Mesh.extrudeCurveAsSolid() requires at least 3 unique boundary points.");
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const normal = direction.scale(1 / height);
|
|
1500
|
+
const mesh = Mesh.extrudePlanarCurve(points, normal, height, true);
|
|
1501
|
+
if (mesh.vertexCount === 0) {
|
|
1502
|
+
throw new Error("Mesh.extrudeCurveAsSolid() failed. Ensure the curve is planar and closed.");
|
|
1503
|
+
}
|
|
1504
|
+
return mesh;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Shift a closed cutter profile slightly opposite to travel direction and
|
|
1509
|
+
* compensate height so the distal end remains at the user-intended depth.
|
|
1510
|
+
*/
|
|
1286
1511
|
static prepareBooleanCutterCurve(
|
|
1287
1512
|
points: Point[],
|
|
1288
1513
|
closed: boolean,
|
|
@@ -1591,10 +1816,10 @@ export class Mesh {
|
|
|
1591
1816
|
return Mesh.fromTrustedBuffer(result);
|
|
1592
1817
|
}
|
|
1593
1818
|
|
|
1594
|
-
static encodeBooleanOperationToken(
|
|
1595
|
-
operation: MeshBooleanOperation,
|
|
1596
|
-
a: Mesh,
|
|
1597
|
-
b: Mesh,
|
|
1819
|
+
static encodeBooleanOperationToken(
|
|
1820
|
+
operation: MeshBooleanOperation,
|
|
1821
|
+
a: Mesh,
|
|
1822
|
+
b: Mesh,
|
|
1598
1823
|
options?: MeshBooleanOptions,
|
|
1599
1824
|
): string {
|
|
1600
1825
|
const tokens: string[] = [operation];
|
|
@@ -1603,9 +1828,24 @@ export class Mesh {
|
|
|
1603
1828
|
}
|
|
1604
1829
|
if (options?.debugForceFaceID) {
|
|
1605
1830
|
tokens.push("forceFaceID");
|
|
1606
|
-
}
|
|
1607
|
-
return tokens.join("@");
|
|
1608
|
-
}
|
|
1831
|
+
}
|
|
1832
|
+
return tokens.join("@");
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
static encodeBooleanSplitToken(
|
|
1836
|
+
a: Mesh,
|
|
1837
|
+
b: Mesh,
|
|
1838
|
+
options?: MeshBooleanOptions,
|
|
1839
|
+
): string {
|
|
1840
|
+
const tokens: string[] = ["split"];
|
|
1841
|
+
if (a._trustedBooleanInput && b._trustedBooleanInput) {
|
|
1842
|
+
tokens.push("trustedInput");
|
|
1843
|
+
}
|
|
1844
|
+
if (options?.debugForceFaceID) {
|
|
1845
|
+
tokens.push("forceFaceID");
|
|
1846
|
+
}
|
|
1847
|
+
return tokens.join("@");
|
|
1848
|
+
}
|
|
1609
1849
|
|
|
1610
1850
|
// ── Booleans ───────────────────────────────────────────────────
|
|
1611
1851
|
|
|
@@ -1638,22 +1878,22 @@ export class Mesh {
|
|
|
1638
1878
|
* @param options - Optional safety overrides
|
|
1639
1879
|
* @returns New mesh with other's volume removed from this
|
|
1640
1880
|
*/
|
|
1641
|
-
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1642
|
-
ensureInit();
|
|
1643
|
-
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
|
|
1644
|
-
return this.runBoolean(
|
|
1645
|
-
other,
|
|
1646
|
-
"subtraction",
|
|
1647
|
-
() => wasm.mesh_boolean_operation(
|
|
1648
|
-
this._vertexCount,
|
|
1649
|
-
this._buffer,
|
|
1650
|
-
other._vertexCount,
|
|
1651
|
-
other._buffer,
|
|
1652
|
-
operationToken,
|
|
1653
|
-
),
|
|
1654
|
-
options,
|
|
1655
|
-
);
|
|
1656
|
-
}
|
|
1881
|
+
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1882
|
+
ensureInit();
|
|
1883
|
+
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
|
|
1884
|
+
return this.runBoolean(
|
|
1885
|
+
other,
|
|
1886
|
+
"subtraction",
|
|
1887
|
+
() => wasm.mesh_boolean_operation(
|
|
1888
|
+
this._vertexCount,
|
|
1889
|
+
this._buffer,
|
|
1890
|
+
other._vertexCount,
|
|
1891
|
+
other._buffer,
|
|
1892
|
+
operationToken,
|
|
1893
|
+
),
|
|
1894
|
+
options,
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1657
1897
|
|
|
1658
1898
|
/**
|
|
1659
1899
|
* Compute boolean intersection with another mesh.
|
|
@@ -1661,11 +1901,11 @@ export class Mesh {
|
|
|
1661
1901
|
* @param options - Optional safety overrides
|
|
1662
1902
|
* @returns New mesh containing only the overlapping volume
|
|
1663
1903
|
*/
|
|
1664
|
-
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1665
|
-
ensureInit();
|
|
1666
|
-
const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
|
|
1667
|
-
return this.runBoolean(
|
|
1668
|
-
other,
|
|
1904
|
+
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1905
|
+
ensureInit();
|
|
1906
|
+
const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
|
|
1907
|
+
return this.runBoolean(
|
|
1908
|
+
other,
|
|
1669
1909
|
"intersection",
|
|
1670
1910
|
() => wasm.mesh_boolean_operation(
|
|
1671
1911
|
this._vertexCount,
|
|
@@ -1674,13 +1914,162 @@ export class Mesh {
|
|
|
1674
1914
|
other._buffer,
|
|
1675
1915
|
operationToken,
|
|
1676
1916
|
),
|
|
1677
|
-
options,
|
|
1678
|
-
);
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1917
|
+
options,
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
private splitWithMesh(other: Mesh, options?: MeshBooleanOptions): MeshSplitResult {
|
|
1922
|
+
ensureInit();
|
|
1923
|
+
|
|
1924
|
+
const faceCountA = this.faceCount;
|
|
1925
|
+
const faceCountB = other.faceCount;
|
|
1926
|
+
const hostClosed = this.isClosedVolume();
|
|
1927
|
+
const cutterClosed = other.isClosedVolume();
|
|
1928
|
+
const useVolumetricSplit = hostClosed && cutterClosed;
|
|
1929
|
+
if (faceCountA === 0) return Mesh.createSplitResult([], [], useVolumetricSplit ? "volume" : "surface");
|
|
1930
|
+
if (faceCountB === 0) {
|
|
1931
|
+
return Mesh.createSplitResult(
|
|
1932
|
+
[Mesh.cloneMesh(this)],
|
|
1933
|
+
[],
|
|
1934
|
+
useVolumetricSplit || cutterClosed ? "volume" : "surface",
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const boundsA = Mesh.computeRawBounds(this);
|
|
1939
|
+
const boundsB = Mesh.computeRawBounds(other);
|
|
1940
|
+
const contactTol = Mesh.booleanContactTolerance(boundsA, boundsB);
|
|
1941
|
+
if (useVolumetricSplit && !Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
|
|
1942
|
+
return Mesh.createSplitResult([Mesh.cloneMesh(this)], [], "volume");
|
|
1943
|
+
}
|
|
1944
|
+
if (!useVolumetricSplit && cutterClosed && !Mesh.boundsOverlap(boundsA, boundsB, contactTol)) {
|
|
1945
|
+
return Mesh.createSplitResult([Mesh.cloneMesh(this)], [], "volume");
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (!options?.allowUnsafe) {
|
|
1949
|
+
const limits = Mesh.resolveBooleanLimits(options?.limits);
|
|
1950
|
+
const maxInputFaces = Math.max(faceCountA, faceCountB);
|
|
1951
|
+
const combinedInputFaces = faceCountA + faceCountB;
|
|
1952
|
+
const faceProduct = faceCountA * faceCountB;
|
|
1953
|
+
if (
|
|
1954
|
+
maxInputFaces > limits.maxInputFacesPerMesh
|
|
1955
|
+
|| combinedInputFaces > limits.maxCombinedInputFaces
|
|
1956
|
+
|| faceProduct > limits.maxFaceProduct
|
|
1957
|
+
) {
|
|
1958
|
+
throw new Error(
|
|
1959
|
+
`Mesh split blocked by safety limits `
|
|
1960
|
+
+ `(A faces=${faceCountA}, B faces=${faceCountB}, faceProduct=${faceProduct}). `
|
|
1961
|
+
+ "Simplify inputs or pass allowUnsafe: true to force execution.",
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const splitToken = Mesh.encodeBooleanSplitToken(this, other, options);
|
|
1967
|
+
const result = useVolumetricSplit
|
|
1968
|
+
? wasm.mesh_boolean_split(
|
|
1969
|
+
this._vertexCount,
|
|
1970
|
+
this._buffer,
|
|
1971
|
+
other._vertexCount,
|
|
1972
|
+
other._buffer,
|
|
1973
|
+
splitToken,
|
|
1974
|
+
)
|
|
1975
|
+
: wasm.mesh_surface_split(
|
|
1976
|
+
this._vertexCount,
|
|
1977
|
+
this._buffer,
|
|
1978
|
+
other._vertexCount,
|
|
1979
|
+
other._buffer,
|
|
1980
|
+
splitToken,
|
|
1981
|
+
);
|
|
1982
|
+
if (result.length === 0) {
|
|
1983
|
+
throw new Error("Mesh split failed and returned an invalid result buffer.");
|
|
1984
|
+
}
|
|
1985
|
+
const parsed = Mesh.parseSplitResultBuffer(result);
|
|
1986
|
+
return cutterClosed
|
|
1987
|
+
? Mesh.createSplitResult(parsed.outside, parsed.inside, "volume")
|
|
1988
|
+
: Mesh.createSurfaceSplitResult(parsed.outside, parsed.inside);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
private splitWithPlane(plane: Plane, options?: MeshPlaneSplitOptions): MeshPlaneSplitResult {
|
|
1992
|
+
ensureInit();
|
|
1993
|
+
const hostClosed = this.isClosedVolume();
|
|
1994
|
+
|
|
1995
|
+
if (this.faceCount === 0) return Mesh.createPlaneSplitResult([], [], hostClosed ? "volume" : "surface");
|
|
1996
|
+
|
|
1997
|
+
const distances = Mesh.classifyPlaneDistances(this, plane);
|
|
1998
|
+
const eps = Mesh.boundsDiag(Mesh.computeRawBounds(this)) * 1e-9 + 1e-9;
|
|
1999
|
+
if (distances.max <= eps) {
|
|
2000
|
+
return Mesh.createPlaneSplitResult([Mesh.cloneMesh(this)], [], hostClosed ? "volume" : "surface");
|
|
2001
|
+
}
|
|
2002
|
+
if (distances.min >= -eps) {
|
|
2003
|
+
return Mesh.createPlaneSplitResult([], [Mesh.cloneMesh(this)], hostClosed ? "volume" : "surface");
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (!hostClosed) {
|
|
2007
|
+
const result = wasm.mesh_surface_split_plane(
|
|
2008
|
+
this._vertexCount,
|
|
2009
|
+
this._buffer,
|
|
2010
|
+
plane.normal.x,
|
|
2011
|
+
plane.normal.y,
|
|
2012
|
+
plane.normal.z,
|
|
2013
|
+
plane.d,
|
|
2014
|
+
Mesh.encodeBooleanSplitToken(this, this, options),
|
|
2015
|
+
);
|
|
2016
|
+
if (result.length === 0) {
|
|
2017
|
+
throw new Error("Mesh split by plane failed and returned an invalid result buffer.");
|
|
2018
|
+
}
|
|
2019
|
+
const split = Mesh.parseSplitResultBuffer(result);
|
|
2020
|
+
return Mesh.createPlaneSplitResult(split.outside, split.inside, "surface");
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const result = wasm.mesh_solid_split_plane(
|
|
2024
|
+
this._vertexCount,
|
|
2025
|
+
this._buffer,
|
|
2026
|
+
plane.normal.x,
|
|
2027
|
+
plane.normal.y,
|
|
2028
|
+
plane.normal.z,
|
|
2029
|
+
plane.d,
|
|
2030
|
+
Mesh.encodeBooleanSplitToken(this, this, options),
|
|
2031
|
+
);
|
|
2032
|
+
if (result.length === 0) {
|
|
2033
|
+
throw new Error("Mesh split by plane failed and returned an invalid result buffer.");
|
|
2034
|
+
}
|
|
2035
|
+
const split = Mesh.parseSplitResultBuffer(result);
|
|
2036
|
+
return Mesh.createPlaneSplitResult(split.outside, split.inside, "volume");
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
private splitWithCurve(curve: SweepableCurve, options?: MeshCurveSplitOptions): MeshSplitResult {
|
|
2040
|
+
ensureInit();
|
|
2041
|
+
if (this.faceCount === 0) return Mesh.createSplitResult([], []);
|
|
2042
|
+
const cutter = Mesh.createCurveSplitCutter(this, curve, options);
|
|
2043
|
+
return this.splitWithMesh(cutter, options);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Split this mesh by another mesh, a plane, or a closed planar curve.
|
|
2048
|
+
* Mesh split: `outside = this - cutter`, `inside = this ∩ cutter`.
|
|
2049
|
+
* Plane split: `negative` is opposite the plane normal, `positive` follows the plane normal.
|
|
2050
|
+
* Curve split: the curve is converted into a cutter solid, either from the
|
|
2051
|
+
* explicit direction you pass or via an automatic through-cut along the
|
|
2052
|
+
* detected curve normal across the host mesh.
|
|
2053
|
+
*/
|
|
2054
|
+
split(other: Mesh, options?: MeshBooleanOptions): MeshSplitResult;
|
|
2055
|
+
split(plane: Plane, options?: MeshPlaneSplitOptions): MeshPlaneSplitResult;
|
|
2056
|
+
split(curve: SweepableCurve, options?: MeshCurveSplitOptions): MeshSplitResult;
|
|
2057
|
+
split(
|
|
2058
|
+
target: Mesh | Plane | SweepableCurve,
|
|
2059
|
+
options?: MeshBooleanOptions | MeshPlaneSplitOptions | MeshCurveSplitOptions,
|
|
2060
|
+
): MeshSplitResult | MeshPlaneSplitResult {
|
|
2061
|
+
if (target instanceof Mesh) {
|
|
2062
|
+
return this.splitWithMesh(target, options as MeshBooleanOptions | undefined);
|
|
2063
|
+
}
|
|
2064
|
+
if (target instanceof Plane) {
|
|
2065
|
+
return this.splitWithPlane(target, options as MeshPlaneSplitOptions | undefined);
|
|
2066
|
+
}
|
|
2067
|
+
return this.splitWithCurve(target, options as MeshCurveSplitOptions | undefined);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
debugBoolean(
|
|
2071
|
+
other: Mesh,
|
|
2072
|
+
operation: MeshBooleanOperation,
|
|
1684
2073
|
options?: MeshBooleanOptions,
|
|
1685
2074
|
debugOptions?: MeshBooleanDebugOptions,
|
|
1686
2075
|
): MeshBooleanDebugReport {
|
|
@@ -1709,184 +2098,184 @@ export class Mesh {
|
|
|
1709
2098
|
return this.debugBoolean(other, "subtraction", options, debugOptions);
|
|
1710
2099
|
}
|
|
1711
2100
|
|
|
1712
|
-
debugIntersect(
|
|
1713
|
-
other: Mesh,
|
|
1714
|
-
options?: MeshBooleanOptions,
|
|
1715
|
-
debugOptions?: MeshBooleanDebugOptions,
|
|
1716
|
-
): MeshBooleanDebugReport {
|
|
1717
|
-
return this.debugBoolean(other, "intersection", options, debugOptions);
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
exportBooleanRepro(
|
|
1721
|
-
other: Mesh,
|
|
1722
|
-
operation: MeshBooleanOperation,
|
|
1723
|
-
options?: MeshBooleanOptions,
|
|
1724
|
-
reproOptions?: MeshBooleanReproOptions,
|
|
1725
|
-
): MeshBooleanReproPayload {
|
|
1726
|
-
if (!reproOptions?.includeResult) {
|
|
1727
|
-
return Mesh.createBooleanReproPayload(this, other, operation, options);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
try {
|
|
1731
|
-
const result = operation === "union"
|
|
1732
|
-
? this.union(other, options)
|
|
1733
|
-
: operation === "subtraction"
|
|
1734
|
-
? this.subtract(other, options)
|
|
1735
|
-
: this.intersect(other, options);
|
|
1736
|
-
return Mesh.createBooleanReproPayload(
|
|
1737
|
-
this,
|
|
1738
|
-
other,
|
|
1739
|
-
operation,
|
|
1740
|
-
options,
|
|
1741
|
-
result,
|
|
1742
|
-
reproOptions.debugOptions,
|
|
1743
|
-
);
|
|
1744
|
-
} catch (error) {
|
|
1745
|
-
return Mesh.createBooleanReproPayload(
|
|
1746
|
-
this,
|
|
1747
|
-
other,
|
|
1748
|
-
operation,
|
|
1749
|
-
options,
|
|
1750
|
-
undefined,
|
|
1751
|
-
undefined,
|
|
1752
|
-
error,
|
|
1753
|
-
);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
exportSubtractRepro(
|
|
1758
|
-
other: Mesh,
|
|
1759
|
-
options?: MeshBooleanOptions,
|
|
1760
|
-
reproOptions?: MeshBooleanReproOptions,
|
|
1761
|
-
): MeshBooleanReproPayload {
|
|
1762
|
-
return this.exportBooleanRepro(other, "subtraction", options, reproOptions);
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
exportBooleanReproJSON(
|
|
1766
|
-
other: Mesh,
|
|
1767
|
-
operation: MeshBooleanOperation,
|
|
1768
|
-
options?: MeshBooleanOptions,
|
|
1769
|
-
reproOptions?: MeshBooleanReproOptions,
|
|
1770
|
-
): string {
|
|
1771
|
-
return JSON.stringify(this.exportBooleanRepro(other, operation, options, reproOptions), null, 2);
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
exportSubtractReproJSON(
|
|
1775
|
-
other: Mesh,
|
|
1776
|
-
options?: MeshBooleanOptions,
|
|
1777
|
-
reproOptions?: MeshBooleanReproOptions,
|
|
1778
|
-
): string {
|
|
1779
|
-
return this.exportBooleanReproJSON(other, "subtraction", options, reproOptions);
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
exportUnionRepro(
|
|
1783
|
-
other: Mesh,
|
|
1784
|
-
options?: MeshBooleanOptions,
|
|
1785
|
-
reproOptions?: MeshBooleanReproOptions,
|
|
1786
|
-
): MeshBooleanReproPayload {
|
|
1787
|
-
return this.exportBooleanRepro(other, "union", options, reproOptions);
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
exportIntersectRepro(
|
|
1791
|
-
other: Mesh,
|
|
1792
|
-
options?: MeshBooleanOptions,
|
|
1793
|
-
reproOptions?: MeshBooleanReproOptions,
|
|
1794
|
-
): MeshBooleanReproPayload {
|
|
1795
|
-
return this.exportBooleanRepro(other, "intersection", options, reproOptions);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
static replayBooleanRepro(payload: MeshBooleanReproPayload): Mesh {
|
|
1799
|
-
const inputA = Mesh.fromBuffer(new Float64Array(payload.inputA.buffer), {
|
|
1800
|
-
trustedBooleanInput: payload.inputA.trustedBooleanInput,
|
|
1801
|
-
});
|
|
1802
|
-
const inputB = Mesh.fromBuffer(new Float64Array(payload.inputB.buffer), {
|
|
1803
|
-
trustedBooleanInput: payload.inputB.trustedBooleanInput,
|
|
1804
|
-
});
|
|
1805
|
-
|
|
1806
|
-
return payload.operation === "union"
|
|
1807
|
-
? inputA.union(inputB, payload.options ?? undefined)
|
|
1808
|
-
: payload.operation === "subtraction"
|
|
1809
|
-
? inputA.subtract(inputB, payload.options ?? undefined)
|
|
1810
|
-
: inputA.intersect(inputB, payload.options ?? undefined);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
/**
|
|
1814
|
-
* Compute boolean union in a dedicated Web Worker (non-blocking).
|
|
1815
|
-
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
1816
|
-
*/
|
|
1817
|
-
async unionAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
1818
|
-
try {
|
|
1819
|
-
const result = await runMeshBooleanInWorkerPool(
|
|
1820
|
-
"union",
|
|
1821
|
-
this._buffer,
|
|
1822
|
-
other._buffer,
|
|
1823
|
-
this._trustedBooleanInput,
|
|
1824
|
-
other._trustedBooleanInput,
|
|
1825
|
-
options,
|
|
1826
|
-
);
|
|
1827
|
-
return Mesh.fromTrustedBuffer(result);
|
|
1828
|
-
} catch (error) {
|
|
1829
|
-
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
1830
|
-
console.warn("Mesh.unionAsync worker failed; falling back to main-thread boolean.", error);
|
|
1831
|
-
return this.union(other, {
|
|
1832
|
-
allowUnsafe: options?.allowUnsafe ?? true,
|
|
1833
|
-
limits: options?.limits,
|
|
1834
|
-
debugForceFaceID: options?.debugForceFaceID,
|
|
1835
|
-
});
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
2101
|
+
debugIntersect(
|
|
2102
|
+
other: Mesh,
|
|
2103
|
+
options?: MeshBooleanOptions,
|
|
2104
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
2105
|
+
): MeshBooleanDebugReport {
|
|
2106
|
+
return this.debugBoolean(other, "intersection", options, debugOptions);
|
|
2107
|
+
}
|
|
1838
2108
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
other._buffer,
|
|
1849
|
-
this._trustedBooleanInput,
|
|
1850
|
-
other._trustedBooleanInput,
|
|
1851
|
-
options,
|
|
1852
|
-
);
|
|
1853
|
-
return Mesh.fromTrustedBuffer(result);
|
|
1854
|
-
} catch (error) {
|
|
1855
|
-
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
1856
|
-
console.warn("Mesh.subtractAsync worker failed; falling back to main-thread boolean.", error);
|
|
1857
|
-
return this.subtract(other, {
|
|
1858
|
-
allowUnsafe: options?.allowUnsafe ?? true,
|
|
1859
|
-
limits: options?.limits,
|
|
1860
|
-
debugForceFaceID: options?.debugForceFaceID,
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
2109
|
+
exportBooleanRepro(
|
|
2110
|
+
other: Mesh,
|
|
2111
|
+
operation: MeshBooleanOperation,
|
|
2112
|
+
options?: MeshBooleanOptions,
|
|
2113
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
2114
|
+
): MeshBooleanReproPayload {
|
|
2115
|
+
if (!reproOptions?.includeResult) {
|
|
2116
|
+
return Mesh.createBooleanReproPayload(this, other, operation, options);
|
|
2117
|
+
}
|
|
1864
2118
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
);
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2119
|
+
try {
|
|
2120
|
+
const result = operation === "union"
|
|
2121
|
+
? this.union(other, options)
|
|
2122
|
+
: operation === "subtraction"
|
|
2123
|
+
? this.subtract(other, options)
|
|
2124
|
+
: this.intersect(other, options);
|
|
2125
|
+
return Mesh.createBooleanReproPayload(
|
|
2126
|
+
this,
|
|
2127
|
+
other,
|
|
2128
|
+
operation,
|
|
2129
|
+
options,
|
|
2130
|
+
result,
|
|
2131
|
+
reproOptions.debugOptions,
|
|
2132
|
+
);
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
return Mesh.createBooleanReproPayload(
|
|
2135
|
+
this,
|
|
2136
|
+
other,
|
|
2137
|
+
operation,
|
|
2138
|
+
options,
|
|
2139
|
+
undefined,
|
|
2140
|
+
undefined,
|
|
2141
|
+
error,
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
exportSubtractRepro(
|
|
2147
|
+
other: Mesh,
|
|
2148
|
+
options?: MeshBooleanOptions,
|
|
2149
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
2150
|
+
): MeshBooleanReproPayload {
|
|
2151
|
+
return this.exportBooleanRepro(other, "subtraction", options, reproOptions);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
exportBooleanReproJSON(
|
|
2155
|
+
other: Mesh,
|
|
2156
|
+
operation: MeshBooleanOperation,
|
|
2157
|
+
options?: MeshBooleanOptions,
|
|
2158
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
2159
|
+
): string {
|
|
2160
|
+
return JSON.stringify(this.exportBooleanRepro(other, operation, options, reproOptions), null, 2);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
exportSubtractReproJSON(
|
|
2164
|
+
other: Mesh,
|
|
2165
|
+
options?: MeshBooleanOptions,
|
|
2166
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
2167
|
+
): string {
|
|
2168
|
+
return this.exportBooleanReproJSON(other, "subtraction", options, reproOptions);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
exportUnionRepro(
|
|
2172
|
+
other: Mesh,
|
|
2173
|
+
options?: MeshBooleanOptions,
|
|
2174
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
2175
|
+
): MeshBooleanReproPayload {
|
|
2176
|
+
return this.exportBooleanRepro(other, "union", options, reproOptions);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
exportIntersectRepro(
|
|
2180
|
+
other: Mesh,
|
|
2181
|
+
options?: MeshBooleanOptions,
|
|
2182
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
2183
|
+
): MeshBooleanReproPayload {
|
|
2184
|
+
return this.exportBooleanRepro(other, "intersection", options, reproOptions);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
static replayBooleanRepro(payload: MeshBooleanReproPayload): Mesh {
|
|
2188
|
+
const inputA = Mesh.fromBuffer(new Float64Array(payload.inputA.buffer), {
|
|
2189
|
+
trustedBooleanInput: payload.inputA.trustedBooleanInput,
|
|
2190
|
+
});
|
|
2191
|
+
const inputB = Mesh.fromBuffer(new Float64Array(payload.inputB.buffer), {
|
|
2192
|
+
trustedBooleanInput: payload.inputB.trustedBooleanInput,
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
return payload.operation === "union"
|
|
2196
|
+
? inputA.union(inputB, payload.options ?? undefined)
|
|
2197
|
+
: payload.operation === "subtraction"
|
|
2198
|
+
? inputA.subtract(inputB, payload.options ?? undefined)
|
|
2199
|
+
: inputA.intersect(inputB, payload.options ?? undefined);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Compute boolean union in a dedicated Web Worker (non-blocking).
|
|
2204
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
2205
|
+
*/
|
|
2206
|
+
async unionAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
2207
|
+
try {
|
|
2208
|
+
const result = await runMeshBooleanInWorkerPool(
|
|
2209
|
+
"union",
|
|
2210
|
+
this._buffer,
|
|
2211
|
+
other._buffer,
|
|
2212
|
+
this._trustedBooleanInput,
|
|
2213
|
+
other._trustedBooleanInput,
|
|
2214
|
+
options,
|
|
2215
|
+
);
|
|
2216
|
+
return Mesh.fromTrustedBuffer(result);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
2219
|
+
console.warn("Mesh.unionAsync worker failed; falling back to main-thread boolean.", error);
|
|
2220
|
+
return this.union(other, {
|
|
2221
|
+
allowUnsafe: options?.allowUnsafe ?? true,
|
|
2222
|
+
limits: options?.limits,
|
|
2223
|
+
debugForceFaceID: options?.debugForceFaceID,
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* Compute boolean subtraction in a dedicated Web Worker (non-blocking).
|
|
2230
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
2231
|
+
*/
|
|
2232
|
+
async subtractAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
2233
|
+
try {
|
|
2234
|
+
const result = await runMeshBooleanInWorkerPool(
|
|
2235
|
+
"subtraction",
|
|
2236
|
+
this._buffer,
|
|
2237
|
+
other._buffer,
|
|
2238
|
+
this._trustedBooleanInput,
|
|
2239
|
+
other._trustedBooleanInput,
|
|
2240
|
+
options,
|
|
2241
|
+
);
|
|
2242
|
+
return Mesh.fromTrustedBuffer(result);
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
2245
|
+
console.warn("Mesh.subtractAsync worker failed; falling back to main-thread boolean.", error);
|
|
2246
|
+
return this.subtract(other, {
|
|
2247
|
+
allowUnsafe: options?.allowUnsafe ?? true,
|
|
2248
|
+
limits: options?.limits,
|
|
2249
|
+
debugForceFaceID: options?.debugForceFaceID,
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* Compute boolean intersection in a dedicated Web Worker (non-blocking).
|
|
2256
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
2257
|
+
*/
|
|
2258
|
+
async intersectAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
2259
|
+
try {
|
|
2260
|
+
const result = await runMeshBooleanInWorkerPool(
|
|
2261
|
+
"intersection",
|
|
2262
|
+
this._buffer,
|
|
2263
|
+
other._buffer,
|
|
2264
|
+
this._trustedBooleanInput,
|
|
2265
|
+
other._trustedBooleanInput,
|
|
2266
|
+
options,
|
|
2267
|
+
);
|
|
2268
|
+
return Mesh.fromTrustedBuffer(result);
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
2271
|
+
console.warn("Mesh.intersectAsync worker failed; falling back to main-thread boolean.", error);
|
|
2272
|
+
return this.intersect(other, {
|
|
2273
|
+
allowUnsafe: options?.allowUnsafe ?? true,
|
|
2274
|
+
limits: options?.limits,
|
|
2275
|
+
debugForceFaceID: options?.debugForceFaceID,
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
1890
2279
|
|
|
1891
2280
|
// ── Intersection queries ───────────────────────────────────────
|
|
1892
2281
|
|
|
@@ -2176,28 +2565,28 @@ export class Mesh {
|
|
|
2176
2565
|
* Check if this triangulated mesh represents a closed volume.
|
|
2177
2566
|
* Returns true when no welded topological boundary edges are found.
|
|
2178
2567
|
*/
|
|
2179
|
-
isClosedVolume(): boolean {
|
|
2180
|
-
if (this._isClosedVolumeCache !== null) {
|
|
2181
|
-
return this._isClosedVolumeCache;
|
|
2182
|
-
}
|
|
2183
|
-
if (this._topologyMetricsCache) {
|
|
2568
|
+
isClosedVolume(): boolean {
|
|
2569
|
+
if (this._isClosedVolumeCache !== null) {
|
|
2570
|
+
return this._isClosedVolumeCache;
|
|
2571
|
+
}
|
|
2572
|
+
if (this._topologyMetricsCache) {
|
|
2184
2573
|
const closedFromMetrics = this._topologyMetricsCache.boundaryEdges === 0;
|
|
2185
|
-
this._isClosedVolumeCache = closedFromMetrics;
|
|
2186
|
-
return closedFromMetrics;
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
if (this._trustedBooleanInput) {
|
|
2190
|
-
const metrics = this.topologyMetrics();
|
|
2191
|
-
const closedFromMetrics = metrics.boundaryEdges === 0;
|
|
2192
|
-
this._isClosedVolumeCache = closedFromMetrics;
|
|
2193
|
-
return closedFromMetrics;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
ensureInit();
|
|
2197
|
-
if (this.faceCount >= Mesh.TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD) {
|
|
2198
|
-
const metrics = this.topologyMetrics();
|
|
2199
|
-
const closedFromMetrics = metrics.boundaryEdges === 0;
|
|
2200
|
-
this._isClosedVolumeCache = closedFromMetrics;
|
|
2574
|
+
this._isClosedVolumeCache = closedFromMetrics;
|
|
2575
|
+
return closedFromMetrics;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
if (this._trustedBooleanInput) {
|
|
2579
|
+
const metrics = this.topologyMetrics();
|
|
2580
|
+
const closedFromMetrics = metrics.boundaryEdges === 0;
|
|
2581
|
+
this._isClosedVolumeCache = closedFromMetrics;
|
|
2582
|
+
return closedFromMetrics;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
ensureInit();
|
|
2586
|
+
if (this.faceCount >= Mesh.TOPOLOGY_METRICS_CACHE_FACE_THRESHOLD) {
|
|
2587
|
+
const metrics = this.topologyMetrics();
|
|
2588
|
+
const closedFromMetrics = metrics.boundaryEdges === 0;
|
|
2589
|
+
this._isClosedVolumeCache = closedFromMetrics;
|
|
2201
2590
|
return closedFromMetrics;
|
|
2202
2591
|
}
|
|
2203
2592
|
|
|
@@ -2206,30 +2595,30 @@ export class Mesh {
|
|
|
2206
2595
|
return closed;
|
|
2207
2596
|
}
|
|
2208
2597
|
|
|
2209
|
-
/**
|
|
2210
|
-
* Return topology metrics for this triangulated mesh.
|
|
2211
|
-
*
|
|
2212
|
-
* For trusted boolean buffers we preserve raw index topology, which is closer
|
|
2213
|
-
* to MeshGL semantics where same-position seam vertices can stay distinct.
|
|
2214
|
-
* For ordinary triangle soups we continue to use welded-edge metrics.
|
|
2215
|
-
*
|
|
2216
|
-
* `boundaryEdges`: edges referenced by exactly one triangle.
|
|
2217
|
-
* `nonManifoldEdges`: edges referenced by more than two triangles.
|
|
2218
|
-
*/
|
|
2219
|
-
topologyMetrics(): { boundaryEdges: number; nonManifoldEdges: number } {
|
|
2598
|
+
/**
|
|
2599
|
+
* Return topology metrics for this triangulated mesh.
|
|
2600
|
+
*
|
|
2601
|
+
* For trusted boolean buffers we preserve raw index topology, which is closer
|
|
2602
|
+
* to MeshGL semantics where same-position seam vertices can stay distinct.
|
|
2603
|
+
* For ordinary triangle soups we continue to use welded-edge metrics.
|
|
2604
|
+
*
|
|
2605
|
+
* `boundaryEdges`: edges referenced by exactly one triangle.
|
|
2606
|
+
* `nonManifoldEdges`: edges referenced by more than two triangles.
|
|
2607
|
+
*/
|
|
2608
|
+
topologyMetrics(): { boundaryEdges: number; nonManifoldEdges: number } {
|
|
2220
2609
|
if (this._topologyMetricsCache) {
|
|
2221
2610
|
return {
|
|
2222
2611
|
boundaryEdges: this._topologyMetricsCache.boundaryEdges,
|
|
2223
2612
|
nonManifoldEdges: this._topologyMetricsCache.nonManifoldEdges,
|
|
2224
2613
|
};
|
|
2225
2614
|
}
|
|
2226
|
-
|
|
2227
|
-
ensureInit();
|
|
2228
|
-
const metrics = this._trustedBooleanInput
|
|
2229
|
-
? mesh_topology_metrics_raw(this._vertexCount, this._buffer)
|
|
2230
|
-
: mesh_topology_metrics(this._vertexCount, this._buffer);
|
|
2231
|
-
const boundaryEdges = Math.floor(metrics[0] ?? 0);
|
|
2232
|
-
const nonManifoldEdges = Math.floor(metrics[1] ?? 0);
|
|
2615
|
+
|
|
2616
|
+
ensureInit();
|
|
2617
|
+
const metrics = this._trustedBooleanInput
|
|
2618
|
+
? mesh_topology_metrics_raw(this._vertexCount, this._buffer)
|
|
2619
|
+
: mesh_topology_metrics(this._vertexCount, this._buffer);
|
|
2620
|
+
const boundaryEdges = Math.floor(metrics[0] ?? 0);
|
|
2621
|
+
const nonManifoldEdges = Math.floor(metrics[1] ?? 0);
|
|
2233
2622
|
this._topologyMetricsCache = { boundaryEdges, nonManifoldEdges };
|
|
2234
2623
|
if (this._isClosedVolumeCache === null) {
|
|
2235
2624
|
this._isClosedVolumeCache = boundaryEdges === 0;
|