forgecad 0.9.7 → 0.9.9

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 (47) hide show
  1. package/README.md +1 -0
  2. package/dist/assets/{AdminPage-DX0mpSZT.js → AdminPage-CNEvQM7c.js} +1 -1
  3. package/dist/assets/{BlogPage-CI_P0_Pf.js → BlogPage-Cc41PP4d.js} +1 -1
  4. package/dist/assets/{DocsPage-DLhIIZyJ.js → DocsPage-9U1hGjrg.js} +2 -2
  5. package/dist/assets/{EditorApp-DfFT2Dn8.css → EditorApp-D11wL4Qn.css} +51 -0
  6. package/dist/assets/{EditorApp-BujZvuwX.js → EditorApp-DnddQvBt.js} +151 -9
  7. package/dist/assets/{EmbedViewer-0S0qXKog.js → EmbedViewer-B2lhWTcd.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-O_yMtAri.js → LandingPageProofDriven-CFet-l3o.js} +1 -1
  9. package/dist/assets/{PricingPage-DGkX3Ahr.js → PricingPage-CPm8mQx3.js} +1 -1
  10. package/dist/assets/{SettingsPage-DBsqTB_y.js → SettingsPage-BarZonVZ.js} +1 -1
  11. package/dist/assets/__vite-browser-external-Dhvy_jtL.js +4 -0
  12. package/dist/assets/{app-BE2nD6Yz.js → app-BjSEB4sY.js} +1006 -526
  13. package/dist/assets/cli/{render-iP9qh475.js → render-CXVrHY8q.js} +647 -481
  14. package/dist/assets/constructionHistoryWorker-z9_LGiRd.js +42984 -0
  15. package/dist/assets/{evalWorker-Ds5U4xtN.js → evalWorker-CtO7GsJR.js} +42 -9
  16. package/dist/assets/{inspectWorker-Dll4eVyD.js → inspectWorker-BZ2CkQZr.js} +785 -111
  17. package/dist/assets/{manifold-DjYsd7A_.js → manifold-BRdhoQy5.js} +2 -2
  18. package/dist/assets/{manifold-sJ-axdXM.js → manifold-BVi4_OeB.js} +1 -1
  19. package/dist/assets/manifold-Cp_dCC7i.js +3018 -0
  20. package/dist/assets/{manifold-Bk26ViCr.js → manifold-DAzn2Fsa.js} +1 -1
  21. package/dist/assets/{renderSceneState-Bngp5MrQ.js → renderSceneState-CWO8rHlt.js} +1 -1
  22. package/dist/assets/{reportWorker-CU8RZ4O0.js → reportWorker-Bz9tGiHb.js} +42 -9
  23. package/dist/assets/{sectionPlaneMath-BdTjyVfs.js → scalar-sampling-budget-Bmewod18.js} +1339 -215
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +1 -1
  26. package/dist/docs-raw/CLI.md +10 -10
  27. package/dist/docs-raw/coding-best-practices.md +1 -1
  28. package/dist/docs-raw/guides/inspection-bundles.md +77 -19
  29. package/dist/docs-raw/guides/skill-maintenance.md +1 -1
  30. package/dist/docs-raw/runbook.md +2 -2
  31. package/dist/docs-raw/skills/forgecad-make-a-model.md +11 -0
  32. package/dist/docs-raw/skills/forgecad-render-inspect.md +12 -6
  33. package/dist/docs-raw/skills/index.md +1 -1
  34. package/dist/index.html +1 -1
  35. package/dist/sitemap.xml +6 -6
  36. package/dist-cli/forgecad.js +596 -354
  37. package/dist-cli/forgecad.js.map +1 -1
  38. package/dist-skill/CONTEXT.md +77 -19
  39. package/dist-skill/docs/CLI.md +10 -10
  40. package/dist-skill/docs/guides/inspection-bundles.md +77 -19
  41. package/dist-skill/docs-dev/CLI.md +10 -10
  42. package/dist-skill/docs-dev/coding-best-practices.md +1 -1
  43. package/dist-skill/docs-dev/guides/inspection-bundles.md +77 -19
  44. package/dist-skill/docs-dev/guides/skill-maintenance.md +1 -1
  45. package/dist-skill/library/forgecad-make-a-model/SKILL.md +11 -0
  46. package/dist-skill/library/forgecad-render-inspect/SKILL.md +12 -6
  47. package/package.json +6 -3
@@ -11132,7 +11132,7 @@ if (typeof window !== "undefined") {
11132
11132
  }
11133
11133
  }
11134
11134
  const DEFAULT_LEAF_SIZE = 8;
