okgeometry-api 1.1.5 → 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.
Files changed (52) hide show
  1. package/dist/Arc.js +1 -1
  2. package/dist/Arc.js.map +1 -1
  3. package/dist/Circle.js +1 -1
  4. package/dist/Circle.js.map +1 -1
  5. package/dist/Line.js +1 -1
  6. package/dist/Line.js.map +1 -1
  7. package/dist/Mesh.d.ts +104 -7
  8. package/dist/Mesh.d.ts.map +1 -1
  9. package/dist/Mesh.js +106 -8
  10. package/dist/Mesh.js.map +1 -1
  11. package/dist/NurbsCurve.js +1 -1
  12. package/dist/NurbsCurve.js.map +1 -1
  13. package/dist/NurbsSurface.d.ts.map +1 -1
  14. package/dist/NurbsSurface.js +10 -7
  15. package/dist/NurbsSurface.js.map +1 -1
  16. package/dist/PolyCurve.js +1 -1
  17. package/dist/PolyCurve.js.map +1 -1
  18. package/dist/Polyline.js +1 -1
  19. package/dist/Polyline.js.map +1 -1
  20. package/dist/Ray.js +1 -1
  21. package/dist/Ray.js.map +1 -1
  22. package/dist/engine.d.ts.map +1 -1
  23. package/dist/engine.js +1 -3
  24. package/dist/engine.js.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/wasm-base64.d.ts +1 -1
  29. package/dist/wasm-base64.d.ts.map +1 -1
  30. package/dist/wasm-base64.js +1 -1
  31. package/dist/wasm-base64.js.map +1 -1
  32. package/package.json +7 -5
  33. package/src/Arc.ts +117 -117
  34. package/src/Circle.ts +153 -153
  35. package/src/Line.ts +144 -144
  36. package/src/Mesh.ts +663 -444
  37. package/src/NurbsCurve.ts +240 -240
  38. package/src/NurbsSurface.ts +249 -245
  39. package/src/PolyCurve.ts +306 -306
  40. package/src/Polyline.ts +153 -153
  41. package/src/Ray.ts +90 -90
  42. package/src/engine.ts +9 -11
  43. package/src/index.ts +6 -0
  44. package/src/wasm-base64.ts +1 -1
  45. package/wasm/README.md +0 -104
  46. package/wasm/okgeometrycore.d.ts +0 -754
  47. package/wasm/okgeometrycore.js +0 -2005
  48. package/wasm/okgeometrycore_bg.d.ts +0 -3
  49. package/wasm/okgeometrycore_bg.js +0 -1686
  50. package/wasm/okgeometrycore_bg.wasm +0 -0
  51. package/wasm/okgeometrycore_bg.wasm.d.ts +0 -100
  52. 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/okgeometrycore_bg.js";
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 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;
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 MeshDebugSummary {
79
- vertexCount: number;
80
- faceCount: number;
152
+ export interface MeshBooleanReproOperand {
81
153
  trustedBooleanInput: boolean;
82
- isClosedVolume: boolean;
83
- topology: {
84
- boundaryEdges: number;
85
- nonManifoldEdges: number;
86
- };
87
- bounds: MeshDebugBounds;
154
+ summary: MeshDebugSummary;
155
+ buffer: number[];
88
156
  }
89
157
 
90
- export interface MeshDebugRayHit {
91
- point: Point;
92
- normal: Vec3;
93
- faceIndex: number;
94
- distance: number;
158
+ export interface MeshBooleanReproResult {
159
+ summary: MeshDebugSummary;
160
+ buffer: number[];
161
+ probeSummary?: MeshBooleanDebugProbeSummary[];
95
162
  }
96
163
 
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[];
164
+ export interface MeshBooleanReproError {
165
+ name: string;
166
+ message: string;
167
+ stack?: string;
105
168
  }
106
169
 
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[];
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 MeshBooleanDebugReport {
117
- operation: MeshBooleanOperation;
118
- inputA: MeshDebugSummary;
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
- min: bounds.min,
258
- max: bounds.max,
259
- center: new Point(
260
- (bounds.min.x + bounds.max.x) * 0.5,
261
- (bounds.min.y + bounds.max.y) * 0.5,
262
- (bounds.min.z + bounds.max.z) * 0.5,
263
- ),
264
- size,
265
- diagonal: Math.hypot(size.x, size.y, size.z),
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 clampDebugHitCount(count?: number): number {
270
- if (!Number.isFinite(count)) return 4;
271
- return Math.max(1, Math.min(32, Math.floor(count ?? 4)));
272
- }
273
-
274
- private static collectDebugHits(
275
- mesh: Mesh,
276
- origin: Point,
277
- direction: Vec3,
278
- maxDistance: number,
279
- maxHits: number,
280
- ): MeshDebugRayHit[] {
281
- return mesh.raycastAll(origin, direction, maxDistance).slice(0, maxHits).map((hit) => ({
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)];
568
+ private static serializeBooleanOptions(options?: MeshBooleanOptions): MeshBooleanOptions | null {
569
+ return options == null ? null : { ...options };
293
570
  }
294
571
 
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
- posX: new Vec3(0, minor, -major),
303
- negX: new Vec3(0, -minor, major),
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 buildDirectionalDebugProbes(
312
- inputA: Mesh,
313
- inputB: Mesh,
314
- result: Mesh,
315
- center: Point,
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
- }
375
-
376
- private static buildBooleanDebugProbes(
377
- inputA: Mesh,
378
- 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
592
  }
441
593
 
442
- private static createBooleanDebugReport(
594
+ private static createBooleanReproPayload(
443
595
  inputA: Mesh,
444
596
  inputB: Mesh,
445
- result: Mesh,
446
597
  operation: MeshBooleanOperation,
598
+ options?: MeshBooleanOptions,
599
+ result?: Mesh,
447
600
  debugOptions?: MeshBooleanDebugOptions,
448
- ): MeshBooleanDebugReport {
449
- return {
601
+ error?: unknown,
602
+ ): MeshBooleanReproPayload {
603
+ const payload: MeshBooleanReproPayload = {
604
+ version: 1,
450
605
  operation,
451
- inputA: inputA.debugSummary(),
452
- inputB: inputB.debugSummary(),
453
- result,
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
- private static roundDebugScalar(value: number): number {
460
- return Number.isFinite(value) ? Number(value.toFixed(4)) : value;
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
- private static summarizeDebugHit(hit: MeshDebugRayHit | undefined): {
464
- point: { x: number; y: number; z: number };
465
- normal: { x: number; y: number; z: number };
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
- private static summarizeDebugProbe(probe: MeshBooleanDebugProbe): {
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.
@@ -1007,10 +1133,10 @@ export class Mesh {
1007
1133
  }
1008
1134
 
1009
1135
  /**
1010
- * Create a UV sphere centered at origin.
1136
+ * Create a geodesic sphere centered at origin, matching Manifold's sphere topology.
1011
1137
  * @param radius - Sphere radius
1012
- * @param segments - Number of longitudinal segments
1013
- * @param rings - Number of latitudinal rings
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
1014
1140
  * @returns New Mesh representing the sphere
1015
1141
  */
1016
1142
  static createSphere(radius: number, segments: number, rings: number): Mesh {
@@ -1019,11 +1145,11 @@ export class Mesh {
1019
1145
  }
1020
1146
 
1021
1147
  /**
1022
- * Create a cylinder centered at origin with axis along Y.
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 caps
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 apex at top.
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 cap
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
- debugBoolean(
1723
+ exportBooleanRepro(
1559
1724
  other: Mesh,
1560
1725
  operation: MeshBooleanOperation,
1561
1726
  options?: MeshBooleanOptions,
1562
- debugOptions?: MeshBooleanDebugOptions,
1563
- ): MeshBooleanDebugReport {
1564
- const result = operation === "union"
1565
- ? this.union(other, options)
1566
- : operation === "subtraction"
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
- return Mesh.createBooleanDebugReport(this, other, result, operation, debugOptions);
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
- debugUnion(
1760
+ exportSubtractRepro(
1574
1761
  other: Mesh,
1575
1762
  options?: MeshBooleanOptions,
1576
- debugOptions?: MeshBooleanDebugOptions,
1577
- ): MeshBooleanDebugReport {
1578
- return this.debugBoolean(other, "union", options, debugOptions);
1763
+ reproOptions?: MeshBooleanReproOptions,
1764
+ ): MeshBooleanReproPayload {
1765
+ return this.exportBooleanRepro(other, "subtraction", options, reproOptions);
1579
1766
  }
1580
1767
 
1581
- debugSubtract(
1768
+ exportBooleanReproJSON(
1582
1769
  other: Mesh,
1770
+ operation: MeshBooleanOperation,
1583
1771
  options?: MeshBooleanOptions,
1584
- debugOptions?: MeshBooleanDebugOptions,
1585
- ): MeshBooleanDebugReport {
1586
- return this.debugBoolean(other, "subtraction", options, debugOptions);
1772
+ reproOptions?: MeshBooleanReproOptions,
1773
+ ): string {
1774
+ return JSON.stringify(this.exportBooleanRepro(other, operation, options, reproOptions), null, 2);
1587
1775
  }
1588
1776
 
1589
- debugIntersect(
1777
+ exportSubtractReproJSON(
1590
1778
  other: Mesh,
1591
1779
  options?: MeshBooleanOptions,
1592
- debugOptions?: MeshBooleanDebugOptions,
1593
- ): MeshBooleanDebugReport {
1594
- return this.debugBoolean(other, "intersection", options, debugOptions);
1780
+ reproOptions?: MeshBooleanReproOptions,
1781
+ ): string {
1782
+ return this.exportBooleanReproJSON(other, "subtraction", options, reproOptions);
1595
1783
  }
1596
-
1597
- /**
1598
- * Compute boolean union in a dedicated Web Worker (non-blocking).
1599
- * Defaults to allowUnsafe=true so high-poly jobs can run off the UI thread.
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(