okgeometry-api 1.1.6 → 1.1.7
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/Arc.js +1 -1
- package/dist/Arc.js.map +1 -1
- package/dist/Circle.js +1 -1
- package/dist/Circle.js.map +1 -1
- package/dist/Line.js +1 -1
- package/dist/Line.js.map +1 -1
- package/dist/Mesh.d.ts +101 -4
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +103 -5
- package/dist/Mesh.js.map +1 -1
- package/dist/NurbsCurve.js +1 -1
- package/dist/NurbsCurve.js.map +1 -1
- package/dist/NurbsSurface.d.ts.map +1 -1
- package/dist/NurbsSurface.js +10 -7
- package/dist/NurbsSurface.js.map +1 -1
- package/dist/PolyCurve.js +1 -1
- package/dist/PolyCurve.js.map +1 -1
- package/dist/Polyline.js +1 -1
- package/dist/Polyline.js.map +1 -1
- package/dist/Ray.js +1 -1
- package/dist/Ray.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -3
- package/dist/engine.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/package.json +7 -6
- package/src/Arc.ts +117 -117
- package/src/Circle.ts +153 -153
- package/src/Line.ts +144 -144
- package/src/Mesh.ts +671 -452
- package/src/NurbsCurve.ts +240 -240
- package/src/NurbsSurface.ts +249 -245
- package/src/PolyCurve.ts +306 -306
- package/src/Polyline.ts +153 -153
- package/src/Ray.ts +90 -90
- package/src/engine.ts +9 -11
- package/src/index.ts +6 -0
- package/src/wasm-base64.ts +1 -1
- package/wasm/README.md +0 -104
- package/wasm/okgeometrycore.d.ts +0 -754
- package/wasm/okgeometrycore.js +0 -2005
- package/wasm/okgeometrycore_bg.d.ts +0 -3
- package/wasm/okgeometrycore_bg.js +0 -1686
- package/wasm/okgeometrycore_bg.wasm +0 -0
- package/wasm/okgeometrycore_bg.wasm.d.ts +0 -100
- package/wasm/package.json +0 -19
package/src/Mesh.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
MeshBooleanLimits,
|
|
24
24
|
MeshBooleanOptions,
|
|
25
25
|
} from "./mesh-boolean.protocol.js";
|
|
26
|
-
import * as wasm from "../wasm/
|
|
26
|
+
import * as wasm from "../wasm/okgeometrycore.js";
|
|
27
27
|
import { mesh_topology_metrics } from "../wasm/okgeometrycore.js";
|
|
28
28
|
|
|
29
29
|
export { MeshBooleanExecutionError };
|
|
@@ -47,79 +47,139 @@ export interface PlanarCircle {
|
|
|
47
47
|
radius: number;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export interface PlanarArc {
|
|
51
|
-
points: Point[];
|
|
52
|
-
center: Point;
|
|
53
|
-
radius: number;
|
|
54
|
-
startAngle: number;
|
|
55
|
-
endAngle: number;
|
|
56
|
-
sweepAngle: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface RawMeshBounds {
|
|
60
|
-
minX: number;
|
|
61
|
-
minY: number;
|
|
62
|
-
minZ: number;
|
|
63
|
-
maxX: number;
|
|
64
|
-
maxY: number;
|
|
65
|
-
maxZ: number;
|
|
50
|
+
export interface PlanarArc {
|
|
51
|
+
points: Point[];
|
|
52
|
+
center: Point;
|
|
53
|
+
radius: number;
|
|
54
|
+
startAngle: number;
|
|
55
|
+
endAngle: number;
|
|
56
|
+
sweepAngle: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RawMeshBounds {
|
|
60
|
+
minX: number;
|
|
61
|
+
minY: number;
|
|
62
|
+
minZ: number;
|
|
63
|
+
maxX: number;
|
|
64
|
+
maxY: number;
|
|
65
|
+
maxZ: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type MeshBooleanOperation = "union" | "subtraction" | "intersection";
|
|
69
|
+
|
|
70
|
+
export interface MeshDebugBounds {
|
|
71
|
+
min: Point;
|
|
72
|
+
max: Point;
|
|
73
|
+
center: Point;
|
|
74
|
+
size: Vec3;
|
|
75
|
+
diagonal: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MeshDebugSummary {
|
|
79
|
+
vertexCount: number;
|
|
80
|
+
faceCount: number;
|
|
81
|
+
trustedBooleanInput: boolean;
|
|
82
|
+
isClosedVolume: boolean;
|
|
83
|
+
topology: {
|
|
84
|
+
boundaryEdges: number;
|
|
85
|
+
nonManifoldEdges: number;
|
|
86
|
+
};
|
|
87
|
+
bounds: MeshDebugBounds;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface MeshDebugRayHit {
|
|
91
|
+
point: Point;
|
|
92
|
+
normal: Vec3;
|
|
93
|
+
faceIndex: number;
|
|
94
|
+
distance: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MeshBooleanDebugProbe {
|
|
98
|
+
label: string;
|
|
99
|
+
origin: Point;
|
|
100
|
+
direction: Vec3;
|
|
101
|
+
maxDistance: number;
|
|
102
|
+
inputAHits: MeshDebugRayHit[];
|
|
103
|
+
inputBHits: MeshDebugRayHit[];
|
|
104
|
+
resultHits: MeshDebugRayHit[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type MeshBooleanDebugProbeId =
|
|
108
|
+
"posX" | "negX" | "posY" | "negY" | "posZ" | "negZ";
|
|
109
|
+
|
|
110
|
+
export interface MeshBooleanDebugOptions {
|
|
111
|
+
rayPaddingScale?: number;
|
|
112
|
+
maxRayHits?: number;
|
|
113
|
+
probes?: MeshBooleanDebugProbeId[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface MeshBooleanDebugReport {
|
|
117
|
+
operation: MeshBooleanOperation;
|
|
118
|
+
inputA: MeshDebugSummary;
|
|
119
|
+
inputB: MeshDebugSummary;
|
|
120
|
+
result: Mesh;
|
|
121
|
+
resultSummary: MeshDebugSummary;
|
|
122
|
+
probes: MeshBooleanDebugProbe[];
|
|
66
123
|
}
|
|
67
124
|
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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;
|
|
76
150
|
}
|
|
77
151
|
|
|
78
|
-
export interface
|
|
79
|
-
vertexCount: number;
|
|
80
|
-
faceCount: number;
|
|
152
|
+
export interface MeshBooleanReproOperand {
|
|
81
153
|
trustedBooleanInput: boolean;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
boundaryEdges: number;
|
|
85
|
-
nonManifoldEdges: number;
|
|
86
|
-
};
|
|
87
|
-
bounds: MeshDebugBounds;
|
|
154
|
+
summary: MeshDebugSummary;
|
|
155
|
+
buffer: number[];
|
|
88
156
|
}
|
|
89
157
|
|
|
90
|
-
export interface
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
distance: number;
|
|
158
|
+
export interface MeshBooleanReproResult {
|
|
159
|
+
summary: MeshDebugSummary;
|
|
160
|
+
buffer: number[];
|
|
161
|
+
probeSummary?: MeshBooleanDebugProbeSummary[];
|
|
95
162
|
}
|
|
96
163
|
|
|
97
|
-
export interface
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
maxDistance: number;
|
|
102
|
-
inputAHits: MeshDebugRayHit[];
|
|
103
|
-
inputBHits: MeshDebugRayHit[];
|
|
104
|
-
resultHits: MeshDebugRayHit[];
|
|
164
|
+
export interface MeshBooleanReproError {
|
|
165
|
+
name: string;
|
|
166
|
+
message: string;
|
|
167
|
+
stack?: string;
|
|
105
168
|
}
|
|
106
169
|
|
|
107
|
-
export
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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;
|
|
114
178
|
}
|
|
115
179
|
|
|
116
|
-
export interface
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
inputB: MeshDebugSummary;
|
|
120
|
-
result: Mesh;
|
|
121
|
-
resultSummary: MeshDebugSummary;
|
|
122
|
-
probes: MeshBooleanDebugProbe[];
|
|
180
|
+
export interface MeshBooleanReproOptions {
|
|
181
|
+
includeResult?: boolean;
|
|
182
|
+
debugOptions?: MeshBooleanDebugOptions;
|
|
123
183
|
}
|
|
124
184
|
|
|
125
185
|
/**
|
|
@@ -128,18 +188,18 @@ export interface MeshBooleanDebugReport {
|
|
|
128
188
|
*
|
|
129
189
|
* Buffer format: [vertexCount, x1,y1,z1, ..., i1,i2,i3, ...]
|
|
130
190
|
*/
|
|
131
|
-
export class Mesh {
|
|
132
|
-
private _buffer: Float64Array;
|
|
133
|
-
private _vertexCount: number;
|
|
134
|
-
private _trustedBooleanInput: boolean;
|
|
135
|
-
private static readonly DEFAULT_BOOLEAN_DEBUG_PROBES: MeshBooleanDebugProbeId[] = [
|
|
136
|
-
"posX",
|
|
137
|
-
"negX",
|
|
138
|
-
"posY",
|
|
139
|
-
"negY",
|
|
140
|
-
"posZ",
|
|
141
|
-
"negZ",
|
|
142
|
-
];
|
|
191
|
+
export class Mesh {
|
|
192
|
+
private _buffer: Float64Array;
|
|
193
|
+
private _vertexCount: number;
|
|
194
|
+
private _trustedBooleanInput: boolean;
|
|
195
|
+
private static readonly DEFAULT_BOOLEAN_DEBUG_PROBES: MeshBooleanDebugProbeId[] = [
|
|
196
|
+
"posX",
|
|
197
|
+
"negX",
|
|
198
|
+
"posY",
|
|
199
|
+
"negY",
|
|
200
|
+
"posZ",
|
|
201
|
+
"negZ",
|
|
202
|
+
];
|
|
143
203
|
|
|
144
204
|
private static readonly DEFAULT_BOOLEAN_LIMITS: MeshBooleanLimits = {
|
|
145
205
|
maxInputFacesPerMesh: 120_000,
|
|
@@ -242,322 +302,377 @@ export class Mesh {
|
|
|
242
302
|
return Number.isFinite(diag) && diag > 0 ? diag : 1;
|
|
243
303
|
}
|
|
244
304
|
|
|
245
|
-
private static booleanContactTolerance(a: RawMeshBounds | null, b: RawMeshBounds | null): number {
|
|
246
|
-
const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
|
|
247
|
-
return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private static toDebugBounds(bounds: { min: Point; max: Point }): MeshDebugBounds {
|
|
251
|
-
const size = new Vec3(
|
|
252
|
-
bounds.max.x - bounds.min.x,
|
|
253
|
-
bounds.max.y - bounds.min.y,
|
|
254
|
-
bounds.max.z - bounds.min.z,
|
|
255
|
-
);
|
|
305
|
+
private static booleanContactTolerance(a: RawMeshBounds | null, b: RawMeshBounds | null): number {
|
|
306
|
+
const scale = Math.max(Mesh.boundsDiag(a), Mesh.boundsDiag(b));
|
|
307
|
+
return Math.min(1e-4, Math.max(1e-9, scale * 1e-6));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private static toDebugBounds(bounds: { min: Point; max: Point }): MeshDebugBounds {
|
|
311
|
+
const size = new Vec3(
|
|
312
|
+
bounds.max.x - bounds.min.x,
|
|
313
|
+
bounds.max.y - bounds.min.y,
|
|
314
|
+
bounds.max.z - bounds.min.z,
|
|
315
|
+
);
|
|
316
|
+
return {
|
|
317
|
+
min: bounds.min,
|
|
318
|
+
max: bounds.max,
|
|
319
|
+
center: new Point(
|
|
320
|
+
(bounds.min.x + bounds.max.x) * 0.5,
|
|
321
|
+
(bounds.min.y + bounds.max.y) * 0.5,
|
|
322
|
+
(bounds.min.z + bounds.max.z) * 0.5,
|
|
323
|
+
),
|
|
324
|
+
size,
|
|
325
|
+
diagonal: Math.hypot(size.x, size.y, size.z),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private static clampDebugHitCount(count?: number): number {
|
|
330
|
+
if (!Number.isFinite(count)) return 4;
|
|
331
|
+
return Math.max(1, Math.min(32, Math.floor(count ?? 4)));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private static collectDebugHits(
|
|
335
|
+
mesh: Mesh,
|
|
336
|
+
origin: Point,
|
|
337
|
+
direction: Vec3,
|
|
338
|
+
maxDistance: number,
|
|
339
|
+
maxHits: number,
|
|
340
|
+
): MeshDebugRayHit[] {
|
|
341
|
+
return mesh.raycastAll(origin, direction, maxDistance).slice(0, maxHits).map((hit) => ({
|
|
342
|
+
point: hit.point,
|
|
343
|
+
normal: hit.normal,
|
|
344
|
+
faceIndex: hit.faceIndex,
|
|
345
|
+
distance: hit.distance,
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private static uniqueDebugProbeIds(
|
|
350
|
+
probeIds: MeshBooleanDebugProbeId[],
|
|
351
|
+
): MeshBooleanDebugProbeId[] {
|
|
352
|
+
return [...new Set(probeIds)];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private static buildInputBProbeOriginOffsets(
|
|
356
|
+
jitter: number,
|
|
357
|
+
): Record<MeshBooleanDebugProbeId, Vec3> {
|
|
358
|
+
const major = jitter;
|
|
359
|
+
const minor = jitter * 0.61803398875;
|
|
360
|
+
const micro = jitter * 0.38196601125;
|
|
361
|
+
return {
|
|
362
|
+
posX: new Vec3(0, minor, -major),
|
|
363
|
+
negX: new Vec3(0, -minor, major),
|
|
364
|
+
posY: new Vec3(major, 0, micro),
|
|
365
|
+
negY: new Vec3(-major, 0, -micro),
|
|
366
|
+
posZ: new Vec3(major, -minor, 0),
|
|
367
|
+
negZ: new Vec3(-major, minor, 0),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private static buildDirectionalDebugProbes(
|
|
372
|
+
inputA: Mesh,
|
|
373
|
+
inputB: Mesh,
|
|
374
|
+
result: Mesh,
|
|
375
|
+
center: Point,
|
|
376
|
+
pad: number,
|
|
377
|
+
maxDistance: number,
|
|
378
|
+
maxHits: number,
|
|
379
|
+
probeIds: MeshBooleanDebugProbeId[],
|
|
380
|
+
labelPrefix = "",
|
|
381
|
+
originOffsets?: Partial<Record<MeshBooleanDebugProbeId, Vec3>>,
|
|
382
|
+
): MeshBooleanDebugProbe[] {
|
|
383
|
+
const prefixed = (id: string) => (labelPrefix ? `${labelPrefix}:${id}` : id);
|
|
384
|
+
const probeSpecs: Record<
|
|
385
|
+
MeshBooleanDebugProbeId,
|
|
386
|
+
{ origin: Point; direction: Vec3 }
|
|
387
|
+
> = {
|
|
388
|
+
posX: {
|
|
389
|
+
origin: new Point(center.x + pad, center.y, center.z),
|
|
390
|
+
direction: new Vec3(-1, 0, 0),
|
|
391
|
+
},
|
|
392
|
+
negX: {
|
|
393
|
+
origin: new Point(center.x - pad, center.y, center.z),
|
|
394
|
+
direction: new Vec3(1, 0, 0),
|
|
395
|
+
},
|
|
396
|
+
posY: {
|
|
397
|
+
origin: new Point(center.x, center.y + pad, center.z),
|
|
398
|
+
direction: new Vec3(0, -1, 0),
|
|
399
|
+
},
|
|
400
|
+
negY: {
|
|
401
|
+
origin: new Point(center.x, center.y - pad, center.z),
|
|
402
|
+
direction: new Vec3(0, 1, 0),
|
|
403
|
+
},
|
|
404
|
+
posZ: {
|
|
405
|
+
origin: new Point(center.x, center.y, center.z + pad),
|
|
406
|
+
direction: new Vec3(0, 0, -1),
|
|
407
|
+
},
|
|
408
|
+
negZ: {
|
|
409
|
+
origin: new Point(center.x, center.y, center.z - pad),
|
|
410
|
+
direction: new Vec3(0, 0, 1),
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
return probeIds.map((id) => {
|
|
415
|
+
const spec = probeSpecs[id];
|
|
416
|
+
const originOffset = originOffsets?.[id];
|
|
417
|
+
const origin = originOffset
|
|
418
|
+
? new Point(
|
|
419
|
+
spec.origin.x + originOffset.x,
|
|
420
|
+
spec.origin.y + originOffset.y,
|
|
421
|
+
spec.origin.z + originOffset.z,
|
|
422
|
+
)
|
|
423
|
+
: spec.origin;
|
|
424
|
+
return {
|
|
425
|
+
label: prefixed(id),
|
|
426
|
+
origin,
|
|
427
|
+
direction: spec.direction,
|
|
428
|
+
maxDistance,
|
|
429
|
+
inputAHits: Mesh.collectDebugHits(inputA, origin, spec.direction, maxDistance, maxHits),
|
|
430
|
+
inputBHits: Mesh.collectDebugHits(inputB, origin, spec.direction, maxDistance, maxHits),
|
|
431
|
+
resultHits: Mesh.collectDebugHits(result, origin, spec.direction, maxDistance, maxHits),
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private static buildBooleanDebugProbes(
|
|
437
|
+
inputA: Mesh,
|
|
438
|
+
inputB: Mesh,
|
|
439
|
+
result: Mesh,
|
|
440
|
+
options?: MeshBooleanDebugOptions,
|
|
441
|
+
): MeshBooleanDebugProbe[] {
|
|
442
|
+
const boundsA = Mesh.toDebugBounds(inputA.getBounds());
|
|
443
|
+
const boundsB = Mesh.toDebugBounds(inputB.getBounds());
|
|
444
|
+
const resultBounds = Mesh.toDebugBounds(result.getBounds());
|
|
445
|
+
const min = new Point(
|
|
446
|
+
Math.min(boundsA.min.x, boundsB.min.x, resultBounds.min.x),
|
|
447
|
+
Math.min(boundsA.min.y, boundsB.min.y, resultBounds.min.y),
|
|
448
|
+
Math.min(boundsA.min.z, boundsB.min.z, resultBounds.min.z),
|
|
449
|
+
);
|
|
450
|
+
const max = new Point(
|
|
451
|
+
Math.max(boundsA.max.x, boundsB.max.x, resultBounds.max.x),
|
|
452
|
+
Math.max(boundsA.max.y, boundsB.max.y, resultBounds.max.y),
|
|
453
|
+
Math.max(boundsA.max.z, boundsB.max.z, resultBounds.max.z),
|
|
454
|
+
);
|
|
455
|
+
const center = new Point(
|
|
456
|
+
(min.x + max.x) * 0.5,
|
|
457
|
+
(min.y + max.y) * 0.5,
|
|
458
|
+
(min.z + max.z) * 0.5,
|
|
459
|
+
);
|
|
460
|
+
const diag = Math.max(
|
|
461
|
+
1e-6,
|
|
462
|
+
Math.hypot(max.x - min.x, max.y - min.y, max.z - min.z),
|
|
463
|
+
);
|
|
464
|
+
const padScale = Number.isFinite(options?.rayPaddingScale)
|
|
465
|
+
? Math.max(0.01, options?.rayPaddingScale ?? 0.25)
|
|
466
|
+
: 0.25;
|
|
467
|
+
const pad = Math.max(1e-3, diag * padScale);
|
|
468
|
+
const maxHits = Mesh.clampDebugHitCount(options?.maxRayHits);
|
|
469
|
+
const sceneProbeIds = Mesh.uniqueDebugProbeIds(
|
|
470
|
+
options?.probes?.length
|
|
471
|
+
? options.probes
|
|
472
|
+
: Mesh.DEFAULT_BOOLEAN_DEBUG_PROBES,
|
|
473
|
+
);
|
|
474
|
+
const inputBProbeIds = Mesh.uniqueDebugProbeIds(sceneProbeIds.concat(["posZ", "negZ"]));
|
|
475
|
+
const inputBJitter = Math.max(1e-6, Math.min(pad * 0.1, boundsB.diagonal * 0.01));
|
|
476
|
+
const maxDistance = diag + pad * 2;
|
|
477
|
+
const sceneCenterProbes = Mesh.buildDirectionalDebugProbes(
|
|
478
|
+
inputA,
|
|
479
|
+
inputB,
|
|
480
|
+
result,
|
|
481
|
+
center,
|
|
482
|
+
pad,
|
|
483
|
+
maxDistance,
|
|
484
|
+
maxHits,
|
|
485
|
+
sceneProbeIds,
|
|
486
|
+
);
|
|
487
|
+
const inputBCenterProbes = Mesh.buildDirectionalDebugProbes(
|
|
488
|
+
inputA,
|
|
489
|
+
inputB,
|
|
490
|
+
result,
|
|
491
|
+
boundsB.center,
|
|
492
|
+
pad,
|
|
493
|
+
maxDistance,
|
|
494
|
+
maxHits,
|
|
495
|
+
inputBProbeIds,
|
|
496
|
+
"inputB",
|
|
497
|
+
Mesh.buildInputBProbeOriginOffsets(inputBJitter),
|
|
498
|
+
);
|
|
499
|
+
return sceneCenterProbes.concat(inputBCenterProbes);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private static createBooleanDebugReport(
|
|
503
|
+
inputA: Mesh,
|
|
504
|
+
inputB: Mesh,
|
|
505
|
+
result: Mesh,
|
|
506
|
+
operation: MeshBooleanOperation,
|
|
507
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
508
|
+
): MeshBooleanDebugReport {
|
|
509
|
+
return {
|
|
510
|
+
operation,
|
|
511
|
+
inputA: inputA.debugSummary(),
|
|
512
|
+
inputB: inputB.debugSummary(),
|
|
513
|
+
result,
|
|
514
|
+
resultSummary: result.debugSummary(),
|
|
515
|
+
probes: Mesh.buildBooleanDebugProbes(inputA, inputB, result, debugOptions),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private static roundDebugScalar(value: number): number {
|
|
520
|
+
return Number.isFinite(value) ? Number(value.toFixed(4)) : value;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private static summarizeDebugHit(hit: MeshDebugRayHit | undefined): {
|
|
524
|
+
point: { x: number; y: number; z: number };
|
|
525
|
+
normal: { x: number; y: number; z: number };
|
|
526
|
+
faceIndex: number;
|
|
527
|
+
distance: number;
|
|
528
|
+
} | null {
|
|
529
|
+
if (!hit) return null;
|
|
530
|
+
return {
|
|
531
|
+
point: {
|
|
532
|
+
x: Mesh.roundDebugScalar(hit.point.x),
|
|
533
|
+
y: Mesh.roundDebugScalar(hit.point.y),
|
|
534
|
+
z: Mesh.roundDebugScalar(hit.point.z),
|
|
535
|
+
},
|
|
536
|
+
normal: {
|
|
537
|
+
x: Mesh.roundDebugScalar(hit.normal.x),
|
|
538
|
+
y: Mesh.roundDebugScalar(hit.normal.y),
|
|
539
|
+
z: Mesh.roundDebugScalar(hit.normal.z),
|
|
540
|
+
},
|
|
541
|
+
faceIndex: hit.faceIndex,
|
|
542
|
+
distance: Mesh.roundDebugScalar(hit.distance),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private static summarizeDebugProbe(probe: MeshBooleanDebugProbe): MeshBooleanDebugProbeSummary {
|
|
256
547
|
return {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
(
|
|
261
|
-
(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
548
|
+
label: probe.label,
|
|
549
|
+
origin: {
|
|
550
|
+
x: Mesh.roundDebugScalar(probe.origin.x),
|
|
551
|
+
y: Mesh.roundDebugScalar(probe.origin.y),
|
|
552
|
+
z: Mesh.roundDebugScalar(probe.origin.z),
|
|
553
|
+
},
|
|
554
|
+
direction: {
|
|
555
|
+
x: Mesh.roundDebugScalar(probe.direction.x),
|
|
556
|
+
y: Mesh.roundDebugScalar(probe.direction.y),
|
|
557
|
+
z: Mesh.roundDebugScalar(probe.direction.z),
|
|
558
|
+
},
|
|
559
|
+
inputAFirst: Mesh.summarizeDebugHit(probe.inputAHits[0]),
|
|
560
|
+
inputBFirst: Mesh.summarizeDebugHit(probe.inputBHits[0]),
|
|
561
|
+
resultFirst: Mesh.summarizeDebugHit(probe.resultHits[0]),
|
|
562
|
+
inputAHits: probe.inputAHits.length,
|
|
563
|
+
inputBHits: probe.inputBHits.length,
|
|
564
|
+
resultHits: probe.resultHits.length,
|
|
266
565
|
};
|
|
267
566
|
}
|
|
268
567
|
|
|
269
|
-
private static
|
|
270
|
-
|
|
271
|
-
return Math.max(1, Math.min(32, Math.floor(count ?? 4)));
|
|
568
|
+
private static serializeBooleanOptions(options?: MeshBooleanOptions): MeshBooleanOptions | null {
|
|
569
|
+
return options == null ? null : { ...options };
|
|
272
570
|
}
|
|
273
571
|
|
|
274
|
-
private static
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
point: hit.point,
|
|
283
|
-
normal: hit.normal,
|
|
284
|
-
faceIndex: hit.faceIndex,
|
|
285
|
-
distance: hit.distance,
|
|
286
|
-
}));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private static uniqueDebugProbeIds(
|
|
290
|
-
probeIds: MeshBooleanDebugProbeId[],
|
|
291
|
-
): MeshBooleanDebugProbeId[] {
|
|
292
|
-
return [...new Set(probeIds)];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
private static buildInputBProbeOriginOffsets(
|
|
296
|
-
jitter: number,
|
|
297
|
-
): Record<MeshBooleanDebugProbeId, Vec3> {
|
|
298
|
-
const major = jitter;
|
|
299
|
-
const minor = jitter * 0.61803398875;
|
|
300
|
-
const micro = jitter * 0.38196601125;
|
|
572
|
+
private static serializeBooleanError(error: unknown): MeshBooleanReproError {
|
|
573
|
+
if (error instanceof Error) {
|
|
574
|
+
return {
|
|
575
|
+
name: error.name,
|
|
576
|
+
message: error.message,
|
|
577
|
+
stack: error.stack,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
301
580
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
posY: new Vec3(major, 0, micro),
|
|
305
|
-
negY: new Vec3(-major, 0, -micro),
|
|
306
|
-
posZ: new Vec3(major, -minor, 0),
|
|
307
|
-
negZ: new Vec3(-major, minor, 0),
|
|
581
|
+
name: typeof error,
|
|
582
|
+
message: typeof error === "string" ? error : JSON.stringify(error),
|
|
308
583
|
};
|
|
309
584
|
}
|
|
310
585
|
|
|
311
|
-
private static
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
pad: number,
|
|
317
|
-
maxDistance: number,
|
|
318
|
-
maxHits: number,
|
|
319
|
-
probeIds: MeshBooleanDebugProbeId[],
|
|
320
|
-
labelPrefix = "",
|
|
321
|
-
originOffsets?: Partial<Record<MeshBooleanDebugProbeId, Vec3>>,
|
|
322
|
-
): MeshBooleanDebugProbe[] {
|
|
323
|
-
const prefixed = (id: string) => (labelPrefix ? `${labelPrefix}:${id}` : id);
|
|
324
|
-
const probeSpecs: Record<
|
|
325
|
-
MeshBooleanDebugProbeId,
|
|
326
|
-
{ origin: Point; direction: Vec3 }
|
|
327
|
-
> = {
|
|
328
|
-
posX: {
|
|
329
|
-
origin: new Point(center.x + pad, center.y, center.z),
|
|
330
|
-
direction: new Vec3(-1, 0, 0),
|
|
331
|
-
},
|
|
332
|
-
negX: {
|
|
333
|
-
origin: new Point(center.x - pad, center.y, center.z),
|
|
334
|
-
direction: new Vec3(1, 0, 0),
|
|
335
|
-
},
|
|
336
|
-
posY: {
|
|
337
|
-
origin: new Point(center.x, center.y + pad, center.z),
|
|
338
|
-
direction: new Vec3(0, -1, 0),
|
|
339
|
-
},
|
|
340
|
-
negY: {
|
|
341
|
-
origin: new Point(center.x, center.y - pad, center.z),
|
|
342
|
-
direction: new Vec3(0, 1, 0),
|
|
343
|
-
},
|
|
344
|
-
posZ: {
|
|
345
|
-
origin: new Point(center.x, center.y, center.z + pad),
|
|
346
|
-
direction: new Vec3(0, 0, -1),
|
|
347
|
-
},
|
|
348
|
-
negZ: {
|
|
349
|
-
origin: new Point(center.x, center.y, center.z - pad),
|
|
350
|
-
direction: new Vec3(0, 0, 1),
|
|
351
|
-
},
|
|
586
|
+
private static exportBooleanReproOperand(mesh: Mesh): MeshBooleanReproOperand {
|
|
587
|
+
return {
|
|
588
|
+
trustedBooleanInput: mesh._trustedBooleanInput,
|
|
589
|
+
summary: mesh.debugSummary(),
|
|
590
|
+
buffer: Array.from(mesh._buffer),
|
|
352
591
|
};
|
|
353
|
-
|
|
354
|
-
return probeIds.map((id) => {
|
|
355
|
-
const spec = probeSpecs[id];
|
|
356
|
-
const originOffset = originOffsets?.[id];
|
|
357
|
-
const origin = originOffset
|
|
358
|
-
? new Point(
|
|
359
|
-
spec.origin.x + originOffset.x,
|
|
360
|
-
spec.origin.y + originOffset.y,
|
|
361
|
-
spec.origin.z + originOffset.z,
|
|
362
|
-
)
|
|
363
|
-
: spec.origin;
|
|
364
|
-
return {
|
|
365
|
-
label: prefixed(id),
|
|
366
|
-
origin,
|
|
367
|
-
direction: spec.direction,
|
|
368
|
-
maxDistance,
|
|
369
|
-
inputAHits: Mesh.collectDebugHits(inputA, origin, spec.direction, maxDistance, maxHits),
|
|
370
|
-
inputBHits: Mesh.collectDebugHits(inputB, origin, spec.direction, maxDistance, maxHits),
|
|
371
|
-
resultHits: Mesh.collectDebugHits(result, origin, spec.direction, maxDistance, maxHits),
|
|
372
|
-
};
|
|
373
|
-
});
|
|
374
592
|
}
|
|
375
593
|
|
|
376
|
-
private static
|
|
594
|
+
private static createBooleanReproPayload(
|
|
377
595
|
inputA: Mesh,
|
|
378
596
|
inputB: Mesh,
|
|
379
|
-
result: Mesh,
|
|
380
|
-
options?: MeshBooleanDebugOptions,
|
|
381
|
-
): MeshBooleanDebugProbe[] {
|
|
382
|
-
const boundsA = Mesh.toDebugBounds(inputA.getBounds());
|
|
383
|
-
const boundsB = Mesh.toDebugBounds(inputB.getBounds());
|
|
384
|
-
const resultBounds = Mesh.toDebugBounds(result.getBounds());
|
|
385
|
-
const min = new Point(
|
|
386
|
-
Math.min(boundsA.min.x, boundsB.min.x, resultBounds.min.x),
|
|
387
|
-
Math.min(boundsA.min.y, boundsB.min.y, resultBounds.min.y),
|
|
388
|
-
Math.min(boundsA.min.z, boundsB.min.z, resultBounds.min.z),
|
|
389
|
-
);
|
|
390
|
-
const max = new Point(
|
|
391
|
-
Math.max(boundsA.max.x, boundsB.max.x, resultBounds.max.x),
|
|
392
|
-
Math.max(boundsA.max.y, boundsB.max.y, resultBounds.max.y),
|
|
393
|
-
Math.max(boundsA.max.z, boundsB.max.z, resultBounds.max.z),
|
|
394
|
-
);
|
|
395
|
-
const center = new Point(
|
|
396
|
-
(min.x + max.x) * 0.5,
|
|
397
|
-
(min.y + max.y) * 0.5,
|
|
398
|
-
(min.z + max.z) * 0.5,
|
|
399
|
-
);
|
|
400
|
-
const diag = Math.max(
|
|
401
|
-
1e-6,
|
|
402
|
-
Math.hypot(max.x - min.x, max.y - min.y, max.z - min.z),
|
|
403
|
-
);
|
|
404
|
-
const padScale = Number.isFinite(options?.rayPaddingScale)
|
|
405
|
-
? Math.max(0.01, options?.rayPaddingScale ?? 0.25)
|
|
406
|
-
: 0.25;
|
|
407
|
-
const pad = Math.max(1e-3, diag * padScale);
|
|
408
|
-
const maxHits = Mesh.clampDebugHitCount(options?.maxRayHits);
|
|
409
|
-
const sceneProbeIds = Mesh.uniqueDebugProbeIds(
|
|
410
|
-
options?.probes?.length
|
|
411
|
-
? options.probes
|
|
412
|
-
: Mesh.DEFAULT_BOOLEAN_DEBUG_PROBES,
|
|
413
|
-
);
|
|
414
|
-
const inputBProbeIds = Mesh.uniqueDebugProbeIds(sceneProbeIds.concat(["posZ", "negZ"]));
|
|
415
|
-
const inputBJitter = Math.max(1e-6, Math.min(pad * 0.1, boundsB.diagonal * 0.01));
|
|
416
|
-
const maxDistance = diag + pad * 2;
|
|
417
|
-
const sceneCenterProbes = Mesh.buildDirectionalDebugProbes(
|
|
418
|
-
inputA,
|
|
419
|
-
inputB,
|
|
420
|
-
result,
|
|
421
|
-
center,
|
|
422
|
-
pad,
|
|
423
|
-
maxDistance,
|
|
424
|
-
maxHits,
|
|
425
|
-
sceneProbeIds,
|
|
426
|
-
);
|
|
427
|
-
const inputBCenterProbes = Mesh.buildDirectionalDebugProbes(
|
|
428
|
-
inputA,
|
|
429
|
-
inputB,
|
|
430
|
-
result,
|
|
431
|
-
boundsB.center,
|
|
432
|
-
pad,
|
|
433
|
-
maxDistance,
|
|
434
|
-
maxHits,
|
|
435
|
-
inputBProbeIds,
|
|
436
|
-
"inputB",
|
|
437
|
-
Mesh.buildInputBProbeOriginOffsets(inputBJitter),
|
|
438
|
-
);
|
|
439
|
-
return sceneCenterProbes.concat(inputBCenterProbes);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
private static createBooleanDebugReport(
|
|
443
|
-
inputA: Mesh,
|
|
444
|
-
inputB: Mesh,
|
|
445
|
-
result: Mesh,
|
|
446
597
|
operation: MeshBooleanOperation,
|
|
598
|
+
options?: MeshBooleanOptions,
|
|
599
|
+
result?: Mesh,
|
|
447
600
|
debugOptions?: MeshBooleanDebugOptions,
|
|
448
|
-
|
|
449
|
-
|
|
601
|
+
error?: unknown,
|
|
602
|
+
): MeshBooleanReproPayload {
|
|
603
|
+
const payload: MeshBooleanReproPayload = {
|
|
604
|
+
version: 1,
|
|
450
605
|
operation,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
resultSummary: result.debugSummary(),
|
|
455
|
-
probes: Mesh.buildBooleanDebugProbes(inputA, inputB, result, debugOptions),
|
|
606
|
+
options: Mesh.serializeBooleanOptions(options),
|
|
607
|
+
inputA: Mesh.exportBooleanReproOperand(inputA),
|
|
608
|
+
inputB: Mesh.exportBooleanReproOperand(inputB),
|
|
456
609
|
};
|
|
457
|
-
}
|
|
458
610
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
611
|
+
if (result) {
|
|
612
|
+
payload.result = {
|
|
613
|
+
summary: result.debugSummary(),
|
|
614
|
+
buffer: Array.from(result._buffer),
|
|
615
|
+
};
|
|
616
|
+
if (debugOptions) {
|
|
617
|
+
try {
|
|
618
|
+
const report = Mesh.createBooleanDebugReport(inputA, inputB, result, operation, debugOptions);
|
|
619
|
+
payload.result.probeSummary = report.probes.map((probe) => Mesh.summarizeDebugProbe(probe));
|
|
620
|
+
} catch {
|
|
621
|
+
// Keep the repro payload usable even when extra debug probes fail.
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
462
625
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
faceIndex: number;
|
|
467
|
-
distance: number;
|
|
468
|
-
} | null {
|
|
469
|
-
if (!hit) return null;
|
|
470
|
-
return {
|
|
471
|
-
point: {
|
|
472
|
-
x: Mesh.roundDebugScalar(hit.point.x),
|
|
473
|
-
y: Mesh.roundDebugScalar(hit.point.y),
|
|
474
|
-
z: Mesh.roundDebugScalar(hit.point.z),
|
|
475
|
-
},
|
|
476
|
-
normal: {
|
|
477
|
-
x: Mesh.roundDebugScalar(hit.normal.x),
|
|
478
|
-
y: Mesh.roundDebugScalar(hit.normal.y),
|
|
479
|
-
z: Mesh.roundDebugScalar(hit.normal.z),
|
|
480
|
-
},
|
|
481
|
-
faceIndex: hit.faceIndex,
|
|
482
|
-
distance: Mesh.roundDebugScalar(hit.distance),
|
|
483
|
-
};
|
|
484
|
-
}
|
|
626
|
+
if (error !== undefined) {
|
|
627
|
+
payload.error = Mesh.serializeBooleanError(error);
|
|
628
|
+
}
|
|
485
629
|
|
|
486
|
-
|
|
487
|
-
label: string;
|
|
488
|
-
origin: { x: number; y: number; z: number };
|
|
489
|
-
direction: { x: number; y: number; z: number };
|
|
490
|
-
inputAFirst: ReturnType<typeof Mesh.summarizeDebugHit>;
|
|
491
|
-
inputBFirst: ReturnType<typeof Mesh.summarizeDebugHit>;
|
|
492
|
-
resultFirst: ReturnType<typeof Mesh.summarizeDebugHit>;
|
|
493
|
-
inputAHits: number;
|
|
494
|
-
inputBHits: number;
|
|
495
|
-
resultHits: number;
|
|
496
|
-
} {
|
|
497
|
-
return {
|
|
498
|
-
label: probe.label,
|
|
499
|
-
origin: {
|
|
500
|
-
x: Mesh.roundDebugScalar(probe.origin.x),
|
|
501
|
-
y: Mesh.roundDebugScalar(probe.origin.y),
|
|
502
|
-
z: Mesh.roundDebugScalar(probe.origin.z),
|
|
503
|
-
},
|
|
504
|
-
direction: {
|
|
505
|
-
x: Mesh.roundDebugScalar(probe.direction.x),
|
|
506
|
-
y: Mesh.roundDebugScalar(probe.direction.y),
|
|
507
|
-
z: Mesh.roundDebugScalar(probe.direction.z),
|
|
508
|
-
},
|
|
509
|
-
inputAFirst: Mesh.summarizeDebugHit(probe.inputAHits[0]),
|
|
510
|
-
inputBFirst: Mesh.summarizeDebugHit(probe.inputBHits[0]),
|
|
511
|
-
resultFirst: Mesh.summarizeDebugHit(probe.resultHits[0]),
|
|
512
|
-
inputAHits: probe.inputAHits.length,
|
|
513
|
-
inputBHits: probe.inputBHits.length,
|
|
514
|
-
resultHits: probe.resultHits.length,
|
|
515
|
-
};
|
|
630
|
+
return payload;
|
|
516
631
|
}
|
|
517
632
|
|
|
518
633
|
private static logSubtractDebugSuccess(
|
|
519
634
|
inputA: Mesh,
|
|
520
635
|
inputB: Mesh,
|
|
521
|
-
result: Mesh,
|
|
522
|
-
options?: MeshBooleanOptions,
|
|
523
|
-
): void {
|
|
524
|
-
try {
|
|
525
|
-
const report = Mesh.createBooleanDebugReport(
|
|
526
|
-
inputA,
|
|
527
|
-
inputB,
|
|
528
|
-
result,
|
|
529
|
-
"subtraction",
|
|
530
|
-
{ maxRayHits: 2, probes: ["posX", "negX", "posY"] },
|
|
531
|
-
);
|
|
532
|
-
const probeSummary = report.probes.map((probe) => Mesh.summarizeDebugProbe(probe));
|
|
533
|
-
console.log("[okgeometry-api] Mesh.subtract debug", {
|
|
534
|
-
options: options ?? null,
|
|
535
|
-
inputA: report.inputA,
|
|
536
|
-
inputB: report.inputB,
|
|
537
|
-
result: report.resultSummary,
|
|
538
|
-
deltas: {
|
|
539
|
-
faceCount: report.resultSummary.faceCount - report.inputA.faceCount,
|
|
540
|
-
vertexCount: report.resultSummary.vertexCount - report.inputA.vertexCount,
|
|
541
|
-
},
|
|
542
|
-
probeSummary,
|
|
543
|
-
report,
|
|
544
|
-
});
|
|
545
|
-
console.log("[okgeometry-api] Mesh.subtract summary", {
|
|
546
|
-
inputAFaces: report.inputA.faceCount,
|
|
547
|
-
inputBFaces: report.inputB.faceCount,
|
|
548
|
-
resultFaces: report.resultSummary.faceCount,
|
|
549
|
-
inputAVerts: report.inputA.vertexCount,
|
|
550
|
-
inputBVerts: report.inputB.vertexCount,
|
|
551
|
-
resultVerts: report.resultSummary.vertexCount,
|
|
552
|
-
resultClosed: report.resultSummary.isClosedVolume,
|
|
553
|
-
resultTopology: report.resultSummary.topology,
|
|
554
|
-
});
|
|
555
|
-
console.log("[okgeometry-api] Mesh.subtract probeSummary", probeSummary);
|
|
556
|
-
} catch (debugError) {
|
|
557
|
-
console.error("[okgeometry-api] Mesh.subtract debug logging failed", debugError);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
636
|
+
result: Mesh,
|
|
637
|
+
options?: MeshBooleanOptions,
|
|
638
|
+
): void {
|
|
639
|
+
try {
|
|
640
|
+
const report = Mesh.createBooleanDebugReport(
|
|
641
|
+
inputA,
|
|
642
|
+
inputB,
|
|
643
|
+
result,
|
|
644
|
+
"subtraction",
|
|
645
|
+
{ maxRayHits: 2, probes: ["posX", "negX", "posY"] },
|
|
646
|
+
);
|
|
647
|
+
const probeSummary = report.probes.map((probe) => Mesh.summarizeDebugProbe(probe));
|
|
648
|
+
console.log("[okgeometry-api] Mesh.subtract debug", {
|
|
649
|
+
options: options ?? null,
|
|
650
|
+
inputA: report.inputA,
|
|
651
|
+
inputB: report.inputB,
|
|
652
|
+
result: report.resultSummary,
|
|
653
|
+
deltas: {
|
|
654
|
+
faceCount: report.resultSummary.faceCount - report.inputA.faceCount,
|
|
655
|
+
vertexCount: report.resultSummary.vertexCount - report.inputA.vertexCount,
|
|
656
|
+
},
|
|
657
|
+
probeSummary,
|
|
658
|
+
report,
|
|
659
|
+
});
|
|
660
|
+
console.log("[okgeometry-api] Mesh.subtract summary", {
|
|
661
|
+
inputAFaces: report.inputA.faceCount,
|
|
662
|
+
inputBFaces: report.inputB.faceCount,
|
|
663
|
+
resultFaces: report.resultSummary.faceCount,
|
|
664
|
+
inputAVerts: report.inputA.vertexCount,
|
|
665
|
+
inputBVerts: report.inputB.vertexCount,
|
|
666
|
+
resultVerts: report.resultSummary.vertexCount,
|
|
667
|
+
resultClosed: report.resultSummary.isClosedVolume,
|
|
668
|
+
resultTopology: report.resultSummary.topology,
|
|
669
|
+
});
|
|
670
|
+
console.log("[okgeometry-api] Mesh.subtract probeSummary", probeSummary);
|
|
671
|
+
} catch (debugError) {
|
|
672
|
+
console.error("[okgeometry-api] Mesh.subtract debug logging failed", debugError);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
561
676
|
private static logSubtractDebugFailure(
|
|
562
677
|
inputA: Mesh,
|
|
563
678
|
inputB: Mesh,
|
|
@@ -565,12 +680,23 @@ export class Mesh {
|
|
|
565
680
|
error: unknown,
|
|
566
681
|
): void {
|
|
567
682
|
try {
|
|
683
|
+
const repro = Mesh.createBooleanReproPayload(
|
|
684
|
+
inputA,
|
|
685
|
+
inputB,
|
|
686
|
+
"subtraction",
|
|
687
|
+
options,
|
|
688
|
+
undefined,
|
|
689
|
+
undefined,
|
|
690
|
+
error,
|
|
691
|
+
);
|
|
568
692
|
console.error("[okgeometry-api] Mesh.subtract failed", {
|
|
569
693
|
options: options ?? null,
|
|
570
694
|
inputA: inputA.debugSummary(),
|
|
571
695
|
inputB: inputB.debugSummary(),
|
|
572
696
|
error,
|
|
573
697
|
});
|
|
698
|
+
console.error("[okgeometry-api] Mesh.subtract repro", repro);
|
|
699
|
+
console.error("[okgeometry-api] Mesh.subtract repro JSON", JSON.stringify(repro, null, 2));
|
|
574
700
|
} catch (debugError) {
|
|
575
701
|
console.error("[okgeometry-api] Mesh.subtract failure logging failed", debugError);
|
|
576
702
|
}
|
|
@@ -689,22 +815,22 @@ export class Mesh {
|
|
|
689
815
|
return new Mesh(buffer, true);
|
|
690
816
|
}
|
|
691
817
|
|
|
692
|
-
get trustedBooleanInput(): boolean {
|
|
693
|
-
return this._trustedBooleanInput;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
debugSummary(): MeshDebugSummary {
|
|
697
|
-
const bounds = Mesh.toDebugBounds(this.getBounds());
|
|
698
|
-
const topology = this.topologyMetrics();
|
|
699
|
-
return {
|
|
700
|
-
vertexCount: this.vertexCount,
|
|
701
|
-
faceCount: this.faceCount,
|
|
702
|
-
trustedBooleanInput: this._trustedBooleanInput,
|
|
703
|
-
isClosedVolume: this.isClosedVolume(),
|
|
704
|
-
topology,
|
|
705
|
-
bounds,
|
|
706
|
-
};
|
|
707
|
-
}
|
|
818
|
+
get trustedBooleanInput(): boolean {
|
|
819
|
+
return this._trustedBooleanInput;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
debugSummary(): MeshDebugSummary {
|
|
823
|
+
const bounds = Mesh.toDebugBounds(this.getBounds());
|
|
824
|
+
const topology = this.topologyMetrics();
|
|
825
|
+
return {
|
|
826
|
+
vertexCount: this.vertexCount,
|
|
827
|
+
faceCount: this.faceCount,
|
|
828
|
+
trustedBooleanInput: this._trustedBooleanInput,
|
|
829
|
+
isClosedVolume: this.isClosedVolume(),
|
|
830
|
+
topology,
|
|
831
|
+
bounds,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
708
834
|
|
|
709
835
|
/**
|
|
710
836
|
* Build an axis-aligned rectangle on a plane basis from opposite corners.
|
|
@@ -1006,24 +1132,24 @@ export class Mesh {
|
|
|
1006
1132
|
return Mesh.fromTrustedBuffer(wasm.mesh_create_box(width, height, depth));
|
|
1007
1133
|
}
|
|
1008
1134
|
|
|
1009
|
-
/**
|
|
1010
|
-
* Create a geodesic sphere centered at origin, matching Manifold's sphere topology.
|
|
1011
|
-
* @param radius - Sphere radius
|
|
1012
|
-
* @param segments - Segment hint; rounded up internally to a multiple of 4
|
|
1013
|
-
* @param rings - Compatibility-only detail hint; the higher of `segments` and `rings` is used
|
|
1014
|
-
* @returns New Mesh representing the sphere
|
|
1015
|
-
*/
|
|
1016
|
-
static createSphere(radius: number, segments: number, rings: number): Mesh {
|
|
1017
|
-
ensureInit();
|
|
1018
|
-
return Mesh.fromTrustedBuffer(wasm.mesh_create_sphere(radius, segments, rings));
|
|
1019
|
-
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Create a geodesic sphere centered at origin, matching Manifold's sphere topology.
|
|
1137
|
+
* @param radius - Sphere radius
|
|
1138
|
+
* @param segments - Segment hint; rounded up internally to a multiple of 4
|
|
1139
|
+
* @param rings - Compatibility-only detail hint; the higher of `segments` and `rings` is used
|
|
1140
|
+
* @returns New Mesh representing the sphere
|
|
1141
|
+
*/
|
|
1142
|
+
static createSphere(radius: number, segments: number, rings: number): Mesh {
|
|
1143
|
+
ensureInit();
|
|
1144
|
+
return Mesh.fromTrustedBuffer(wasm.mesh_create_sphere(radius, segments, rings));
|
|
1145
|
+
}
|
|
1020
1146
|
|
|
1021
1147
|
/**
|
|
1022
|
-
* Create a cylinder centered at origin with axis along
|
|
1148
|
+
* Create a cylinder centered at origin with axis along Z, matching Manifold.
|
|
1023
1149
|
* @param radius - Cylinder radius
|
|
1024
1150
|
* @param height - Total height (extends height/2 above and below origin)
|
|
1025
1151
|
* @param segments - Number of circumferential segments
|
|
1026
|
-
* @returns New Mesh with
|
|
1152
|
+
* @returns New Mesh with cap triangulation matching Manifold
|
|
1027
1153
|
*/
|
|
1028
1154
|
static createCylinder(radius: number, height: number, segments: number): Mesh {
|
|
1029
1155
|
ensureInit();
|
|
@@ -1043,11 +1169,11 @@ export class Mesh {
|
|
|
1043
1169
|
}
|
|
1044
1170
|
|
|
1045
1171
|
/**
|
|
1046
|
-
* Create a cone centered at origin with
|
|
1172
|
+
* Create a cone centered at origin with axis along Z, matching Manifold.
|
|
1047
1173
|
* @param radius - Base radius
|
|
1048
1174
|
* @param height - Height from base to apex
|
|
1049
1175
|
* @param segments - Number of circumferential segments
|
|
1050
|
-
* @returns New Mesh with base
|
|
1176
|
+
* @returns New Mesh with base triangulation matching Manifold
|
|
1051
1177
|
*/
|
|
1052
1178
|
static createCone(radius: number, height: number, segments: number): Mesh {
|
|
1053
1179
|
ensureInit();
|
|
@@ -1461,12 +1587,12 @@ export class Mesh {
|
|
|
1461
1587
|
return Mesh.fromTrustedBuffer(result);
|
|
1462
1588
|
}
|
|
1463
1589
|
|
|
1464
|
-
static encodeBooleanOperationToken(
|
|
1465
|
-
operation: MeshBooleanOperation,
|
|
1466
|
-
a: Mesh,
|
|
1467
|
-
b: Mesh,
|
|
1468
|
-
options?: MeshBooleanOptions,
|
|
1469
|
-
): string {
|
|
1590
|
+
static encodeBooleanOperationToken(
|
|
1591
|
+
operation: MeshBooleanOperation,
|
|
1592
|
+
a: Mesh,
|
|
1593
|
+
b: Mesh,
|
|
1594
|
+
options?: MeshBooleanOptions,
|
|
1595
|
+
): string {
|
|
1470
1596
|
const tokens: string[] = [operation];
|
|
1471
1597
|
if (a._trustedBooleanInput && b._trustedBooleanInput) {
|
|
1472
1598
|
tokens.push("trustedInput");
|
|
@@ -1508,29 +1634,29 @@ export class Mesh {
|
|
|
1508
1634
|
* @param options - Optional safety overrides
|
|
1509
1635
|
* @returns New mesh with other's volume removed from this
|
|
1510
1636
|
*/
|
|
1511
|
-
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1512
|
-
ensureInit();
|
|
1513
|
-
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
|
|
1514
|
-
try {
|
|
1515
|
-
const result = this.runBoolean(
|
|
1516
|
-
other,
|
|
1517
|
-
"subtraction",
|
|
1518
|
-
() => wasm.mesh_boolean_operation(
|
|
1519
|
-
this._vertexCount,
|
|
1520
|
-
this._buffer,
|
|
1521
|
-
other._vertexCount,
|
|
1522
|
-
other._buffer,
|
|
1523
|
-
operationToken,
|
|
1524
|
-
),
|
|
1525
|
-
options,
|
|
1526
|
-
);
|
|
1527
|
-
Mesh.logSubtractDebugSuccess(this, other, result, options);
|
|
1528
|
-
return result;
|
|
1529
|
-
} catch (error) {
|
|
1530
|
-
Mesh.logSubtractDebugFailure(this, other, options, error);
|
|
1531
|
-
throw error;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1637
|
+
subtract(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1638
|
+
ensureInit();
|
|
1639
|
+
const operationToken = Mesh.encodeBooleanOperationToken("subtraction", this, other, options);
|
|
1640
|
+
try {
|
|
1641
|
+
const result = this.runBoolean(
|
|
1642
|
+
other,
|
|
1643
|
+
"subtraction",
|
|
1644
|
+
() => wasm.mesh_boolean_operation(
|
|
1645
|
+
this._vertexCount,
|
|
1646
|
+
this._buffer,
|
|
1647
|
+
other._vertexCount,
|
|
1648
|
+
other._buffer,
|
|
1649
|
+
operationToken,
|
|
1650
|
+
),
|
|
1651
|
+
options,
|
|
1652
|
+
);
|
|
1653
|
+
Mesh.logSubtractDebugSuccess(this, other, result, options);
|
|
1654
|
+
return result;
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
Mesh.logSubtractDebugFailure(this, other, options, error);
|
|
1657
|
+
throw error;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1534
1660
|
|
|
1535
1661
|
/**
|
|
1536
1662
|
* Compute boolean intersection with another mesh.
|
|
@@ -1538,9 +1664,9 @@ export class Mesh {
|
|
|
1538
1664
|
* @param options - Optional safety overrides
|
|
1539
1665
|
* @returns New mesh containing only the overlapping volume
|
|
1540
1666
|
*/
|
|
1541
|
-
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1542
|
-
ensureInit();
|
|
1543
|
-
const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
|
|
1667
|
+
intersect(other: Mesh, options?: MeshBooleanOptions): Mesh {
|
|
1668
|
+
ensureInit();
|
|
1669
|
+
const operationToken = Mesh.encodeBooleanOperationToken("intersection", this, other, options);
|
|
1544
1670
|
return this.runBoolean(
|
|
1545
1671
|
other,
|
|
1546
1672
|
"intersection",
|
|
@@ -1551,52 +1677,145 @@ export class Mesh {
|
|
|
1551
1677
|
other._buffer,
|
|
1552
1678
|
operationToken,
|
|
1553
1679
|
),
|
|
1554
|
-
options,
|
|
1555
|
-
);
|
|
1680
|
+
options,
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
debugBoolean(
|
|
1685
|
+
other: Mesh,
|
|
1686
|
+
operation: MeshBooleanOperation,
|
|
1687
|
+
options?: MeshBooleanOptions,
|
|
1688
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
1689
|
+
): MeshBooleanDebugReport {
|
|
1690
|
+
const result = operation === "union"
|
|
1691
|
+
? this.union(other, options)
|
|
1692
|
+
: operation === "subtraction"
|
|
1693
|
+
? this.subtract(other, options)
|
|
1694
|
+
: this.intersect(other, options);
|
|
1695
|
+
|
|
1696
|
+
return Mesh.createBooleanDebugReport(this, other, result, operation, debugOptions);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
debugUnion(
|
|
1700
|
+
other: Mesh,
|
|
1701
|
+
options?: MeshBooleanOptions,
|
|
1702
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
1703
|
+
): MeshBooleanDebugReport {
|
|
1704
|
+
return this.debugBoolean(other, "union", options, debugOptions);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
debugSubtract(
|
|
1708
|
+
other: Mesh,
|
|
1709
|
+
options?: MeshBooleanOptions,
|
|
1710
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
1711
|
+
): MeshBooleanDebugReport {
|
|
1712
|
+
return this.debugBoolean(other, "subtraction", options, debugOptions);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
debugIntersect(
|
|
1716
|
+
other: Mesh,
|
|
1717
|
+
options?: MeshBooleanOptions,
|
|
1718
|
+
debugOptions?: MeshBooleanDebugOptions,
|
|
1719
|
+
): MeshBooleanDebugReport {
|
|
1720
|
+
return this.debugBoolean(other, "intersection", options, debugOptions);
|
|
1556
1721
|
}
|
|
1557
1722
|
|
|
1558
|
-
|
|
1723
|
+
exportBooleanRepro(
|
|
1559
1724
|
other: Mesh,
|
|
1560
1725
|
operation: MeshBooleanOperation,
|
|
1561
1726
|
options?: MeshBooleanOptions,
|
|
1562
|
-
|
|
1563
|
-
):
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
? this.subtract(other, options)
|
|
1568
|
-
: this.intersect(other, options);
|
|
1727
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
1728
|
+
): MeshBooleanReproPayload {
|
|
1729
|
+
if (!reproOptions?.includeResult) {
|
|
1730
|
+
return Mesh.createBooleanReproPayload(this, other, operation, options);
|
|
1731
|
+
}
|
|
1569
1732
|
|
|
1570
|
-
|
|
1733
|
+
try {
|
|
1734
|
+
const result = operation === "union"
|
|
1735
|
+
? this.union(other, options)
|
|
1736
|
+
: operation === "subtraction"
|
|
1737
|
+
? this.subtract(other, options)
|
|
1738
|
+
: this.intersect(other, options);
|
|
1739
|
+
return Mesh.createBooleanReproPayload(
|
|
1740
|
+
this,
|
|
1741
|
+
other,
|
|
1742
|
+
operation,
|
|
1743
|
+
options,
|
|
1744
|
+
result,
|
|
1745
|
+
reproOptions.debugOptions,
|
|
1746
|
+
);
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
return Mesh.createBooleanReproPayload(
|
|
1749
|
+
this,
|
|
1750
|
+
other,
|
|
1751
|
+
operation,
|
|
1752
|
+
options,
|
|
1753
|
+
undefined,
|
|
1754
|
+
undefined,
|
|
1755
|
+
error,
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1571
1758
|
}
|
|
1572
1759
|
|
|
1573
|
-
|
|
1760
|
+
exportSubtractRepro(
|
|
1574
1761
|
other: Mesh,
|
|
1575
1762
|
options?: MeshBooleanOptions,
|
|
1576
|
-
|
|
1577
|
-
):
|
|
1578
|
-
return this.
|
|
1763
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
1764
|
+
): MeshBooleanReproPayload {
|
|
1765
|
+
return this.exportBooleanRepro(other, "subtraction", options, reproOptions);
|
|
1579
1766
|
}
|
|
1580
1767
|
|
|
1581
|
-
|
|
1768
|
+
exportBooleanReproJSON(
|
|
1582
1769
|
other: Mesh,
|
|
1770
|
+
operation: MeshBooleanOperation,
|
|
1583
1771
|
options?: MeshBooleanOptions,
|
|
1584
|
-
|
|
1585
|
-
):
|
|
1586
|
-
return this.
|
|
1772
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
1773
|
+
): string {
|
|
1774
|
+
return JSON.stringify(this.exportBooleanRepro(other, operation, options, reproOptions), null, 2);
|
|
1587
1775
|
}
|
|
1588
1776
|
|
|
1589
|
-
|
|
1777
|
+
exportSubtractReproJSON(
|
|
1590
1778
|
other: Mesh,
|
|
1591
1779
|
options?: MeshBooleanOptions,
|
|
1592
|
-
|
|
1593
|
-
):
|
|
1594
|
-
return this.
|
|
1780
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
1781
|
+
): string {
|
|
1782
|
+
return this.exportBooleanReproJSON(other, "subtraction", options, reproOptions);
|
|
1595
1783
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1784
|
+
|
|
1785
|
+
exportUnionRepro(
|
|
1786
|
+
other: Mesh,
|
|
1787
|
+
options?: MeshBooleanOptions,
|
|
1788
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
1789
|
+
): MeshBooleanReproPayload {
|
|
1790
|
+
return this.exportBooleanRepro(other, "union", options, reproOptions);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
exportIntersectRepro(
|
|
1794
|
+
other: Mesh,
|
|
1795
|
+
options?: MeshBooleanOptions,
|
|
1796
|
+
reproOptions?: MeshBooleanReproOptions,
|
|
1797
|
+
): MeshBooleanReproPayload {
|
|
1798
|
+
return this.exportBooleanRepro(other, "intersection", options, reproOptions);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
static replayBooleanRepro(payload: MeshBooleanReproPayload): Mesh {
|
|
1802
|
+
const inputA = Mesh.fromBuffer(new Float64Array(payload.inputA.buffer), {
|
|
1803
|
+
trustedBooleanInput: payload.inputA.trustedBooleanInput,
|
|
1804
|
+
});
|
|
1805
|
+
const inputB = Mesh.fromBuffer(new Float64Array(payload.inputB.buffer), {
|
|
1806
|
+
trustedBooleanInput: payload.inputB.trustedBooleanInput,
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
return payload.operation === "union"
|
|
1810
|
+
? inputA.union(inputB, payload.options ?? undefined)
|
|
1811
|
+
: payload.operation === "subtraction"
|
|
1812
|
+
? inputA.subtract(inputB, payload.options ?? undefined)
|
|
1813
|
+
: inputA.intersect(inputB, payload.options ?? undefined);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Compute boolean union in a dedicated Web Worker (non-blocking).
|
|
1818
|
+
* Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
|
|
1600
1819
|
*/
|
|
1601
1820
|
async unionAsync(other: Mesh, options?: MeshBooleanAsyncOptions): Promise<Mesh> {
|
|
1602
1821
|
const result = await runMeshBooleanInWorkerPool(
|