11135
- function cloneVec3$1(value) {
11135
+ function cloneVec3$2(value) {
11136
11136
  return [value[0], value[1], value[2]];
11137
11137
  }
11138
11138
  function emptyBounds() {
@@ -11200,14 +11200,14 @@ class AabbSpatialIndex {
11200
11200
  nodeCount += 1;
11201
11201
  const bounds = boundsFor(entries, indexes);
11202
11202
  if (indexes.length <= this.leafSize) {
11203
- return { min: cloneVec3$1(bounds.min), max: cloneVec3$1(bounds.max), left: null, right: null, indexes };
11203
+ return { min: cloneVec3$2(bounds.min), max: cloneVec3$2(bounds.max), left: null, right: null, indexes };
11204
11204
  }
11205
11205
  const axis = longestAxis(bounds);
11206
11206
  indexes.sort((a, b) => centerOnAxis(entries[a], axis) - centerOnAxis(entries[b], axis) || a - b);
11207
11207
  const mid = Math.max(1, Math.floor(indexes.length / 2));
11208
11208
  return {
11209
- min: cloneVec3$1(bounds.min),
11210
- max: cloneVec3$1(bounds.max),
11209
+ min: cloneVec3$2(bounds.min),
11210
+ max: cloneVec3$2(bounds.max),
11211
11211
  left: build(indexes.slice(0, mid)),
11212
11212
  right: build(indexes.slice(mid)),
11213
11213
  indexes: null
@@ -11270,13 +11270,333 @@ class AabbSpatialIndex {
11270
11270
  };
11271
11271
  }
11272
11272
  }
11273
+ const AXIS_NAMES = ["x", "y", "z"];
11274
+ function nearestBoundaryGap(a, b, axis) {
11275
+ return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
11276
+ }
11277
+ function hasPositiveGap(gaps) {
11278
+ return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
11279
+ }
11280
+ function contactFromBBoxes(a, b, tolerance) {
11281
+ const gaps = aabbGaps(a, b);
11282
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
11283
+ if (largestGap > tolerance) return { touching: false, gap: largestGap };
11284
+ const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
11285
+ if (separatedAxes.length > 0) {
11286
+ const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
11287
+ return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
11288
+ }
11289
+ const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
11290
+ axis,
11291
+ axisName,
11292
+ gap: nearestBoundaryGap(a, b, axis)
11293
+ })).filter((entry) => entry.gap <= tolerance);
11294
+ if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
11295
+ const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
11296
+ return { touching: true, gap: nearest.gap, axis: nearest.axisName };
11297
+ }
11298
+ function isFiniteTriangle(positions, offset) {
11299
+ for (let index = 0; index < 9; index += 1) {
11300
+ if (!Number.isFinite(positions[offset + index])) return false;
11301
+ }
11302
+ return true;
11303
+ }
11304
+ function triangleRef(positions, offset) {
11305
+ if (!isFiniteTriangle(positions, offset)) return null;
11306
+ const ax = positions[offset];
11307
+ const ay = positions[offset + 1];
11308
+ const az = positions[offset + 2];
11309
+ const bx = positions[offset + 3];
11310
+ const by = positions[offset + 4];
11311
+ const bz = positions[offset + 5];
11312
+ const cx = positions[offset + 6];
11313
+ const cy = positions[offset + 7];
11314
+ const cz = positions[offset + 8];
11315
+ return {
11316
+ offset,
11317
+ min: [Math.min(ax, bx, cx), Math.min(ay, by, cy), Math.min(az, bz, cz)],
11318
+ max: [Math.max(ax, bx, cx), Math.max(ay, by, cy), Math.max(az, bz, cz)]
11319
+ };
11320
+ }
11321
+ function meshContactDataFor(entry) {
11322
+ const positions = entry.positions;
11323
+ if (!positions || positions.length < 9) return null;
11324
+ const triangleCount = Math.floor(positions.length / 9);
11325
+ const triangles = [];
11326
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
11327
+ const triangle = triangleRef(positions, triangleIndex * 9);
11328
+ if (triangle) triangles.push(triangle);
11329
+ }
11330
+ if (triangles.length === 0) return null;
11331
+ return {
11332
+ positions,
11333
+ vertexCount: triangleCount * 3,
11334
+ triangles
11335
+ };
11336
+ }
11337
+ function distSq(px, py, pz, qx, qy, qz) {
11338
+ const dx = px - qx;
11339
+ const dy = py - qy;
11340
+ const dz = pz - qz;
11341
+ return dx * dx + dy * dy + dz * dz;
11342
+ }
11343
+ function pointSegmentDistanceSq(point, start, end) {
11344
+ const dx = end[0] - start[0];
11345
+ const dy = end[1] - start[1];
11346
+ const dz = end[2] - start[2];
11347
+ const lengthSq = dx * dx + dy * dy + dz * dz;
11348
+ if (lengthSq <= 1e-20) return distSq(point[0], point[1], point[2], start[0], start[1], start[2]);
11349
+ const t = Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy + (point[2] - start[2]) * dz) / lengthSq));
11350
+ return distSq(point[0], point[1], point[2], start[0] + t * dx, start[1] + t * dy, start[2] + t * dz);
11351
+ }
11352
+ function pointTriangleDistanceSq(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz) {
11353
+ const abx = bx - ax;
11354
+ const aby = by - ay;
11355
+ const abz = bz - az;
11356
+ const acx = cx - ax;
11357
+ const acy = cy - ay;
11358
+ const acz = cz - az;
11359
+ const apx = px - ax;
11360
+ const apy = py - ay;
11361
+ const apz = pz - az;
11362
+ const d1 = abx * apx + aby * apy + abz * apz;
11363
+ const d2 = acx * apx + acy * apy + acz * apz;
11364
+ if (d1 <= 0 && d2 <= 0) return distSq(px, py, pz, ax, ay, az);
11365
+ const bpx = px - bx;
11366
+ const bpy = py - by;
11367
+ const bpz = pz - bz;
11368
+ const d3 = abx * bpx + aby * bpy + abz * bpz;
11369
+ const d4 = acx * bpx + acy * bpy + acz * bpz;
11370
+ if (d3 >= 0 && d4 <= d3) return distSq(px, py, pz, bx, by, bz);
11371
+ const vc = d1 * d4 - d3 * d2;
11372
+ if (vc <= 0 && d1 >= 0 && d3 <= 0) {
11373
+ const v2 = d1 / (d1 - d3);
11374
+ return distSq(px, py, pz, ax + v2 * abx, ay + v2 * aby, az + v2 * abz);
11375
+ }
11376
+ const cpx = px - cx;
11377
+ const cpy = py - cy;
11378
+ const cpz = pz - cz;
11379
+ const d5 = abx * cpx + aby * cpy + abz * cpz;
11380
+ const d6 = acx * cpx + acy * cpy + acz * cpz;
11381
+ if (d6 >= 0 && d5 <= d6) return distSq(px, py, pz, cx, cy, cz);
11382
+ const vb = d5 * d2 - d1 * d6;
11383
+ if (vb <= 0 && d2 >= 0 && d6 <= 0) {
11384
+ const w2 = d2 / (d2 - d6);
11385
+ return distSq(px, py, pz, ax + w2 * acx, ay + w2 * acy, az + w2 * acz);
11386
+ }
11387
+ const va = d3 * d6 - d5 * d4;
11388
+ if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
11389
+ const bcx = cx - bx;
11390
+ const bcy = cy - by;
11391
+ const bcz = cz - bz;
11392
+ const w2 = (d4 - d3) / (d4 - d3 + d5 - d6);
11393
+ return distSq(px, py, pz, bx + w2 * bcx, by + w2 * bcy, bz + w2 * bcz);
11394
+ }
11395
+ const barycentricTotal = va + vb + vc;
11396
+ if (!Number.isFinite(barycentricTotal) || Math.abs(barycentricTotal) < 1e-20) {
11397
+ return Math.min(distSq(px, py, pz, ax, ay, az), distSq(px, py, pz, bx, by, bz), distSq(px, py, pz, cx, cy, cz));
11398
+ }
11399
+ const denom = 1 / barycentricTotal;
11400
+ const v = vb * denom;
11401
+ const w = vc * denom;
11402
+ return distSq(px, py, pz, ax + abx * v + acx * w, ay + aby * v + acy * w, az + abz * v + acz * w);
11403
+ }
11404
+ function segmentSegmentDistanceSq(a0, a1, b0, b1) {
11405
+ const ux = a1[0] - a0[0];
11406
+ const uy = a1[1] - a0[1];
11407
+ const uz = a1[2] - a0[2];
11408
+ const vx = b1[0] - b0[0];
11409
+ const vy = b1[1] - b0[1];
11410
+ const vz = b1[2] - b0[2];
11411
+ const wx = a0[0] - b0[0];
11412
+ const wy = a0[1] - b0[1];
11413
+ const wz = a0[2] - b0[2];
11414
+ const a = ux * ux + uy * uy + uz * uz;
11415
+ const b = ux * vx + uy * vy + uz * vz;
11416
+ const c = vx * vx + vy * vy + vz * vz;
11417
+ const d = ux * wx + uy * wy + uz * wz;
11418
+ const e = vx * wx + vy * wy + vz * wz;
11419
+ const denom = a * c - b * b;
11420
+ let sNumerator = denom;
11421
+ let sDenominator = denom;
11422
+ let tNumerator = denom;
11423
+ let tDenominator = denom;
11424
+ if (a <= 1e-20) return pointSegmentDistanceSq(a0, b0, b1);
11425
+ if (c <= 1e-20) return pointSegmentDistanceSq(b0, a0, a1);
11426
+ if (denom < 1e-20) {
11427
+ sNumerator = 0;
11428
+ sDenominator = 1;
11429
+ tNumerator = e;
11430
+ tDenominator = c;
11431
+ } else {
11432
+ sNumerator = b * e - c * d;
11433
+ tNumerator = a * e - b * d;
11434
+ if (sNumerator < 0) {
11435
+ sNumerator = 0;
11436
+ tNumerator = e;
11437
+ tDenominator = c;
11438
+ } else if (sNumerator > sDenominator) {
11439
+ sNumerator = sDenominator;
11440
+ tNumerator = e + b;
11441
+ tDenominator = c;
11442
+ }
11443
+ }
11444
+ if (tNumerator < 0) {
11445
+ tNumerator = 0;
11446
+ if (-d < 0) {
11447
+ sNumerator = 0;
11448
+ } else if (-d > a) {
11449
+ sNumerator = sDenominator;
11450
+ } else {
11451
+ sNumerator = -d;
11452
+ sDenominator = a;
11453
+ }
11454
+ } else if (tNumerator > tDenominator) {
11455
+ tNumerator = tDenominator;
11456
+ if (-d + b < 0) {
11457
+ sNumerator = 0;
11458
+ } else if (-d + b > a) {
11459
+ sNumerator = sDenominator;
11460
+ } else {
11461
+ sNumerator = -d + b;
11462
+ sDenominator = a;
11463
+ }
11464
+ }
11465
+ const s = Math.abs(sNumerator) < 1e-20 ? 0 : sNumerator / sDenominator;
11466
+ const t = Math.abs(tNumerator) < 1e-20 ? 0 : tNumerator / tDenominator;
11467
+ const dx = wx + s * ux - t * vx;
11468
+ const dy = wy + s * uy - t * vy;
11469
+ const dz = wz + s * uz - t * vz;
11470
+ return dx * dx + dy * dy + dz * dz;
11471
+ }
11472
+ function trianglePoint(positions, triangle, corner) {
11473
+ const offset = triangle.offset + corner * 3;
11474
+ return [positions[offset], positions[offset + 1], positions[offset + 2]];
11475
+ }
11476
+ function bboxDistanceSq(a, b) {
11477
+ let total = 0;
11478
+ for (let axis = 0; axis < 3; axis += 1) {
11479
+ const gap = Math.max(0, a.min[axis] - b.max[axis], b.min[axis] - a.max[axis]);
11480
+ total += gap * gap;
11481
+ }
11482
+ return total;
11483
+ }
11484
+ function triangleDistanceSq(aPositions, a, bPositions, b) {
11485
+ const a0 = trianglePoint(aPositions, a, 0);
11486
+ const a1 = trianglePoint(aPositions, a, 1);
11487
+ const a2 = trianglePoint(aPositions, a, 2);
11488
+ const b0 = trianglePoint(bPositions, b, 0);
11489
+ const b1 = trianglePoint(bPositions, b, 1);
11490
+ const b2 = trianglePoint(bPositions, b, 2);
11491
+ return Math.min(
11492
+ pointTriangleDistanceSq(a0[0], a0[1], a0[2], b0[0], b0[1], b0[2], b1[0], b1[1], b1[2], b2[0], b2[1], b2[2]),
11493
+ pointTriangleDistanceSq(a1[0], a1[1], a1[2], b0[0], b0[1], b0[2], b1[0], b1[1], b1[2], b2[0], b2[1], b2[2]),
11494
+ pointTriangleDistanceSq(a2[0], a2[1], a2[2], b0[0], b0[1], b0[2], b1[0], b1[1], b1[2], b2[0], b2[1], b2[2]),
11495
+ pointTriangleDistanceSq(b0[0], b0[1], b0[2], a0[0], a0[1], a0[2], a1[0], a1[1], a1[2], a2[0], a2[1], a2[2]),
11496
+ pointTriangleDistanceSq(b1[0], b1[1], b1[2], a0[0], a0[1], a0[2], a1[0], a1[1], a1[2], a2[0], a2[1], a2[2]),
11497
+ pointTriangleDistanceSq(b2[0], b2[1], b2[2], a0[0], a0[1], a0[2], a1[0], a1[1], a1[2], a2[0], a2[1], a2[2]),
11498
+ segmentSegmentDistanceSq(a0, a1, b0, b1),
11499
+ segmentSegmentDistanceSq(a0, a1, b1, b2),
11500
+ segmentSegmentDistanceSq(a0, a1, b2, b0),
11501
+ segmentSegmentDistanceSq(a1, a2, b0, b1),
11502
+ segmentSegmentDistanceSq(a1, a2, b1, b2),
11503
+ segmentSegmentDistanceSq(a1, a2, b2, b0),
11504
+ segmentSegmentDistanceSq(a2, a0, b0, b1),
11505
+ segmentSegmentDistanceSq(a2, a0, b1, b2),
11506
+ segmentSegmentDistanceSq(a2, a0, b2, b0)
11507
+ );
11508
+ }
11509
+ function meshEntriesContactDistance(a, b, tolerance) {
11510
+ const toleranceSq = tolerance * tolerance;
11511
+ let bestDistanceSq = Infinity;
11512
+ for (const aTriangle of a.triangles) {
11513
+ for (const bTriangle of b.triangles) {
11514
+ if (bboxDistanceSq(aTriangle, bTriangle) > toleranceSq) continue;
11515
+ const distanceSq = triangleDistanceSq(a.positions, aTriangle, b.positions, bTriangle);
11516
+ if (distanceSq > toleranceSq) continue;
11517
+ if (distanceSq <= 1e-20) return 0;
11518
+ bestDistanceSq = Math.min(bestDistanceSq, distanceSq);
11519
+ }
11520
+ }
11521
+ return Number.isFinite(bestDistanceSq) ? Math.sqrt(bestDistanceSq) : null;
11522
+ }
11523
+ function intersectionVolume(a, b) {
11524
+ try {
11525
+ const hit = a.shape.intersect(b.shape);
11526
+ if (hit.isEmpty()) return { volume: 0 };
11527
+ const volume = hit.volume();
11528
+ return { volume: Number.isFinite(volume) ? volume : 0 };
11529
+ } catch (err) {
11530
+ const message = err instanceof Error ? err.message : String(err);
11531
+ return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
11532
+ }
11533
+ }
11534
+ function detectPhysicalContact(a, b, options, meshCache = {}) {
11535
+ const gaps = aabbGaps(a, b);
11536
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
11537
+ if (largestGap > options.contactTolerance) return { contact: null };
11538
+ const sourceMesh = meshCache.sourceMesh !== void 0 ? meshCache.sourceMesh : meshContactDataFor(a);
11539
+ const targetMesh = meshCache.targetMesh !== void 0 ? meshCache.targetMesh : meshContactDataFor(b);
11540
+ if (sourceMesh && targetMesh) {
11541
+ const distance = meshEntriesContactDistance(sourceMesh, targetMesh, options.contactTolerance);
11542
+ if (distance != null) {
11543
+ return {
11544
+ contact: {
11545
+ kind: "touching",
11546
+ method: "mesh-surface-distance",
11547
+ gap: distance
11548
+ }
11549
+ };
11550
+ }
11551
+ }
11552
+ const bboxOverlaps = !hasPositiveGap(gaps) && aabbInteriorOverlaps(a, b);
11553
+ if (options.exactGeometry && bboxOverlaps) {
11554
+ if (aabbOverlapVolume(a, b) <= options.minOverlapVolume) return { contact: null };
11555
+ const overlap = intersectionVolume(a, b);
11556
+ if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
11557
+ return {
11558
+ contact: {
11559
+ kind: "overlap",
11560
+ method: "boolean-intersection",
11561
+ gap: 0,
11562
+ overlapVolume: overlap.volume
11563
+ },
11564
+ warning: overlap.warning
11565
+ };
11566
+ }
11567
+ return { contact: null, warning: overlap.warning };
11568
+ }
11569
+ if (bboxOverlaps && options.mergeOverlappingBBoxes) {
11570
+ return {
11571
+ contact: {
11572
+ kind: "overlap",
11573
+ method: "bbox-overlap",
11574
+ gap: 0,
11575
+ overlapVolume: aabbOverlapVolume(a, b)
11576
+ }
11577
+ };
11578
+ }
11579
+ if (options.mergeTouchingBBoxes) {
11580
+ const contact = contactFromBBoxes(a, b, options.contactTolerance);
11581
+ if (contact.touching) {
11582
+ return {
11583
+ contact: {
11584
+ kind: "touching",
11585
+ method: "bbox-contact",
11586
+ gap: contact.gap,
11587
+ axis: contact.axis
11588
+ }
11589
+ };
11590
+ }
11591
+ }
11592
+ return { contact: null };
11593
+ }
11273
11594
  const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
11274
11595
  contactTolerance: 0.05,
11275
11596
  minOverlapVolume: 0.1,
11276
11597
  exactGeometry: true
11277
11598
  };
11278
- const AXIS_NAMES = ["x", "y", "z"];
11279
- let UnionFind$1 = class UnionFind {
11599
+ let UnionFind$2 = class UnionFind {
11280
11600
  constructor(size) {
11281
11601
  __publicField(this, "parent");
11282
11602
  __publicField(this, "rank");
@@ -11306,27 +11626,21 @@ let UnionFind$1 = class UnionFind {
11306
11626
  this.rank[rootA] += 1;
11307
11627
  }
11308
11628
  };
11309
- function cloneVec3(value) {
11629
+ function cloneVec3$1(value) {
11310
11630
  return [value[0], value[1], value[2]];
11311
11631
  }
11312
- function emptyBBox$1() {
11632
+ function emptyBBox$2() {
11313
11633
  return {
11314
11634
  min: [Infinity, Infinity, Infinity],
11315
11635
  max: [-Infinity, -Infinity, -Infinity]
11316
11636
  };
11317
11637
  }
11318
- function expandBBox$1(target, min, max) {
11638
+ function expandBBox$2(target, min, max) {
11319
11639
  for (let axis = 0; axis < 3; axis += 1) {
11320
11640
  target.min[axis] = Math.min(target.min[axis], min[axis]);
11321
11641
  target.max[axis] = Math.max(target.max[axis], max[axis]);
11322
11642
  }
11323
11643
  }
11324
- function nearestBoundaryGap(a, b, axis) {
11325
- return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
11326
- }
11327
- function hasPositiveGap(gaps) {
11328
- return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
11329
- }
11330
11644
  function collectCandidatePairs(entries, tolerance) {
11331
11645
  if (entries.length < 2) return [];
11332
11646
  const index = new AabbSpatialIndex(entries);
@@ -11338,36 +11652,7 @@ function collectCandidatePairs(entries, tolerance) {
11338
11652
  pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
11339
11653
  return pairs;
11340
11654
  }
11341
- function contactFromBBoxes(a, b, tolerance) {
11342
- const gaps = aabbGaps(a, b);
11343
- const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
11344
- if (largestGap > tolerance) return { touching: false, gap: largestGap };
11345
- const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
11346
- if (separatedAxes.length > 0) {
11347
- const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
11348
- return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
11349
- }
11350
- const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
11351
- axis,
11352
- axisName,
11353
- gap: nearestBoundaryGap(a, b, axis)
11354
- })).filter((entry) => entry.gap <= tolerance);
11355
- if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
11356
- const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
11357
- return { touching: true, gap: nearest.gap, axis: nearest.axisName };
11358
- }
11359
- function intersectionVolume(a, b) {
11360
- try {
11361
- const hit = a.shape.intersect(b.shape);
11362
- if (hit.isEmpty()) return { volume: 0 };
11363
- const volume = hit.volume();
11364
- return { volume: Number.isFinite(volume) ? volume : 0 };
11365
- } catch (err) {
11366
- const message = err instanceof Error ? err.message : String(err);
11367
- return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
11368
- }
11369
- }
11370
- function bodyCountForEntry(entry) {
11655
+ function bodyCountForEntry$1(entry) {
11371
11656
  if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
11372
11657
  return Math.max(0, Math.round(entry.bodyCount));
11373
11658
  }
@@ -11397,53 +11682,23 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
11397
11682
  };
11398
11683
  const warnings = [];
11399
11684
  const edges = [];
11400
- const unionFind = new UnionFind$1(entries.length);
11685
+ const unionFind = new UnionFind$2(entries.length);
11686
+ const meshDataByIndex = /* @__PURE__ */ new Map();
11687
+ entries.forEach((entry, index) => {
11688
+ const meshData = meshContactDataFor(entry);
11689
+ if (meshData) meshDataByIndex.set(index, meshData);
11690
+ });
11401
11691
  for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
11402
11692
  const i = pair.sourceIndex;
11403
11693
  const j = pair.targetIndex;
11404
- const a = entries[i];
11405
- const b = entries[j];
11406
- const bboxOverlaps = !hasPositiveGap(pair.gaps) && aabbInteriorOverlaps(a, b);
11407
- if (options.exactGeometry && bboxOverlaps) {
11408
- const overlap = intersectionVolume(a, b);
11409
- if (overlap.warning) warnings.push(overlap.warning);
11410
- if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
11411
- unionFind.union(i, j);
11412
- edges.push(
11413
- makeEdge(entries, i, j, {
11414
- kind: "overlap",
11415
- method: "boolean-intersection",
11416
- gap: 0,
11417
- overlapVolume: overlap.volume
11418
- })
11419
- );
11420
- continue;
11421
- }
11422
- continue;
11423
- }
11424
- if (bboxOverlaps && options.mergeOverlappingBBoxes) {
11425
- unionFind.union(i, j);
11426
- edges.push(
11427
- makeEdge(entries, i, j, {
11428
- kind: "overlap",
11429
- method: "bbox-overlap",
11430
- gap: 0,
11431
- overlapVolume: aabbOverlapVolume(a, b)
11432
- })
11433
- );
11434
- } else {
11435
- const contact = contactFromBBoxes(a, b, options.contactTolerance);
11436
- if (!contact.touching || !options.mergeTouchingBBoxes) continue;
11437
- unionFind.union(i, j);
11438
- edges.push(
11439
- makeEdge(entries, i, j, {
11440
- kind: "touching",
11441
- method: "bbox-contact",
11442
- gap: contact.gap,
11443
- axis: contact.axis
11444
- })
11445
- );
11446
- }
11694
+ const detection = detectPhysicalContact(entries[i], entries[j], options, {
11695
+ sourceMesh: meshDataByIndex.get(i) ?? null,
11696
+ targetMesh: meshDataByIndex.get(j) ?? null
11697
+ });
11698
+ if (detection.warning) warnings.push(detection.warning);
11699
+ if (!detection.contact) continue;
11700
+ unionFind.union(i, j);
11701
+ edges.push(makeEdge(entries, i, j, detection.contact));
11447
11702
  }
11448
11703
  const objects = entries.map((entry, index) => ({
11449
11704
  index,
@@ -11452,10 +11707,10 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
11452
11707
  groupName: entry.groupName,
11453
11708
  treePath: entry.treePath,
11454
11709
  mock: entry.mock === true,
11455
- bodyCount: bodyCountForEntry(entry),
11710
+ bodyCount: bodyCountForEntry$1(entry),
11456
11711
  bbox: {
11457
- min: cloneVec3(entry.min),
11458
- max: cloneVec3(entry.max)
11712
+ min: cloneVec3$1(entry.min),
11713
+ max: cloneVec3$1(entry.max)
11459
11714
  },
11460
11715
  componentIndex: 0
11461
11716
  }));
@@ -11472,7 +11727,7 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
11472
11727
  objectNames: [],
11473
11728
  objectCount: 0,
11474
11729
  bodyCount: 0,
11475
- bbox: emptyBBox$1()
11730
+ bbox: emptyBBox$2()
11476
11731
  };
11477
11732
  componentByRoot.set(root, component);
11478
11733
  rootToComponentIndex.set(root, component.index);
@@ -11484,11 +11739,11 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
11484
11739
  component.objectNames.push(object.name);
11485
11740
  component.objectCount += 1;
11486
11741
  component.bodyCount += object.bodyCount;
11487
- expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
11742
+ expandBBox$2(component.bbox, object.bbox.min, object.bbox.max);
11488
11743
  }
11489
11744
  const components = [...componentByRoot.values()];
11490
11745
  return {
11491
- method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
11746
+ method: options.exactGeometry ? "mesh-contact-plus-boolean-overlap" : "bbox-neighborhood",
11492
11747
  options,
11493
11748
  objectCount: objects.length,
11494
11749
  componentCount: components.length,
@@ -11689,6 +11944,185 @@ function analyzeDistanceInspection(entries, rawOptions = {}) {
11689
11944
  warnings: [...connectivity.warnings]
11690
11945
  };
11691
11946
  }
11947
+ const DEFAULT_CONTACT_TOLERANCE = 0.05;
11948
+ const DEFAULT_BED_TOLERANCE = 0.05;
11949
+ const DEFAULT_GROUND_Z = 0;
11950
+ const MIN_SHAPE_OVERLAP_VOLUME = 1e-9;
11951
+ let UnionFind$1 = class UnionFind2 {
11952
+ constructor(size) {
11953
+ __publicField(this, "parent");
11954
+ __publicField(this, "rank");
11955
+ this.parent = Array.from({ length: size }, (_, index) => index);
11956
+ this.rank = Array.from({ length: size }, () => 0);
11957
+ }
11958
+ find(value) {
11959
+ const parent = this.parent[value];
11960
+ if (parent === value) return value;
11961
+ const root = this.find(parent);
11962
+ this.parent[value] = root;
11963
+ return root;
11964
+ }
11965
+ union(a, b) {
11966
+ const rootA = this.find(a);
11967
+ const rootB = this.find(b);
11968
+ if (rootA === rootB) return;
11969
+ if (this.rank[rootA] < this.rank[rootB]) {
11970
+ this.parent[rootA] = rootB;
11971
+ return;
11972
+ }
11973
+ if (this.rank[rootA] > this.rank[rootB]) {
11974
+ this.parent[rootB] = rootA;
11975
+ return;
11976
+ }
11977
+ this.parent[rootB] = rootA;
11978
+ this.rank[rootA] += 1;
11979
+ }
11980
+ };
11981
+ function cloneVec3(value) {
11982
+ return [value[0], value[1], value[2]];
11983
+ }
11984
+ function emptyBBox$1() {
11985
+ return {
11986
+ min: [Infinity, Infinity, Infinity],
11987
+ max: [-Infinity, -Infinity, -Infinity]
11988
+ };
11989
+ }
11990
+ function expandBBox$1(target, min, max) {
11991
+ for (let axis = 0; axis < 3; axis += 1) {
11992
+ target.min[axis] = Math.min(target.min[axis], min[axis]);
11993
+ target.max[axis] = Math.max(target.max[axis], max[axis]);
11994
+ }
11995
+ }
11996
+ function bodyCountForEntry(entry) {
11997
+ if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
11998
+ return Math.max(0, Math.round(entry.bodyCount));
11999
+ }
12000
+ return 1;
12001
+ }
12002
+ function resolveNonNegative(value, fallback) {
12003
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
12004
+ }
12005
+ function resolveFinite(value, fallback) {
12006
+ return Number.isFinite(value) ? value : fallback;
12007
+ }
12008
+ function analyzeFloatingInspection(entries, rawOptions = {}) {
12009
+ const options = {
12010
+ contactTolerance: resolveNonNegative(rawOptions.contactTolerance, DEFAULT_CONTACT_TOLERANCE),
12011
+ bedTolerance: resolveNonNegative(rawOptions.bedTolerance, DEFAULT_BED_TOLERANCE),
12012
+ groundZ: resolveFinite(rawOptions.groundZ, DEFAULT_GROUND_Z)
12013
+ };
12014
+ const unionFind = new UnionFind$1(entries.length);
12015
+ const contacts = [];
12016
+ const warnings = [];
12017
+ const meshDataByIndex = /* @__PURE__ */ new Map();
12018
+ entries.forEach((entry, index2) => {
12019
+ const meshData = meshContactDataFor(entry);
12020
+ if (meshData) meshDataByIndex.set(index2, meshData);
12021
+ });
12022
+ const candidateTolerance = options.contactTolerance > 0 ? options.contactTolerance : Number.EPSILON;
12023
+ const index = new AabbSpatialIndex(entries);
12024
+ const contactOptions = {
12025
+ contactTolerance: options.contactTolerance,
12026
+ minOverlapVolume: MIN_SHAPE_OVERLAP_VOLUME,
12027
+ exactGeometry: true,
12028
+ mergeOverlappingBBoxes: false,
12029
+ mergeTouchingBBoxes: false
12030
+ };
12031
+ for (const pair of index.overlapPairs({ padding: candidateTolerance }).pairs) {
12032
+ const source = entries[pair.sourceIndex];
12033
+ const target = entries[pair.targetIndex];
12034
+ const detection = detectPhysicalContact(source, target, contactOptions, {
12035
+ sourceMesh: meshDataByIndex.get(pair.sourceIndex) ?? null,
12036
+ targetMesh: meshDataByIndex.get(pair.targetIndex) ?? null
12037
+ });
12038
+ if (detection.warning) warnings.push(detection.warning);
12039
+ if (!detection.contact) continue;
12040
+ unionFind.union(pair.sourceIndex, pair.targetIndex);
12041
+ contacts.push({
12042
+ sourceIndex: pair.sourceIndex,
12043
+ targetIndex: pair.targetIndex,
12044
+ sourceId: source.id,
12045
+ targetId: target.id,
12046
+ sourceName: source.name,
12047
+ targetName: target.name,
12048
+ gap: detection.contact.gap
12049
+ });
12050
+ }
12051
+ const objects = entries.map((entry, entryIndex) => ({
12052
+ index: entryIndex,
12053
+ id: entry.id,
12054
+ name: entry.name,
12055
+ groupName: entry.groupName,
12056
+ treePath: entry.treePath,
12057
+ mock: entry.mock === true,
12058
+ bodyCount: bodyCountForEntry(entry),
12059
+ bbox: {
12060
+ min: cloneVec3(entry.min),
12061
+ max: cloneVec3(entry.max)
12062
+ },
12063
+ componentIndex: 0,
12064
+ isRootComponent: false,
12065
+ isBedSupported: entry.min[2] <= options.groundZ + options.bedTolerance,
12066
+ isFloating: false
12067
+ }));
12068
+ const componentByRoot = /* @__PURE__ */ new Map();
12069
+ const rootToComponentIndex = /* @__PURE__ */ new Map();
12070
+ for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
12071
+ const root = unionFind.find(objectIndex);
12072
+ let component = componentByRoot.get(root);
12073
+ if (!component) {
12074
+ component = {
12075
+ index: componentByRoot.size + 1,
12076
+ objectIndexes: [],
12077
+ objectIds: [],
12078
+ objectNames: [],
12079
+ objectCount: 0,
12080
+ bodyCount: 0,
12081
+ bbox: emptyBBox$1(),
12082
+ isRoot: false,
12083
+ isBedSupported: false,
12084
+ isFloating: false
12085
+ };
12086
+ componentByRoot.set(root, component);
12087
+ rootToComponentIndex.set(root, component.index);
12088
+ }
12089
+ const object = objects[objectIndex];
12090
+ object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
12091
+ component.objectIndexes.push(object.index);
12092
+ component.objectIds.push(object.id);
12093
+ component.objectNames.push(object.name);
12094
+ component.objectCount += 1;
12095
+ component.bodyCount += object.bodyCount;
12096
+ component.isBedSupported || (component.isBedSupported = object.isBedSupported);
12097
+ expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
12098
+ }
12099
+ const components = [...componentByRoot.values()];
12100
+ for (const component of components) {
12101
+ component.isRoot = false;
12102
+ component.isFloating = !component.isBedSupported;
12103
+ }
12104
+ const componentByIndex = new Map(components.map((component) => [component.index, component]));
12105
+ for (const object of objects) {
12106
+ const component = componentByIndex.get(object.componentIndex);
12107
+ object.isRootComponent = false;
12108
+ object.isBedSupported = (component == null ? void 0 : component.isBedSupported) ?? object.isBedSupported;
12109
+ object.isFloating = (component == null ? void 0 : component.isFloating) ?? false;
12110
+ }
12111
+ return {
12112
+ method: "mesh-contact-ground-reachability",
12113
+ options,
12114
+ rootComponentIndex: null,
12115
+ objectCount: objects.length,
12116
+ componentCount: components.length,
12117
+ floatingComponentCount: components.filter((component) => component.isFloating).length,
12118
+ floatingObjectCount: objects.filter((object) => object.isFloating).length,
12119
+ floatingBodyCount: components.filter((component) => component.isFloating).reduce((total, component) => total + component.bodyCount, 0),
12120
+ contacts,
12121
+ objects,
12122
+ components,
12123
+ warnings
12124
+ };
12125
+ }
11692
12126
  const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
11693
12127
  smoothAngleDeg: 5,
11694
12128
  sharpAngleDeg: 30,
@@ -12086,7 +12520,8 @@ const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
12086
12520
  minThickness: 1.2,
12087
12521
  warnThickness: 2,
12088
12522
  maxThickness: 6,
12089
- maxSamplesPerObject: 5e3
12523
+ maxSamplesPerObject: 5e3,
12524
+ contactTolerance: DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance
12090
12525
  };
12091
12526
  const THICKNESS_COLORS = {
12092
12527
  critical: [255, 28, 28],
@@ -12102,6 +12537,13 @@ function finitePositive(value, fallback, label) {
12102
12537
  }
12103
12538
  return value;
12104
12539
  }
12540
+ function finiteNonNegative(value, fallback, label) {
12541
+ if (value === void 0) return fallback;
12542
+ if (!Number.isFinite(value) || value < 0) {
12543
+ throw new Error(`${label} must be a non-negative finite number.`);
12544
+ }
12545
+ return value;
12546
+ }
12105
12547
  function resolveThicknessInspectionOptions(raw = {}) {
12106
12548
  const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
12107
12549
  const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
@@ -12111,6 +12553,11 @@ function resolveThicknessInspectionOptions(raw = {}) {
12111
12553
  DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
12112
12554
  "maxSamplesPerObject"
12113
12555
  );
12556
+ const contactTolerance = finiteNonNegative(
12557
+ raw.contactTolerance,
12558
+ DEFAULT_THICKNESS_INSPECTION_OPTIONS.contactTolerance,
12559
+ "contactTolerance"
12560
+ );
12114
12561
  if (minThickness > warnThickness) {
12115
12562
  throw new Error("minThickness must be less than or equal to warnThickness.");
12116
12563
  }
@@ -12121,7 +12568,8 @@ function resolveThicknessInspectionOptions(raw = {}) {
12121
12568
  minThickness,
12122
12569
  warnThickness,
12123
12570
  maxThickness,
12124
- maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
12571
+ maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject)),
12572
+ contactTolerance
12125
12573
  };
12126
12574
  }
12127
12575
  function lerp$1(a, b, t) {
@@ -12162,22 +12610,45 @@ function geometryMaxDimension(geometry) {
12162
12610
  box.getSize(size);
12163
12611
  return Math.max(1, size.x, size.y, size.z);
12164
12612
  }
12165
- function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
12613
+ function firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, jumpableMeshes, point, direction, epsilon, far, contactTolerance) {
12166
12614
  const origin = point.clone().addScaledVector(direction, epsilon);
12167
12615
  raycaster.set(origin, direction);
12168
12616
  raycaster.near = epsilon;
12169
12617
  raycaster.far = far;
12170
- const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
12171
- return hit ? hit.distance + epsilon : null;
12172
- }
12173
- function triangleThickness(raycaster, mesh, centroid, normal, epsilon, far) {
12174
- const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon, far);
12175
- const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon, far);
12618
+ const hits = raycaster.intersectObjects(rayTargetMeshes, false);
12619
+ for (const hit of hits) {
12620
+ if (hit.distance <= epsilon) continue;
12621
+ if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
12622
+ return hit.distance + epsilon;
12623
+ }
12624
+ return null;
12625
+ }
12626
+ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid, normal, epsilon, far, contactTolerance) {
12627
+ const forward = firstOppositeSurfaceDistance(
12628
+ raycaster,
12629
+ rayTargetMeshes,
12630
+ jumpableMeshes,
12631
+ centroid,
12632
+ normal,
12633
+ epsilon,
12634
+ far,
12635
+ contactTolerance
12636
+ );
12637
+ const backward = firstOppositeSurfaceDistance(
12638
+ raycaster,
12639
+ rayTargetMeshes,
12640
+ jumpableMeshes,
12641
+ centroid,
12642
+ normal.clone().negate(),
12643
+ epsilon,
12644
+ far,
12645
+ contactTolerance
12646
+ );
12176
12647
  if (forward == null) return backward;
12177
12648
  if (backward == null) return forward;
12178
12649
  return Math.min(forward, backward);
12179
12650
  }
12180
- function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
12651
+ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
12181
12652
  const options = resolveThicknessInspectionOptions(rawOptions);
12182
12653
  const geometry = cloneGeometryForFaceColors(sourceGeometry);
12183
12654
  const position = geometry.getAttribute("position");
@@ -12195,7 +12666,8 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
12195
12666
  const triangleCount = Math.floor(position.count / 3);
12196
12667
  const surfaceTriangles = readSurfaceTriangles(position);
12197
12668
  const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
12198
- const maxDim = geometryMaxDimension(geometry);
12669
+ const connectedGeometries = context.connectedGeometries ?? [];
12670
+ const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
12199
12671
  const epsilon = Math.max(1e-4, maxDim * 1e-6);
12200
12672
  const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
12201
12673
  const colors = new Float32Array(position.count * 3);
@@ -12204,7 +12676,15 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
12204
12676
  const pointSamples = [];
12205
12677
  const warnings = [];
12206
12678
  const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
12207
- const rayMesh = new Mesh(geometry, rayMaterial);
12679
+ const rayTargets = [
12680
+ { mesh: new Mesh(geometry, rayMaterial), jumpable: false },
12681
+ ...connectedGeometries.map((connectedGeometry) => ({
12682
+ mesh: new Mesh(connectedGeometry, rayMaterial),
12683
+ jumpable: true
12684
+ }))
12685
+ ];
12686
+ const rayTargetMeshes = rayTargets.map((target) => target.mesh);
12687
+ const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
12208
12688
  const raycaster = new Raycaster();
12209
12689
  if (surfaceTriangles.length === 0) {
12210
12690
  warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
@@ -12216,7 +12696,16 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
12216
12696
  const sampledTriangleIndexes = /* @__PURE__ */ new Set();
12217
12697
  for (const sample of surfaceSamples) {
12218
12698
  sampledTriangleIndexes.add(sample.triangle.index);
12219
- const thickness = triangleThickness(raycaster, rayMesh, sample.position, sample.normal, epsilon, far);
12699
+ const thickness = triangleThickness(
12700
+ raycaster,
12701
+ rayTargetMeshes,
12702
+ jumpableMeshes,
12703
+ sample.position,
12704
+ sample.normal,
12705
+ epsilon,
12706
+ far,
12707
+ options.contactTolerance
12708
+ );
12220
12709
  samples.push({ thickness, area: sample.area });
12221
12710
  const previous = triangleThicknessValues[sample.triangle.index];
12222
12711
  if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
@@ -12254,7 +12743,7 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
12254
12743
  };
12255
12744
  }
12256
12745
  const DEFAULT_VERTEX_TOLERANCE = 1e-5;
12257
- class UnionFind2 {
12746
+ class UnionFind3 {
12258
12747
  constructor(size) {
12259
12748
  __publicField(this, "parent");
12260
12749
  __publicField(this, "rank");
@@ -12313,7 +12802,7 @@ function analyzeMeshConnectedComponents(positions, vertexTolerance = DEFAULT_VER
12313
12802
  const vertexComponentIndexes = new Int32Array(vertexCount);
12314
12803
  if (triangleCount === 0) return { components: [], vertexComponentIndexes };
12315
12804
  const tolerance = Number.isFinite(vertexTolerance) && vertexTolerance > 0 ? vertexTolerance : DEFAULT_VERTEX_TOLERANCE;
12316
- const unionFind = new UnionFind2(triangleCount);
12805
+ const unionFind = new UnionFind3(triangleCount);
12317
12806
  const vertexOwnerByKey = /* @__PURE__ */ new Map();
12318
12807
  for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
12319
12808
  for (let corner = 0; corner < 3; corner += 1) {
@@ -12364,12 +12853,25 @@ function objectConnectivityEntry(object) {
12364
12853
  shape: object.shape,
12365
12854
  min: object.min,
12366
12855
  max: object.max,
12856
+ positions: object.positions,
12367
12857
  groupName: object.groupName,
12368
12858
  treePath: object.treePath,
12369
12859
  mock: object.mock,
12370
12860
  bodyCount: 1
12371
12861
  };
12372
12862
  }
12863
+ function componentPositions(sourcePositions, component) {
12864
+ const out = new Float32Array(component.triangleIndexes.length * 9);
12865
+ let outOffset = 0;
12866
+ for (const triangleIndex of component.triangleIndexes) {
12867
+ const sourceOffset = triangleIndex * 9;
12868
+ for (let offset = 0; offset < 9; offset += 1) {
12869
+ out[outOffset + offset] = sourcePositions[sourceOffset + offset];
12870
+ }
12871
+ outOffset += 9;
12872
+ }
12873
+ return out;
12874
+ }
12373
12875
  function buildMeshBodyConnectivityInput(objects, options) {
12374
12876
  const entries = [];
12375
12877
  const bodyIdsByObjectId = /* @__PURE__ */ new Map();
@@ -12393,6 +12895,7 @@ function buildMeshBodyConnectivityInput(objects, options) {
12393
12895
  shape: options.bodyShape(object, component),
12394
12896
  min: component.bbox.min,
12395
12897
  max: component.bbox.max,
12898
+ positions: componentPositions(object.positions, component),
12396
12899
  groupName: object.groupName,
12397
12900
  treePath: object.treePath,
12398
12901
  mock: object.mock,
@@ -12427,6 +12930,8 @@ function meshVertexColorBuffersFor(input, colorForEntryId) {
12427
12930
  }
12428
12931
  return out;
12429
12932
  }
12933
+ const FLOATING_HIGHLIGHT_COLOR = [255, 68, 16];
12934
+ const FLOATING_HIDDEN_COLOR = [0, 0, 0];
12430
12935
  const MASK_PALETTE = [
12431
12936
  [230, 25, 75],
12432
12937
  [60, 180, 75],
@@ -12475,6 +12980,149 @@ function distanceColorForRootDistance(distance, maxDistance) {
12475
12980
  if (t <= 0.5) return rgbToHex(lerpRgb(DISTANCE_NEAR_COLOR, DISTANCE_MID_COLOR, t * 2));
12476
12981
  return rgbToHex(lerpRgb(DISTANCE_MID_COLOR, DISTANCE_FAR_COLOR, (t - 0.5) * 2));
12477
12982
  }
12983
+ const MIN_FIELD_GRID_SIZE = 18;
12984
+ const MAX_FIELD_GRID_SIZE = 32;
12985
+ const MAX_BLEND_SAMPLES = 12;
12986
+ const MAX_SEARCH_RADIUS = 5;
12987
+ const SAMPLE_RING_OFFSETS = [];
12988
+ for (let radius = 0; radius <= MAX_SEARCH_RADIUS; radius += 1) {
12989
+ const ring = [];
12990
+ for (let x = -radius; x <= radius; x += 1) {
12991
+ for (let y = -radius; y <= radius; y += 1) {
12992
+ for (let z = -radius; z <= radius; z += 1) {
12993
+ if (Math.max(Math.abs(x), Math.abs(y), Math.abs(z)) === radius) ring.push([x, y, z]);
12994
+ }
12995
+ }
12996
+ }
12997
+ SAMPLE_RING_OFFSETS.push(ring);
12998
+ }
12999
+ function gridKey(x, y, z) {
13000
+ return `${x},${y},${z}`;
13001
+ }
13002
+ function buildGeometryBounds(geometry) {
13003
+ const position = geometry.getAttribute("position");
13004
+ if (!position || position.count === 0) return null;
13005
+ const bounds = new Box3().setFromBufferAttribute(position);
13006
+ if (bounds.isEmpty()) return null;
13007
+ const size = bounds.getSize(new Vector3());
13008
+ const pad = Math.max(size.x, size.y, size.z, 1) * 1e-5;
13009
+ bounds.expandByScalar(pad);
13010
+ return bounds;
13011
+ }
13012
+ function buildSampleGrid(pointCloud) {
13013
+ const sampleCount = Math.floor(pointCloud.positions.length / 3);
13014
+ if (sampleCount <= 0) return null;
13015
+ const bounds = new Box3();
13016
+ const point = new Vector3();
13017
+ for (let sample = 0; sample < sampleCount; sample += 1) {
13018
+ const offset = sample * 3;
13019
+ bounds.expandByPoint(point.set(pointCloud.positions[offset], pointCloud.positions[offset + 1], pointCloud.positions[offset + 2]));
13020
+ }
13021
+ const size = bounds.getSize(new Vector3());
13022
+ const maxDim = Math.max(size.x, size.y, size.z);
13023
+ const cellSize = maxDim > 0 ? Math.max(maxDim / Math.cbrt(sampleCount), 1e-6) : 1;
13024
+ const cells = /* @__PURE__ */ new Map();
13025
+ for (let sample = 0; sample < sampleCount; sample += 1) {
13026
+ const offset = sample * 3;
13027
+ const x = Math.floor((pointCloud.positions[offset] - bounds.min.x) / cellSize);
13028
+ const y = Math.floor((pointCloud.positions[offset + 1] - bounds.min.y) / cellSize);
13029
+ const z = Math.floor((pointCloud.positions[offset + 2] - bounds.min.z) / cellSize);
13030
+ const key = gridKey(x, y, z);
13031
+ const entries = cells.get(key);
13032
+ if (entries) {
13033
+ entries.push(sample);
13034
+ } else {
13035
+ cells.set(key, [sample]);
13036
+ }
13037
+ }
13038
+ return { origin: bounds.min, cellSize, cells };
13039
+ }
13040
+ function fieldGridSizeForSampleCount(sampleCount) {
13041
+ if (sampleCount <= 0) return MIN_FIELD_GRID_SIZE;
13042
+ const scaled = Math.ceil(Math.cbrt(sampleCount) * 2.6);
13043
+ return Math.max(MIN_FIELD_GRID_SIZE, Math.min(MAX_FIELD_GRID_SIZE, scaled));
13044
+ }
13045
+ function distanceSquaredToSample(pointCloud, sample, point) {
13046
+ const offset = sample * 3;
13047
+ return (pointCloud.positions[offset] - point.x) ** 2 + (pointCloud.positions[offset + 1] - point.y) ** 2 + (pointCloud.positions[offset + 2] - point.z) ** 2;
13048
+ }
13049
+ function nearestSamples(point, pointCloud, sampleGrid) {
13050
+ const baseX = Math.floor((point.x - sampleGrid.origin.x) / sampleGrid.cellSize);
13051
+ const baseY = Math.floor((point.y - sampleGrid.origin.y) / sampleGrid.cellSize);
13052
+ const baseZ = Math.floor((point.z - sampleGrid.origin.z) / sampleGrid.cellSize);
13053
+ const found = [];
13054
+ for (const ring of SAMPLE_RING_OFFSETS) {
13055
+ for (const [dx, dy, dz] of ring) {
13056
+ const entries = sampleGrid.cells.get(gridKey(baseX + dx, baseY + dy, baseZ + dz));
13057
+ if (!entries) continue;
13058
+ for (const sample of entries) {
13059
+ found.push({ sample, distSq: distanceSquaredToSample(pointCloud, sample, point) });
13060
+ }
13061
+ }
13062
+ if (found.length >= MAX_BLEND_SAMPLES) break;
13063
+ }
13064
+ if (found.length === 0) {
13065
+ const sampleCount = Math.floor(pointCloud.positions.length / 3);
13066
+ for (let sample = 0; sample < sampleCount; sample += 1) {
13067
+ found.push({ sample, distSq: distanceSquaredToSample(pointCloud, sample, point) });
13068
+ }
13069
+ }
13070
+ found.sort((a, b) => a.distSq - b.distSq);
13071
+ return found.slice(0, MAX_BLEND_SAMPLES);
13072
+ }
13073
+ function blendedColorAt(point, pointCloud, sampleGrid) {
13074
+ const samples = nearestSamples(point, pointCloud, sampleGrid);
13075
+ if (samples.length === 0) return [90, 90, 90];
13076
+ const sigma = Math.max(sampleGrid.cellSize * 1.65, 1e-6);
13077
+ const sigmaSq = sigma * sigma;
13078
+ let weightSum = 0;
13079
+ let r = 0;
13080
+ let g = 0;
13081
+ let b = 0;
13082
+ for (const { sample, distSq: distSq2 } of samples) {
13083
+ const colorOffset = sample * 3;
13084
+ const weight = Math.exp(-distSq2 / (2 * sigmaSq)) + 1e-4 / Math.max(distSq2, 1e-8);
13085
+ weightSum += weight;
13086
+ r += (pointCloud.colors[colorOffset] ?? 0.35) * 255 * weight;
13087
+ g += (pointCloud.colors[colorOffset + 1] ?? 0.35) * 255 * weight;
13088
+ b += (pointCloud.colors[colorOffset + 2] ?? 0.35) * 255 * weight;
13089
+ }
13090
+ if (weightSum <= 0) return [90, 90, 90];
13091
+ return [r / weightSum, g / weightSum, b / weightSum];
13092
+ }
13093
+ function buildInspectHeatmapFieldData(geometry, pointCloud) {
13094
+ const bounds = buildGeometryBounds(geometry);
13095
+ const sampleGrid = buildSampleGrid(pointCloud);
13096
+ if (!bounds || !sampleGrid) return null;
13097
+ const gridSize = fieldGridSizeForSampleCount(Math.floor(pointCloud.positions.length / 3));
13098
+ const boundsSize = bounds.getSize(new Vector3());
13099
+ const data = new Uint8Array(gridSize * gridSize * gridSize * 4);
13100
+ const point = new Vector3();
13101
+ let dataOffset = 0;
13102
+ for (let y = 0; y < gridSize; y += 1) {
13103
+ for (let z = 0; z < gridSize; z += 1) {
13104
+ for (let x = 0; x < gridSize; x += 1) {
13105
+ point.set(
13106
+ bounds.min.x + boundsSize.x * x / (gridSize - 1),
13107
+ bounds.min.y + boundsSize.y * y / (gridSize - 1),
13108
+ bounds.min.z + boundsSize.z * z / (gridSize - 1)
13109
+ );
13110
+ const [r, g, b] = blendedColorAt(point, pointCloud, sampleGrid);
13111
+ data[dataOffset] = Math.max(0, Math.min(255, Math.round(r)));
13112
+ data[dataOffset + 1] = Math.max(0, Math.min(255, Math.round(g)));
13113
+ data[dataOffset + 2] = Math.max(0, Math.min(255, Math.round(b)));
13114
+ data[dataOffset + 3] = 255;
13115
+ dataOffset += 4;
13116
+ }
13117
+ }
13118
+ }
13119
+ return {
13120
+ data,
13121
+ boundsMin: [bounds.min.x, bounds.min.y, bounds.min.z],
13122
+ boundsSize: [boundsSize.x, boundsSize.y, boundsSize.z],
13123
+ gridSize
13124
+ };
13125
+ }
12478
13126
  const workerScope = self;
12479
13127
  class EmptyIntersectionShape {
12480
13128
  intersect() {
@@ -12524,6 +13172,7 @@ function objectColorsFor(objects, input, colorByEntryId) {
12524
13172
  }
12525
13173
  function analyzeScalarChannel(request) {
12526
13174
  const pointObjects = [];
13175
+ const heatmapFieldObjects = [];
12527
13176
  const warnings = [];
12528
13177
  for (const object of request.objects) {
12529
13178
  if (!object.positions || object.positions.length < 9) continue;
@@ -12532,6 +13181,13 @@ function analyzeScalarChannel(request) {
12532
13181
  const analysis = request.channel === "thickness" ? analyzeThicknessGeometry(geometry, request.thickness) : analyzeRoughnessGeometry(geometry, request.roughness);
12533
13182
  analysis.warnings.forEach((warning) => warnings.push(`${object.name}: ${warning}`));
12534
13183
  const buffers = pointBuffers(analysis.pointSamples);
13184
+ const field = buildInspectHeatmapFieldData(analysis.geometry, buffers);
13185
+ if (field) {
13186
+ heatmapFieldObjects.push({
13187
+ objectId: object.id,
13188
+ ...field
13189
+ });
13190
+ }
12535
13191
  pointObjects.push({
12536
13192
  objectId: object.id,
12537
13193
  sampleCount: analysis.pointSamples.length,
@@ -12547,6 +13203,7 @@ function analyzeScalarChannel(request) {
12547
13203
  objectColors: {},
12548
13204
  pointObjects,
12549
13205
  meshColorObjects: [],
13206
+ heatmapFieldObjects,
12550
13207
  warnings
12551
13208
  };
12552
13209
  }
@@ -12576,6 +13233,21 @@ function analyzeConnectivityChannel(request) {
12576
13233
  objectColors: objectColorsFor(request.objects, bodyInput, colorByEntryId2),
12577
13234
  pointObjects: [],
12578
13235
  meshColorObjects: meshColorObjectsFor(bodyInput, colorByEntryId2),
13236
+ heatmapFieldObjects: [],
13237
+ warnings: report2.warnings
13238
+ };
13239
+ }
13240
+ if (request.channel === "floating") {
13241
+ const report2 = analyzeFloatingInspection(entries, { groundZ: request.groundZ });
13242
+ const colorByEntryId2 = new Map(
13243
+ report2.objects.map((object) => [object.id, rgbToHex(object.isFloating ? FLOATING_HIGHLIGHT_COLOR : FLOATING_HIDDEN_COLOR)])
13244
+ );
13245
+ return {
13246
+ channel: request.channel,
13247
+ objectColors: objectColorsFor(request.objects, bodyInput, colorByEntryId2),
13248
+ pointObjects: [],
13249
+ meshColorObjects: meshColorObjectsFor(bodyInput, colorByEntryId2),
13250
+ heatmapFieldObjects: [],
12579
13251
  warnings: report2.warnings
12580
13252
  };
12581
13253
  }
@@ -12586,13 +13258,15 @@ function analyzeConnectivityChannel(request) {
12586
13258
  objectColors: objectColorsFor(request.objects, bodyInput, colorByEntryId),
12587
13259
  pointObjects: [],
12588
13260
  meshColorObjects: meshColorObjectsFor(bodyInput, colorByEntryId),
13261
+ heatmapFieldObjects: [],
12589
13262
  warnings: report.warnings
12590
13263
  };
12591
13264
  }
12592
13265
  function transferFor(result) {
12593
13266
  return [
12594
13267
  ...result.pointObjects.flatMap((object) => [object.positions.buffer, object.colors.buffer]),
12595
- ...result.meshColorObjects.map((object) => object.colors.buffer)
13268
+ ...result.meshColorObjects.map((object) => object.colors.buffer),
13269
+ ...result.heatmapFieldObjects.map((object) => object.data.buffer)
12596
13270
  ];
12597
13271
  }
12598
13272
  self.onmessage = (event) => {