forgecad 0.9.7 → 0.9.8

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-CXaVLMiV.js} +1 -1
  3. package/dist/assets/{BlogPage-CI_P0_Pf.js → BlogPage-Crpr3JjH.js} +1 -1
  4. package/dist/assets/{DocsPage-DLhIIZyJ.js → DocsPage-CNBKuitP.js} +2 -2
  5. package/dist/assets/{EditorApp-DfFT2Dn8.css → EditorApp-D11wL4Qn.css} +51 -0
  6. package/dist/assets/{EditorApp-BujZvuwX.js → EditorApp-DVMnXOmO.js} +151 -9
  7. package/dist/assets/{EmbedViewer-0S0qXKog.js → EmbedViewer-KXFLSnpo.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-O_yMtAri.js → LandingPageProofDriven-2q2sn7aW.js} +1 -1
  9. package/dist/assets/{PricingPage-DGkX3Ahr.js → PricingPage-CVvgdv0i.js} +1 -1
  10. package/dist/assets/{SettingsPage-DBsqTB_y.js → SettingsPage-BVj1FtEv.js} +1 -1
  11. package/dist/assets/__vite-browser-external-Dhvy_jtL.js +4 -0
  12. package/dist/assets/{app-BE2nD6Yz.js → app-Dn4EwHhN.js} +707 -458
  13. package/dist/assets/cli/{render-iP9qh475.js → render-BI3gLMXz.js} +1011 -145
  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-sJ-axdXM.js → manifold-BVi4_OeB.js} +1 -1
  18. package/dist/assets/{manifold-DjYsd7A_.js → manifold-C6-sZYQN.js} +2 -2
  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-BIvOkPK3.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-iBAeF8RM.js} +483 -71
  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
@@ -1,8 +1,8 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { D as DoubleSide, bI as initSolverWasm, bH as initKernel, S as Scene, bJ as BoxGeometry, be as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bK as localAabbPlaneRelation, h as Vector2, bL as ShapeUtils, bM as aabbInteriorOverlaps, bN as aabbOverlapVolume, bO as AabbSpatialIndex, bP as aabbGaps, g as Vector3, R as Raycaster, aU as BufferAttribute, a0 as MathUtils, G as Box3, e as Color, aC as resolveForgeRenderStyle, ba as getRenderStylePreset, ax as setParamOverrides, b7 as runScript, bQ as Group, b3 as shapeToGeometry, b8 as MeshPhysicalMaterial, bb as AdditiveBlending, aH as LineBasicMaterial, b9 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bR as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bS as parseCameraCliSpec, bT as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bA as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bh as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bg as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bB as resolveJointAnimation, bC as resolveJointViewValues, bU as PointsMaterial, bV as Points, b2 as analyzeCollisionIntersections, bW as serializeCollisionFinding, bX as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-BdTjyVfs.js";
5
- import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-Bngp5MrQ.js";
4
+ import { D as DoubleSide, bT as initSolverWasm, bS as initKernel, S as Scene, bU as BoxGeometry, bo as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bV as localAabbPlaneRelation, h as Vector2, bW as ShapeUtils, bX as aabbGaps, bY as aabbInteriorOverlaps, bZ as aabbOverlapVolume, b_ as AabbSpatialIndex, g as Vector3, R as Raycaster, aU as BufferAttribute, a0 as MathUtils, G as Box3, e as Color, aC as resolveForgeRenderStyle, bb as getRenderStylePreset, ax as setParamOverrides, b7 as runScript, b$ as Group, b3 as shapeToGeometry, b8 as MeshPhysicalMaterial, bc as AdditiveBlending, aH as LineBasicMaterial, b9 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bi as ZEBRA_STRIPE_FRAGMENT_SHADER, bj as ZEBRA_STRIPE_VERTEX_SHADER, bd as ZEBRA_STRIPE_SOFTNESS, be as ZEBRA_STRIPE_SCALE, bf as ZEBRA_LIGHT_COLOR, bg as ZEBRA_DARK_COLOR, bh as ZEBRA_ACCENT_COLOR, ba as geometryWithVisibleVertexColors, c0 as intersectWithPlane, c1 as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, c2 as parseCameraCliSpec, c3 as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bK as findJointAnimationClip, p as Plane, bk as SURFACE_FIELD_FRAGMENT_SHADER, bl as SURFACE_FIELD_VERTEX_SHADER, Y as Vector4, $ as Matrix4, br as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bq as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bL as resolveJointAnimation, bM as resolveJointViewValues, c4 as PointsMaterial, c5 as Points, b2 as analyzeCollisionIntersections, c6 as serializeCollisionFinding, c7 as worldAuthorPlaneToLocal, a$ as SpotLight, bR as resolveScalarSceneSampleBudget } from "../scalar-sampling-budget-iBAeF8RM.js";
5
+ import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-BIvOkPK3.js";
6
6
  const CAD_MATERIAL_PROPS = {
7
7
  color: 6003669,
8
8
  metalness: 0.05,
@@ -457,13 +457,333 @@ function computeMeshSectionCap(mesh, planeInput) {
457
457
  warnings: stitched.warnings.length > 0 ? stitched.warnings : void 0
458
458
  };
459
459
  }
460
+ const AXIS_NAMES = ["x", "y", "z"];
461
+ function nearestBoundaryGap(a, b, axis) {
462
+ return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
463
+ }
464
+ function hasPositiveGap(gaps) {
465
+ return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
466
+ }
467
+ function contactFromBBoxes(a, b, tolerance) {
468
+ const gaps = aabbGaps(a, b);
469
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
470
+ if (largestGap > tolerance) return { touching: false, gap: largestGap };
471
+ const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
472
+ if (separatedAxes.length > 0) {
473
+ const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
474
+ return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
475
+ }
476
+ const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
477
+ axis,
478
+ axisName,
479
+ gap: nearestBoundaryGap(a, b, axis)
480
+ })).filter((entry) => entry.gap <= tolerance);
481
+ if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
482
+ const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
483
+ return { touching: true, gap: nearest.gap, axis: nearest.axisName };
484
+ }
485
+ function isFiniteTriangle(positions, offset) {
486
+ for (let index = 0; index < 9; index += 1) {
487
+ if (!Number.isFinite(positions[offset + index])) return false;
488
+ }
489
+ return true;
490
+ }
491
+ function triangleRef(positions, offset) {
492
+ if (!isFiniteTriangle(positions, offset)) return null;
493
+ const ax = positions[offset];
494
+ const ay = positions[offset + 1];
495
+ const az = positions[offset + 2];
496
+ const bx = positions[offset + 3];
497
+ const by = positions[offset + 4];
498
+ const bz = positions[offset + 5];
499
+ const cx = positions[offset + 6];
500
+ const cy = positions[offset + 7];
501
+ const cz = positions[offset + 8];
502
+ return {
503
+ offset,
504
+ min: [Math.min(ax, bx, cx), Math.min(ay, by, cy), Math.min(az, bz, cz)],
505
+ max: [Math.max(ax, bx, cx), Math.max(ay, by, cy), Math.max(az, bz, cz)]
506
+ };
507
+ }
508
+ function meshContactDataFor(entry) {
509
+ const positions = entry.positions;
510
+ if (!positions || positions.length < 9) return null;
511
+ const triangleCount = Math.floor(positions.length / 9);
512
+ const triangles = [];
513
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
514
+ const triangle = triangleRef(positions, triangleIndex * 9);
515
+ if (triangle) triangles.push(triangle);
516
+ }
517
+ if (triangles.length === 0) return null;
518
+ return {
519
+ positions,
520
+ vertexCount: triangleCount * 3,
521
+ triangles
522
+ };
523
+ }
524
+ function distSq(px, py, pz, qx, qy, qz) {
525
+ const dx = px - qx;
526
+ const dy = py - qy;
527
+ const dz = pz - qz;
528
+ return dx * dx + dy * dy + dz * dz;
529
+ }
530
+ function pointSegmentDistanceSq(point, start, end) {
531
+ const dx = end[0] - start[0];
532
+ const dy = end[1] - start[1];
533
+ const dz = end[2] - start[2];
534
+ const lengthSq = dx * dx + dy * dy + dz * dz;
535
+ if (lengthSq <= 1e-20) return distSq(point[0], point[1], point[2], start[0], start[1], start[2]);
536
+ const t = Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy + (point[2] - start[2]) * dz) / lengthSq));
537
+ return distSq(point[0], point[1], point[2], start[0] + t * dx, start[1] + t * dy, start[2] + t * dz);
538
+ }
539
+ function pointTriangleDistanceSq(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz) {
540
+ const abx = bx - ax;
541
+ const aby = by - ay;
542
+ const abz = bz - az;
543
+ const acx = cx - ax;
544
+ const acy = cy - ay;
545
+ const acz = cz - az;
546
+ const apx = px - ax;
547
+ const apy = py - ay;
548
+ const apz = pz - az;
549
+ const d1 = abx * apx + aby * apy + abz * apz;
550
+ const d2 = acx * apx + acy * apy + acz * apz;
551
+ if (d1 <= 0 && d2 <= 0) return distSq(px, py, pz, ax, ay, az);
552
+ const bpx = px - bx;
553
+ const bpy = py - by;
554
+ const bpz = pz - bz;
555
+ const d3 = abx * bpx + aby * bpy + abz * bpz;
556
+ const d4 = acx * bpx + acy * bpy + acz * bpz;
557
+ if (d3 >= 0 && d4 <= d3) return distSq(px, py, pz, bx, by, bz);
558
+ const vc = d1 * d4 - d3 * d2;
559
+ if (vc <= 0 && d1 >= 0 && d3 <= 0) {
560
+ const v2 = d1 / (d1 - d3);
561
+ return distSq(px, py, pz, ax + v2 * abx, ay + v2 * aby, az + v2 * abz);
562
+ }
563
+ const cpx = px - cx;
564
+ const cpy = py - cy;
565
+ const cpz = pz - cz;
566
+ const d5 = abx * cpx + aby * cpy + abz * cpz;
567
+ const d6 = acx * cpx + acy * cpy + acz * cpz;
568
+ if (d6 >= 0 && d5 <= d6) return distSq(px, py, pz, cx, cy, cz);
569
+ const vb = d5 * d2 - d1 * d6;
570
+ if (vb <= 0 && d2 >= 0 && d6 <= 0) {
571
+ const w2 = d2 / (d2 - d6);
572
+ return distSq(px, py, pz, ax + w2 * acx, ay + w2 * acy, az + w2 * acz);
573
+ }
574
+ const va = d3 * d6 - d5 * d4;
575
+ if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
576
+ const bcx = cx - bx;
577
+ const bcy = cy - by;
578
+ const bcz = cz - bz;
579
+ const w2 = (d4 - d3) / (d4 - d3 + d5 - d6);
580
+ return distSq(px, py, pz, bx + w2 * bcx, by + w2 * bcy, bz + w2 * bcz);
581
+ }
582
+ const barycentricTotal = va + vb + vc;
583
+ if (!Number.isFinite(barycentricTotal) || Math.abs(barycentricTotal) < 1e-20) {
584
+ return Math.min(distSq(px, py, pz, ax, ay, az), distSq(px, py, pz, bx, by, bz), distSq(px, py, pz, cx, cy, cz));
585
+ }
586
+ const denom = 1 / barycentricTotal;
587
+ const v = vb * denom;
588
+ const w = vc * denom;
589
+ return distSq(px, py, pz, ax + abx * v + acx * w, ay + aby * v + acy * w, az + abz * v + acz * w);
590
+ }
591
+ function segmentSegmentDistanceSq(a0, a1, b0, b1) {
592
+ const ux = a1[0] - a0[0];
593
+ const uy = a1[1] - a0[1];
594
+ const uz = a1[2] - a0[2];
595
+ const vx = b1[0] - b0[0];
596
+ const vy = b1[1] - b0[1];
597
+ const vz = b1[2] - b0[2];
598
+ const wx = a0[0] - b0[0];
599
+ const wy = a0[1] - b0[1];
600
+ const wz = a0[2] - b0[2];
601
+ const a = ux * ux + uy * uy + uz * uz;
602
+ const b = ux * vx + uy * vy + uz * vz;
603
+ const c = vx * vx + vy * vy + vz * vz;
604
+ const d = ux * wx + uy * wy + uz * wz;
605
+ const e = vx * wx + vy * wy + vz * wz;
606
+ const denom = a * c - b * b;
607
+ let sNumerator = denom;
608
+ let sDenominator = denom;
609
+ let tNumerator = denom;
610
+ let tDenominator = denom;
611
+ if (a <= 1e-20) return pointSegmentDistanceSq(a0, b0, b1);
612
+ if (c <= 1e-20) return pointSegmentDistanceSq(b0, a0, a1);
613
+ if (denom < 1e-20) {
614
+ sNumerator = 0;
615
+ sDenominator = 1;
616
+ tNumerator = e;
617
+ tDenominator = c;
618
+ } else {
619
+ sNumerator = b * e - c * d;
620
+ tNumerator = a * e - b * d;
621
+ if (sNumerator < 0) {
622
+ sNumerator = 0;
623
+ tNumerator = e;
624
+ tDenominator = c;
625
+ } else if (sNumerator > sDenominator) {
626
+ sNumerator = sDenominator;
627
+ tNumerator = e + b;
628
+ tDenominator = c;
629
+ }
630
+ }
631
+ if (tNumerator < 0) {
632
+ tNumerator = 0;
633
+ if (-d < 0) {
634
+ sNumerator = 0;
635
+ } else if (-d > a) {
636
+ sNumerator = sDenominator;
637
+ } else {
638
+ sNumerator = -d;
639
+ sDenominator = a;
640
+ }
641
+ } else if (tNumerator > tDenominator) {
642
+ tNumerator = tDenominator;
643
+ if (-d + b < 0) {
644
+ sNumerator = 0;
645
+ } else if (-d + b > a) {
646
+ sNumerator = sDenominator;
647
+ } else {
648
+ sNumerator = -d + b;
649
+ sDenominator = a;
650
+ }
651
+ }
652
+ const s = Math.abs(sNumerator) < 1e-20 ? 0 : sNumerator / sDenominator;
653
+ const t = Math.abs(tNumerator) < 1e-20 ? 0 : tNumerator / tDenominator;
654
+ const dx = wx + s * ux - t * vx;
655
+ const dy = wy + s * uy - t * vy;
656
+ const dz = wz + s * uz - t * vz;
657
+ return dx * dx + dy * dy + dz * dz;
658
+ }
659
+ function trianglePoint(positions, triangle, corner) {
660
+ const offset = triangle.offset + corner * 3;
661
+ return [positions[offset], positions[offset + 1], positions[offset + 2]];
662
+ }
663
+ function bboxDistanceSq(a, b) {
664
+ let total = 0;
665
+ for (let axis = 0; axis < 3; axis += 1) {
666
+ const gap = Math.max(0, a.min[axis] - b.max[axis], b.min[axis] - a.max[axis]);
667
+ total += gap * gap;
668
+ }
669
+ return total;
670
+ }
671
+ function triangleDistanceSq(aPositions, a, bPositions, b) {
672
+ const a0 = trianglePoint(aPositions, a, 0);
673
+ const a1 = trianglePoint(aPositions, a, 1);
674
+ const a2 = trianglePoint(aPositions, a, 2);
675
+ const b0 = trianglePoint(bPositions, b, 0);
676
+ const b1 = trianglePoint(bPositions, b, 1);
677
+ const b2 = trianglePoint(bPositions, b, 2);
678
+ return Math.min(
679
+ 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]),
680
+ 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]),
681
+ 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]),
682
+ 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]),
683
+ 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]),
684
+ 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]),
685
+ segmentSegmentDistanceSq(a0, a1, b0, b1),
686
+ segmentSegmentDistanceSq(a0, a1, b1, b2),
687
+ segmentSegmentDistanceSq(a0, a1, b2, b0),
688
+ segmentSegmentDistanceSq(a1, a2, b0, b1),
689
+ segmentSegmentDistanceSq(a1, a2, b1, b2),
690
+ segmentSegmentDistanceSq(a1, a2, b2, b0),
691
+ segmentSegmentDistanceSq(a2, a0, b0, b1),
692
+ segmentSegmentDistanceSq(a2, a0, b1, b2),
693
+ segmentSegmentDistanceSq(a2, a0, b2, b0)
694
+ );
695
+ }
696
+ function meshEntriesContactDistance(a, b, tolerance) {
697
+ const toleranceSq = tolerance * tolerance;
698
+ let bestDistanceSq = Infinity;
699
+ for (const aTriangle of a.triangles) {
700
+ for (const bTriangle of b.triangles) {
701
+ if (bboxDistanceSq(aTriangle, bTriangle) > toleranceSq) continue;
702
+ const distanceSq = triangleDistanceSq(a.positions, aTriangle, b.positions, bTriangle);
703
+ if (distanceSq > toleranceSq) continue;
704
+ if (distanceSq <= 1e-20) return 0;
705
+ bestDistanceSq = Math.min(bestDistanceSq, distanceSq);
706
+ }
707
+ }
708
+ return Number.isFinite(bestDistanceSq) ? Math.sqrt(bestDistanceSq) : null;
709
+ }
710
+ function intersectionVolume(a, b) {
711
+ try {
712
+ const hit = a.shape.intersect(b.shape);
713
+ if (hit.isEmpty()) return { volume: 0 };
714
+ const volume = hit.volume();
715
+ return { volume: Number.isFinite(volume) ? volume : 0 };
716
+ } catch (err) {
717
+ const message = err instanceof Error ? err.message : String(err);
718
+ return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
719
+ }
720
+ }
721
+ function detectPhysicalContact(a, b, options, meshCache = {}) {
722
+ const gaps = aabbGaps(a, b);
723
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
724
+ if (largestGap > options.contactTolerance) return { contact: null };
725
+ const sourceMesh = meshCache.sourceMesh !== void 0 ? meshCache.sourceMesh : meshContactDataFor(a);
726
+ const targetMesh = meshCache.targetMesh !== void 0 ? meshCache.targetMesh : meshContactDataFor(b);
727
+ if (sourceMesh && targetMesh) {
728
+ const distance = meshEntriesContactDistance(sourceMesh, targetMesh, options.contactTolerance);
729
+ if (distance != null) {
730
+ return {
731
+ contact: {
732
+ kind: "touching",
733
+ method: "mesh-surface-distance",
734
+ gap: distance
735
+ }
736
+ };
737
+ }
738
+ }
739
+ const bboxOverlaps = !hasPositiveGap(gaps) && aabbInteriorOverlaps(a, b);
740
+ if (options.exactGeometry && bboxOverlaps) {
741
+ if (aabbOverlapVolume(a, b) <= options.minOverlapVolume) return { contact: null };
742
+ const overlap = intersectionVolume(a, b);
743
+ if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
744
+ return {
745
+ contact: {
746
+ kind: "overlap",
747
+ method: "boolean-intersection",
748
+ gap: 0,
749
+ overlapVolume: overlap.volume
750
+ },
751
+ warning: overlap.warning
752
+ };
753
+ }
754
+ return { contact: null, warning: overlap.warning };
755
+ }
756
+ if (bboxOverlaps && options.mergeOverlappingBBoxes) {
757
+ return {
758
+ contact: {
759
+ kind: "overlap",
760
+ method: "bbox-overlap",
761
+ gap: 0,
762
+ overlapVolume: aabbOverlapVolume(a, b)
763
+ }
764
+ };
765
+ }
766
+ if (options.mergeTouchingBBoxes) {
767
+ const contact = contactFromBBoxes(a, b, options.contactTolerance);
768
+ if (contact.touching) {
769
+ return {
770
+ contact: {
771
+ kind: "touching",
772
+ method: "bbox-contact",
773
+ gap: contact.gap,
774
+ axis: contact.axis
775
+ }
776
+ };
777
+ }
778
+ }
779
+ return { contact: null };
780
+ }
460
781
  const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
461
782
  contactTolerance: 0.05,
462
783
  minOverlapVolume: 0.1,
463
784
  exactGeometry: true
464
785
  };
465
- const AXIS_NAMES = ["x", "y", "z"];
466
- let UnionFind$1 = class UnionFind {
786
+ let UnionFind$2 = class UnionFind {
467
787
  constructor(size) {
468
788
  __publicField(this, "parent");
469
789
  __publicField(this, "rank");
@@ -493,27 +813,21 @@ let UnionFind$1 = class UnionFind {
493
813
  this.rank[rootA] += 1;
494
814
  }
495
815
  };
496
- function cloneVec3(value) {
816
+ function cloneVec3$1(value) {
497
817
  return [value[0], value[1], value[2]];
498
818
  }
499
- function emptyBBox$1() {
819
+ function emptyBBox$2() {
500
820
  return {
501
821
  min: [Infinity, Infinity, Infinity],
502
822
  max: [-Infinity, -Infinity, -Infinity]
503
823
  };
504
824
  }
505
- function expandBBox$1(target, min, max) {
825
+ function expandBBox$2(target, min, max) {
506
826
  for (let axis = 0; axis < 3; axis += 1) {
507
827
  target.min[axis] = Math.min(target.min[axis], min[axis]);
508
828
  target.max[axis] = Math.max(target.max[axis], max[axis]);
509
829
  }
510
830
  }
511
- function nearestBoundaryGap(a, b, axis) {
512
- return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
513
- }
514
- function hasPositiveGap(gaps) {
515
- return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
516
- }
517
831
  function collectCandidatePairs(entries, tolerance) {
518
832
  if (entries.length < 2) return [];
519
833
  const index = new AabbSpatialIndex(entries);
@@ -525,36 +839,7 @@ function collectCandidatePairs(entries, tolerance) {
525
839
  pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
526
840
  return pairs;
527
841
  }
528
- function contactFromBBoxes(a, b, tolerance) {
529
- const gaps = aabbGaps(a, b);
530
- const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
531
- if (largestGap > tolerance) return { touching: false, gap: largestGap };
532
- const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
533
- if (separatedAxes.length > 0) {
534
- const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
535
- return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
536
- }
537
- const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
538
- axis,
539
- axisName,
540
- gap: nearestBoundaryGap(a, b, axis)
541
- })).filter((entry) => entry.gap <= tolerance);
542
- if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
543
- const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
544
- return { touching: true, gap: nearest.gap, axis: nearest.axisName };
545
- }
546
- function intersectionVolume(a, b) {
547
- try {
548
- const hit = a.shape.intersect(b.shape);
549
- if (hit.isEmpty()) return { volume: 0 };
550
- const volume = hit.volume();
551
- return { volume: Number.isFinite(volume) ? volume : 0 };
552
- } catch (err) {
553
- const message = err instanceof Error ? err.message : String(err);
554
- return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
555
- }
556
- }
557
- function bodyCountForEntry(entry) {
842
+ function bodyCountForEntry$1(entry) {
558
843
  if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
559
844
  return Math.max(0, Math.round(entry.bodyCount));
560
845
  }
@@ -584,53 +869,23 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
584
869
  };
585
870
  const warnings = [];
586
871
  const edges = [];
587
- const unionFind = new UnionFind$1(entries.length);
872
+ const unionFind = new UnionFind$2(entries.length);
873
+ const meshDataByIndex = /* @__PURE__ */ new Map();
874
+ entries.forEach((entry, index) => {
875
+ const meshData = meshContactDataFor(entry);
876
+ if (meshData) meshDataByIndex.set(index, meshData);
877
+ });
588
878
  for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
589
879
  const i = pair.sourceIndex;
590
880
  const j = pair.targetIndex;
591
- const a = entries[i];
592
- const b = entries[j];
593
- const bboxOverlaps = !hasPositiveGap(pair.gaps) && aabbInteriorOverlaps(a, b);
594
- if (options.exactGeometry && bboxOverlaps) {
595
- const overlap = intersectionVolume(a, b);
596
- if (overlap.warning) warnings.push(overlap.warning);
597
- if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
598
- unionFind.union(i, j);
599
- edges.push(
600
- makeEdge(entries, i, j, {
601
- kind: "overlap",
602
- method: "boolean-intersection",
603
- gap: 0,
604
- overlapVolume: overlap.volume
605
- })
606
- );
607
- continue;
608
- }
609
- continue;
610
- }
611
- if (bboxOverlaps && options.mergeOverlappingBBoxes) {
612
- unionFind.union(i, j);
613
- edges.push(
614
- makeEdge(entries, i, j, {
615
- kind: "overlap",
616
- method: "bbox-overlap",
617
- gap: 0,
618
- overlapVolume: aabbOverlapVolume(a, b)
619
- })
620
- );
621
- } else {
622
- const contact = contactFromBBoxes(a, b, options.contactTolerance);
623
- if (!contact.touching || !options.mergeTouchingBBoxes) continue;
624
- unionFind.union(i, j);
625
- edges.push(
626
- makeEdge(entries, i, j, {
627
- kind: "touching",
628
- method: "bbox-contact",
629
- gap: contact.gap,
630
- axis: contact.axis
631
- })
632
- );
633
- }
881
+ const detection = detectPhysicalContact(entries[i], entries[j], options, {
882
+ sourceMesh: meshDataByIndex.get(i) ?? null,
883
+ targetMesh: meshDataByIndex.get(j) ?? null
884
+ });
885
+ if (detection.warning) warnings.push(detection.warning);
886
+ if (!detection.contact) continue;
887
+ unionFind.union(i, j);
888
+ edges.push(makeEdge(entries, i, j, detection.contact));
634
889
  }
635
890
  const objects = entries.map((entry, index) => ({
636
891
  index,
@@ -639,10 +894,10 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
639
894
  groupName: entry.groupName,
640
895
  treePath: entry.treePath,
641
896
  mock: entry.mock === true,
642
- bodyCount: bodyCountForEntry(entry),
897
+ bodyCount: bodyCountForEntry$1(entry),
643
898
  bbox: {
644
- min: cloneVec3(entry.min),
645
- max: cloneVec3(entry.max)
899
+ min: cloneVec3$1(entry.min),
900
+ max: cloneVec3$1(entry.max)
646
901
  },
647
902
  componentIndex: 0
648
903
  }));
@@ -659,7 +914,7 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
659
914
  objectNames: [],
660
915
  objectCount: 0,
661
916
  bodyCount: 0,
662
- bbox: emptyBBox$1()
917
+ bbox: emptyBBox$2()
663
918
  };
664
919
  componentByRoot.set(root, component);
665
920
  rootToComponentIndex.set(root, component.index);
@@ -671,11 +926,11 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
671
926
  component.objectNames.push(object.name);
672
927
  component.objectCount += 1;
673
928
  component.bodyCount += object.bodyCount;
674
- expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
929
+ expandBBox$2(component.bbox, object.bbox.min, object.bbox.max);
675
930
  }
676
931
  const components = [...componentByRoot.values()];
677
932
  return {
678
- method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
933
+ method: options.exactGeometry ? "mesh-contact-plus-boolean-overlap" : "bbox-neighborhood",
679
934
  options,
680
935
  objectCount: objects.length,
681
936
  componentCount: components.length,
@@ -876,6 +1131,185 @@ function analyzeDistanceInspection(entries, rawOptions = {}) {
876
1131
  warnings: [...connectivity.warnings]
877
1132
  };
878
1133
  }
1134
+ const DEFAULT_CONTACT_TOLERANCE = 0.05;
1135
+ const DEFAULT_BED_TOLERANCE = 0.05;
1136
+ const DEFAULT_GROUND_Z = 0;
1137
+ const MIN_SHAPE_OVERLAP_VOLUME = 1e-9;
1138
+ let UnionFind$1 = class UnionFind2 {
1139
+ constructor(size) {
1140
+ __publicField(this, "parent");
1141
+ __publicField(this, "rank");
1142
+ this.parent = Array.from({ length: size }, (_, index) => index);
1143
+ this.rank = Array.from({ length: size }, () => 0);
1144
+ }
1145
+ find(value) {
1146
+ const parent = this.parent[value];
1147
+ if (parent === value) return value;
1148
+ const root = this.find(parent);
1149
+ this.parent[value] = root;
1150
+ return root;
1151
+ }
1152
+ union(a, b) {
1153
+ const rootA = this.find(a);
1154
+ const rootB = this.find(b);
1155
+ if (rootA === rootB) return;
1156
+ if (this.rank[rootA] < this.rank[rootB]) {
1157
+ this.parent[rootA] = rootB;
1158
+ return;
1159
+ }
1160
+ if (this.rank[rootA] > this.rank[rootB]) {
1161
+ this.parent[rootB] = rootA;
1162
+ return;
1163
+ }
1164
+ this.parent[rootB] = rootA;
1165
+ this.rank[rootA] += 1;
1166
+ }
1167
+ };
1168
+ function cloneVec3(value) {
1169
+ return [value[0], value[1], value[2]];
1170
+ }
1171
+ function emptyBBox$1() {
1172
+ return {
1173
+ min: [Infinity, Infinity, Infinity],
1174
+ max: [-Infinity, -Infinity, -Infinity]
1175
+ };
1176
+ }
1177
+ function expandBBox$1(target, min, max) {
1178
+ for (let axis = 0; axis < 3; axis += 1) {
1179
+ target.min[axis] = Math.min(target.min[axis], min[axis]);
1180
+ target.max[axis] = Math.max(target.max[axis], max[axis]);
1181
+ }
1182
+ }
1183
+ function bodyCountForEntry(entry) {
1184
+ if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
1185
+ return Math.max(0, Math.round(entry.bodyCount));
1186
+ }
1187
+ return 1;
1188
+ }
1189
+ function resolveNonNegative(value, fallback) {
1190
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
1191
+ }
1192
+ function resolveFinite(value, fallback) {
1193
+ return Number.isFinite(value) ? value : fallback;
1194
+ }
1195
+ function analyzeFloatingInspection(entries, rawOptions = {}) {
1196
+ const options = {
1197
+ contactTolerance: resolveNonNegative(rawOptions.contactTolerance, DEFAULT_CONTACT_TOLERANCE),
1198
+ bedTolerance: resolveNonNegative(rawOptions.bedTolerance, DEFAULT_BED_TOLERANCE),
1199
+ groundZ: resolveFinite(rawOptions.groundZ, DEFAULT_GROUND_Z)
1200
+ };
1201
+ const unionFind = new UnionFind$1(entries.length);
1202
+ const contacts = [];
1203
+ const warnings = [];
1204
+ const meshDataByIndex = /* @__PURE__ */ new Map();
1205
+ entries.forEach((entry, index2) => {
1206
+ const meshData = meshContactDataFor(entry);
1207
+ if (meshData) meshDataByIndex.set(index2, meshData);
1208
+ });
1209
+ const candidateTolerance = options.contactTolerance > 0 ? options.contactTolerance : Number.EPSILON;
1210
+ const index = new AabbSpatialIndex(entries);
1211
+ const contactOptions = {
1212
+ contactTolerance: options.contactTolerance,
1213
+ minOverlapVolume: MIN_SHAPE_OVERLAP_VOLUME,
1214
+ exactGeometry: true,
1215
+ mergeOverlappingBBoxes: false,
1216
+ mergeTouchingBBoxes: false
1217
+ };
1218
+ for (const pair of index.overlapPairs({ padding: candidateTolerance }).pairs) {
1219
+ const source = entries[pair.sourceIndex];
1220
+ const target = entries[pair.targetIndex];
1221
+ const detection = detectPhysicalContact(source, target, contactOptions, {
1222
+ sourceMesh: meshDataByIndex.get(pair.sourceIndex) ?? null,
1223
+ targetMesh: meshDataByIndex.get(pair.targetIndex) ?? null
1224
+ });
1225
+ if (detection.warning) warnings.push(detection.warning);
1226
+ if (!detection.contact) continue;
1227
+ unionFind.union(pair.sourceIndex, pair.targetIndex);
1228
+ contacts.push({
1229
+ sourceIndex: pair.sourceIndex,
1230
+ targetIndex: pair.targetIndex,
1231
+ sourceId: source.id,
1232
+ targetId: target.id,
1233
+ sourceName: source.name,
1234
+ targetName: target.name,
1235
+ gap: detection.contact.gap
1236
+ });
1237
+ }
1238
+ const objects = entries.map((entry, entryIndex) => ({
1239
+ index: entryIndex,
1240
+ id: entry.id,
1241
+ name: entry.name,
1242
+ groupName: entry.groupName,
1243
+ treePath: entry.treePath,
1244
+ mock: entry.mock === true,
1245
+ bodyCount: bodyCountForEntry(entry),
1246
+ bbox: {
1247
+ min: cloneVec3(entry.min),
1248
+ max: cloneVec3(entry.max)
1249
+ },
1250
+ componentIndex: 0,
1251
+ isRootComponent: false,
1252
+ isBedSupported: entry.min[2] <= options.groundZ + options.bedTolerance,
1253
+ isFloating: false
1254
+ }));
1255
+ const componentByRoot = /* @__PURE__ */ new Map();
1256
+ const rootToComponentIndex = /* @__PURE__ */ new Map();
1257
+ for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
1258
+ const root = unionFind.find(objectIndex);
1259
+ let component = componentByRoot.get(root);
1260
+ if (!component) {
1261
+ component = {
1262
+ index: componentByRoot.size + 1,
1263
+ objectIndexes: [],
1264
+ objectIds: [],
1265
+ objectNames: [],
1266
+ objectCount: 0,
1267
+ bodyCount: 0,
1268
+ bbox: emptyBBox$1(),
1269
+ isRoot: false,
1270
+ isBedSupported: false,
1271
+ isFloating: false
1272
+ };
1273
+ componentByRoot.set(root, component);
1274
+ rootToComponentIndex.set(root, component.index);
1275
+ }
1276
+ const object = objects[objectIndex];
1277
+ object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
1278
+ component.objectIndexes.push(object.index);
1279
+ component.objectIds.push(object.id);
1280
+ component.objectNames.push(object.name);
1281
+ component.objectCount += 1;
1282
+ component.bodyCount += object.bodyCount;
1283
+ component.isBedSupported || (component.isBedSupported = object.isBedSupported);
1284
+ expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
1285
+ }
1286
+ const components = [...componentByRoot.values()];
1287
+ for (const component of components) {
1288
+ component.isRoot = false;
1289
+ component.isFloating = !component.isBedSupported;
1290
+ }
1291
+ const componentByIndex = new Map(components.map((component) => [component.index, component]));
1292
+ for (const object of objects) {
1293
+ const component = componentByIndex.get(object.componentIndex);
1294
+ object.isRootComponent = false;
1295
+ object.isBedSupported = (component == null ? void 0 : component.isBedSupported) ?? object.isBedSupported;
1296
+ object.isFloating = (component == null ? void 0 : component.isFloating) ?? false;
1297
+ }
1298
+ return {
1299
+ method: "mesh-contact-ground-reachability",
1300
+ options,
1301
+ rootComponentIndex: null,
1302
+ objectCount: objects.length,
1303
+ componentCount: components.length,
1304
+ floatingComponentCount: components.filter((component) => component.isFloating).length,
1305
+ floatingObjectCount: objects.filter((object) => object.isFloating).length,
1306
+ floatingBodyCount: components.filter((component) => component.isFloating).reduce((total, component) => total + component.bodyCount, 0),
1307
+ contacts,
1308
+ objects,
1309
+ components,
1310
+ warnings
1311
+ };
1312
+ }
879
1313
  const CAMERA_TOKEN_DIRECTIONS = {
880
1314
  front: [0, -1, 0.2],
881
1315
  back: [0, 1, 0.2],
@@ -908,6 +1342,7 @@ function parseCameraToken(token) {
908
1342
  `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
909
1343
  );
910
1344
  }
1345
+ const CLI_DEFAULT_BACKEND = "manifold";
911
1346
  function formatAvailableViews(views) {
912
1347
  const names = Object.keys(views ?? {}).sort();
913
1348
  if (names.length === 0) {
@@ -936,14 +1371,36 @@ function resolveNamedSceneViewCamera(sceneConfig, viewName) {
936
1371
  }
937
1372
  return sceneViewCameraToViewportCameraState(view.camera);
938
1373
  }
939
- function edgesStrokeAttrs(preset) {
1374
+ function paletteForTheme(theme) {
1375
+ if (theme === "cad-section") {
1376
+ return {
1377
+ background: "#f7f7f4",
1378
+ fill: "#e6e8eb",
1379
+ stroke: "#20262e",
1380
+ hatchStroke: "#a8afb7",
1381
+ strokeThin: 0.22,
1382
+ strokeBold: 0.55,
1383
+ useHatch: true
1384
+ };
1385
+ }
1386
+ return {
1387
+ background: "#2a2a2a",
1388
+ fill: "#4488cc",
1389
+ stroke: "#224466",
1390
+ hatchStroke: "#224466",
1391
+ strokeThin: 0.3,
1392
+ strokeBold: 0.8,
1393
+ useHatch: false
1394
+ };
1395
+ }
1396
+ function edgesStrokeAttrs(preset, palette) {
940
1397
  switch (preset) {
941
1398
  case "off":
942
1399
  return 'stroke="none"';
943
1400
  case "bold":
944
- return 'stroke="#224466" stroke-width="0.8"';
1401
+ return `stroke="${palette.stroke}" stroke-width="${palette.strokeBold}"`;
945
1402
  default:
946
- return 'stroke="#224466" stroke-width="0.3"';
1403
+ return `stroke="${palette.stroke}" stroke-width="${palette.strokeThin}"`;
947
1404
  }
948
1405
  }
949
1406
  function combineBounds(left, right) {
@@ -958,6 +1415,19 @@ function escapeAttribute(value) {
958
1415
  function polygonPath(poly) {
959
1416
  return `${poly.map((point, index) => `${index === 0 ? "M" : "L"}${point[0].toFixed(3)},${(-point[1]).toFixed(3)}`).join(" ")} Z`;
960
1417
  }
1418
+ function compoundPolygonPath(polygons) {
1419
+ return polygons.map(polygonPath).join(" ");
1420
+ }
1421
+ function hatchDefs(palette) {
1422
+ if (!palette.useHatch) return "";
1423
+ return ` <defs>
1424
+ <pattern id="forge-section-hatch" patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)">
1425
+ <rect width="4" height="4" fill="${palette.fill}"/>
1426
+ <line x1="0" y1="0" x2="0" y2="4" stroke="${palette.hatchStroke}" stroke-width="0.28"/>
1427
+ </pattern>
1428
+ </defs>
1429
+ `;
1430
+ }
961
1431
  function prepareEntry(entry) {
962
1432
  const polygons = entry.sketch.toPolygons();
963
1433
  if (polygons.length === 0) {
@@ -979,7 +1449,9 @@ function buildSketchSvgDocument(entries, options = {}) {
979
1449
  throw new Error("Sketch SVG export requires at least one sketch payload.");
980
1450
  }
981
1451
  const edges = options.edges ?? "thin";
982
- const strokeAttrs = edgesStrokeAttrs(edges);
1452
+ const palette = paletteForTheme(options.theme ?? "debug");
1453
+ const strokeAttrs = edgesStrokeAttrs(edges, palette);
1454
+ const fill = palette.useHatch ? "url(#forge-section-hatch)" : palette.fill;
983
1455
  const prepared = entries.map(prepareEntry);
984
1456
  const combinedBounds = prepared.reduce((acc, entry) => combineBounds(acc, entry.bounds), prepared[0].bounds);
985
1457
  const margin = 2;
@@ -992,17 +1464,15 @@ function buildSketchSvgDocument(entries, options = {}) {
992
1464
  let pathCount = 0;
993
1465
  const body = prepared.map((entry) => {
994
1466
  const label = entry.name ? ` data-name="${escapeAttribute(entry.name)}"` : "";
995
- const paths = entry.polygons.map((poly) => {
996
- pathCount += 1;
997
- return ` <path d="${polygonPath(poly)}" fill="#4488cc" ${strokeAttrs}/>`;
998
- }).join("\n");
1467
+ pathCount += entry.polygons.length;
999
1468
  return ` <g${label}>
1000
- ${paths}
1469
+ <path d="${compoundPolygonPath(entry.polygons)}" fill="${fill}" fill-rule="evenodd" ${strokeAttrs}/>
1001
1470
  </g>`;
1002
1471
  }).join("\n");
1003
1472
  return {
1004
1473
  svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX.toFixed(1)} ${(-maxY).toFixed(1)} ${width.toFixed(1)} ${height.toFixed(1)}" width="${Math.max(width * 4, 400)}" height="${Math.max(height * 4, 400)}">
1005
- <rect x="${minX.toFixed(1)}" y="${(-maxY).toFixed(1)}" width="${width.toFixed(1)}" height="${height.toFixed(1)}" fill="#2a2a2a"/>
1474
+ <rect x="${minX.toFixed(1)}" y="${(-maxY).toFixed(1)}" width="${width.toFixed(1)}" height="${height.toFixed(1)}" fill="${palette.background}"/>
1475
+ ${hatchDefs(palette)}
1006
1476
  ${body}
1007
1477
  </svg>`,
1008
1478
  width,
@@ -1015,7 +1485,8 @@ const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
1015
1485
  minThickness: 1.2,
1016
1486
  warnThickness: 2,
1017
1487
  maxThickness: 6,
1018
- maxSamplesPerObject: 5e3
1488
+ maxSamplesPerObject: 5e3,
1489
+ contactTolerance: DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance
1019
1490
  };
1020
1491
  const THICKNESS_COLORS = {
1021
1492
  critical: [255, 28, 28],
@@ -1031,6 +1502,13 @@ function finitePositive(value, fallback, label) {
1031
1502
  }
1032
1503
  return value;
1033
1504
  }
1505
+ function finiteNonNegative(value, fallback, label) {
1506
+ if (value === void 0) return fallback;
1507
+ if (!Number.isFinite(value) || value < 0) {
1508
+ throw new Error(`${label} must be a non-negative finite number.`);
1509
+ }
1510
+ return value;
1511
+ }
1034
1512
  function resolveThicknessInspectionOptions(raw = {}) {
1035
1513
  const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
1036
1514
  const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
@@ -1040,6 +1518,11 @@ function resolveThicknessInspectionOptions(raw = {}) {
1040
1518
  DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
1041
1519
  "maxSamplesPerObject"
1042
1520
  );
1521
+ const contactTolerance = finiteNonNegative(
1522
+ raw.contactTolerance,
1523
+ DEFAULT_THICKNESS_INSPECTION_OPTIONS.contactTolerance,
1524
+ "contactTolerance"
1525
+ );
1043
1526
  if (minThickness > warnThickness) {
1044
1527
  throw new Error("minThickness must be less than or equal to warnThickness.");
1045
1528
  }
@@ -1050,7 +1533,8 @@ function resolveThicknessInspectionOptions(raw = {}) {
1050
1533
  minThickness,
1051
1534
  warnThickness,
1052
1535
  maxThickness,
1053
- maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
1536
+ maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject)),
1537
+ contactTolerance
1054
1538
  };
1055
1539
  }
1056
1540
  function lerp(a, b, t) {
@@ -1249,22 +1733,45 @@ function geometryMaxDimension(geometry) {
1249
1733
  box.getSize(size);
1250
1734
  return Math.max(1, size.x, size.y, size.z);
1251
1735
  }
1252
- function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
1736
+ function firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, jumpableMeshes, point, direction, epsilon, far, contactTolerance) {
1253
1737
  const origin = point.clone().addScaledVector(direction, epsilon);
1254
1738
  raycaster.set(origin, direction);
1255
1739
  raycaster.near = epsilon;
1256
1740
  raycaster.far = far;
1257
- const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
1258
- return hit ? hit.distance + epsilon : null;
1741
+ const hits = raycaster.intersectObjects(rayTargetMeshes, false);
1742
+ for (const hit of hits) {
1743
+ if (hit.distance <= epsilon) continue;
1744
+ if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
1745
+ return hit.distance + epsilon;
1746
+ }
1747
+ return null;
1259
1748
  }
1260
- function triangleThickness(raycaster, mesh, centroid, normal, epsilon, far) {
1261
- const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon, far);
1262
- const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon, far);
1749
+ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid, normal, epsilon, far, contactTolerance) {
1750
+ const forward = firstOppositeSurfaceDistance(
1751
+ raycaster,
1752
+ rayTargetMeshes,
1753
+ jumpableMeshes,
1754
+ centroid,
1755
+ normal,
1756
+ epsilon,
1757
+ far,
1758
+ contactTolerance
1759
+ );
1760
+ const backward = firstOppositeSurfaceDistance(
1761
+ raycaster,
1762
+ rayTargetMeshes,
1763
+ jumpableMeshes,
1764
+ centroid,
1765
+ normal.clone().negate(),
1766
+ epsilon,
1767
+ far,
1768
+ contactTolerance
1769
+ );
1263
1770
  if (forward == null) return backward;
1264
1771
  if (backward == null) return forward;
1265
1772
  return Math.min(forward, backward);
1266
1773
  }
1267
- function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1774
+ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
1268
1775
  const options = resolveThicknessInspectionOptions(rawOptions);
1269
1776
  const geometry = cloneGeometryForFaceColors(sourceGeometry);
1270
1777
  const position = geometry.getAttribute("position");
@@ -1282,7 +1789,8 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1282
1789
  const triangleCount = Math.floor(position.count / 3);
1283
1790
  const surfaceTriangles = readSurfaceTriangles(position);
1284
1791
  const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
1285
- const maxDim = geometryMaxDimension(geometry);
1792
+ const connectedGeometries = context.connectedGeometries ?? [];
1793
+ const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
1286
1794
  const epsilon = Math.max(1e-4, maxDim * 1e-6);
1287
1795
  const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1288
1796
  const colors = new Float32Array(position.count * 3);
@@ -1291,7 +1799,15 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1291
1799
  const pointSamples = [];
1292
1800
  const warnings = [];
1293
1801
  const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1294
- const rayMesh = new Mesh(geometry, rayMaterial);
1802
+ const rayTargets = [
1803
+ { mesh: new Mesh(geometry, rayMaterial), jumpable: false },
1804
+ ...connectedGeometries.map((connectedGeometry) => ({
1805
+ mesh: new Mesh(connectedGeometry, rayMaterial),
1806
+ jumpable: true
1807
+ }))
1808
+ ];
1809
+ const rayTargetMeshes = rayTargets.map((target) => target.mesh);
1810
+ const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
1295
1811
  const raycaster = new Raycaster();
1296
1812
  if (surfaceTriangles.length === 0) {
1297
1813
  warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
@@ -1303,7 +1819,16 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1303
1819
  const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1304
1820
  for (const sample of surfaceSamples) {
1305
1821
  sampledTriangleIndexes.add(sample.triangle.index);
1306
- const thickness = triangleThickness(raycaster, rayMesh, sample.position, sample.normal, epsilon, far);
1822
+ const thickness = triangleThickness(
1823
+ raycaster,
1824
+ rayTargetMeshes,
1825
+ jumpableMeshes,
1826
+ sample.position,
1827
+ sample.normal,
1828
+ epsilon,
1829
+ far,
1830
+ options.contactTolerance
1831
+ );
1307
1832
  samples.push({ thickness, area: sample.area });
1308
1833
  const previous = triangleThicknessValues[sample.triangle.index];
1309
1834
  if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
@@ -1637,7 +2162,7 @@ function pointSegmentDistance(point, start, end) {
1637
2162
  return point.distanceTo(segment.multiplyScalar(t).add(start));
1638
2163
  }
1639
2164
  const DEFAULT_VERTEX_TOLERANCE = 1e-5;
1640
- class UnionFind2 {
2165
+ class UnionFind3 {
1641
2166
  constructor(size) {
1642
2167
  __publicField(this, "parent");
1643
2168
  __publicField(this, "rank");
@@ -1696,7 +2221,7 @@ function analyzeMeshConnectedComponents(positions, vertexTolerance = DEFAULT_VER
1696
2221
  const vertexComponentIndexes = new Int32Array(vertexCount);
1697
2222
  if (triangleCount === 0) return { components: [], vertexComponentIndexes };
1698
2223
  const tolerance = Number.isFinite(vertexTolerance) && vertexTolerance > 0 ? vertexTolerance : DEFAULT_VERTEX_TOLERANCE;
1699
- const unionFind = new UnionFind2(triangleCount);
2224
+ const unionFind = new UnionFind3(triangleCount);
1700
2225
  const vertexOwnerByKey = /* @__PURE__ */ new Map();
1701
2226
  for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
1702
2227
  for (let corner = 0; corner < 3; corner += 1) {
@@ -1747,12 +2272,25 @@ function objectConnectivityEntry(object) {
1747
2272
  shape: object.shape,
1748
2273
  min: object.min,
1749
2274
  max: object.max,
2275
+ positions: object.positions,
1750
2276
  groupName: object.groupName,
1751
2277
  treePath: object.treePath,
1752
2278
  mock: object.mock,
1753
2279
  bodyCount: 1
1754
2280
  };
1755
2281
  }
2282
+ function componentPositions(sourcePositions, component) {
2283
+ const out = new Float32Array(component.triangleIndexes.length * 9);
2284
+ let outOffset = 0;
2285
+ for (const triangleIndex of component.triangleIndexes) {
2286
+ const sourceOffset = triangleIndex * 9;
2287
+ for (let offset = 0; offset < 9; offset += 1) {
2288
+ out[outOffset + offset] = sourcePositions[sourceOffset + offset];
2289
+ }
2290
+ outOffset += 9;
2291
+ }
2292
+ return out;
2293
+ }
1756
2294
  function buildMeshBodyConnectivityInput(objects, options) {
1757
2295
  const entries = [];
1758
2296
  const bodyIdsByObjectId = /* @__PURE__ */ new Map();
@@ -1776,6 +2314,7 @@ function buildMeshBodyConnectivityInput(objects, options) {
1776
2314
  shape: options.bodyShape(object, component),
1777
2315
  min: component.bbox.min,
1778
2316
  max: component.bbox.max,
2317
+ positions: componentPositions(object.positions, component),
1779
2318
  groupName: object.groupName,
1780
2319
  treePath: object.treePath,
1781
2320
  mock: object.mock,
@@ -1854,6 +2393,9 @@ class EmptyInspectionShape {
1854
2393
  const COLLISION_SOURCE_OPACITY = 0.22;
1855
2394
  const COLLISION_SOURCE_COLOR = [180, 200, 220];
1856
2395
  const COLLISION_HIGHLIGHT_COLOR = [255, 68, 16];
2396
+ const FLOATING_HIGHLIGHT_COLOR = [255, 68, 16];
2397
+ const FLOATING_CONTEXT_COLOR = [38, 49, 58];
2398
+ const FLOATING_HIDDEN_COLOR = [0, 0, 0];
1857
2399
  const COLLISION_PALETTE = [
1858
2400
  COLLISION_HIGHLIGHT_COLOR,
1859
2401
  [0, 204, 255],
@@ -1894,18 +2436,13 @@ const MASK_PALETTE = [
1894
2436
  [0, 0, 128],
1895
2437
  [128, 128, 128]
1896
2438
  ];
1897
- function cloneShapePositions(shape) {
1898
- const geometry = shapeToGeometry(shape);
1899
- try {
1900
- const position = geometry.solid.getAttribute("position");
1901
- if (!position) return void 0;
1902
- return new Float32Array(position.array);
1903
- } finally {
1904
- geometry.solid.dispose();
1905
- geometry.edges.dispose();
1906
- }
2439
+ function cloneGeometryPositions(geometry) {
2440
+ if (!geometry) return void 0;
2441
+ const position = geometry.getAttribute("position");
2442
+ if (!position) return void 0;
2443
+ return new Float32Array(position.array);
1907
2444
  }
1908
- function meshConnectivitySource(entry) {
2445
+ function meshConnectivitySource(entry, positions) {
1909
2446
  const bb = entry.shape.boundingBox();
1910
2447
  return {
1911
2448
  id: entry.source.id,
@@ -1916,7 +2453,7 @@ function meshConnectivitySource(entry) {
1916
2453
  groupName: entry.source.groupName,
1917
2454
  treePath: entry.source.treePath,
1918
2455
  mock: entry.source.mock,
1919
- positions: cloneShapePositions(entry.shape)
2456
+ positions
1920
2457
  };
1921
2458
  }
1922
2459
  function summarizeSceneGeometry(entries) {
@@ -2268,6 +2805,37 @@ function renderCurrentNormals(session) {
2268
2805
  normalMaterial.dispose();
2269
2806
  }
2270
2807
  }
2808
+ function renderCurrentZebra(session) {
2809
+ const r = getRenderer(session.size, session.pixelRatio);
2810
+ const zebraMaterial = new ShaderMaterial({
2811
+ uniforms: {
2812
+ uAccentColor: { value: new Color(ZEBRA_ACCENT_COLOR) },
2813
+ uDarkColor: { value: new Color(ZEBRA_DARK_COLOR) },
2814
+ uLightColor: { value: new Color(ZEBRA_LIGHT_COLOR) },
2815
+ uStripeScale: { value: ZEBRA_STRIPE_SCALE },
2816
+ uStripeSoftness: { value: ZEBRA_STRIPE_SOFTNESS }
2817
+ },
2818
+ vertexShader: ZEBRA_STRIPE_VERTEX_SHADER,
2819
+ fragmentShader: ZEBRA_STRIPE_FRAGMENT_SHADER,
2820
+ side: DoubleSide
2821
+ });
2822
+ zebraMaterial.toneMapped = false;
2823
+ const prevOverride = session.scene.overrideMaterial;
2824
+ session.scene.overrideMaterial = zebraMaterial;
2825
+ try {
2826
+ return withSolidOnlyVisibility(
2827
+ session,
2828
+ () => withTemporarySceneBackground(session, new Color(0), () => {
2829
+ updateSdfRaymarchUniforms(session);
2830
+ r.render(session.scene, session.camera);
2831
+ return captureRenderedPng(session.size);
2832
+ })
2833
+ );
2834
+ } finally {
2835
+ session.scene.overrideMaterial = prevOverride;
2836
+ zebraMaterial.dispose();
2837
+ }
2838
+ }
2271
2839
  function maskColorForIndex(index) {
2272
2840
  return MASK_PALETTE[(index - 1) % MASK_PALETTE.length];
2273
2841
  }
@@ -2371,6 +2939,47 @@ function analyzeSessionConnectivity(session) {
2371
2939
  }
2372
2940
  return session.physicalConnectivity;
2373
2941
  }
2942
+ function addContactNeighbor(target, sourceId, targetId) {
2943
+ let neighbors = target.get(sourceId);
2944
+ if (!neighbors) {
2945
+ neighbors = /* @__PURE__ */ new Set();
2946
+ target.set(sourceId, neighbors);
2947
+ }
2948
+ neighbors.add(targetId);
2949
+ }
2950
+ function baseObjectIdForConnectivityEntry(entryId, renderableIds) {
2951
+ if (renderableIds.has(entryId)) return entryId;
2952
+ const bodySuffixIndex = entryId.lastIndexOf(":body:");
2953
+ if (bodySuffixIndex <= 0) return null;
2954
+ const baseId = entryId.slice(0, bodySuffixIndex);
2955
+ return renderableIds.has(baseId) ? baseId : null;
2956
+ }
2957
+ function buildThicknessRaycastConnectivityContext(session) {
2958
+ const report = analyzeSessionConnectivity(session);
2959
+ const renderableById = new Map(
2960
+ session.renderables.filter((renderable) => !renderable.sdfRaymarch).map((renderable) => [renderable.id, renderable])
2961
+ );
2962
+ const renderableIds = new Set(renderableById.keys());
2963
+ const neighborIdsByObjectId = /* @__PURE__ */ new Map();
2964
+ for (const edge of report.edges) {
2965
+ const sourceId = baseObjectIdForConnectivityEntry(edge.sourceId, renderableIds);
2966
+ const targetId = baseObjectIdForConnectivityEntry(edge.targetId, renderableIds);
2967
+ if (!sourceId || !targetId || sourceId === targetId) continue;
2968
+ addContactNeighbor(neighborIdsByObjectId, sourceId, targetId);
2969
+ addContactNeighbor(neighborIdsByObjectId, targetId, sourceId);
2970
+ }
2971
+ return { neighborIdsByObjectId, renderableById };
2972
+ }
2973
+ function connectedThicknessGeometriesFor(context, source) {
2974
+ const neighborIds = context.neighborIdsByObjectId.get(source.id);
2975
+ if (!neighborIds) return [];
2976
+ const geometries = [];
2977
+ for (const neighborId of neighborIds) {
2978
+ const renderable = context.renderableById.get(neighborId);
2979
+ if (renderable) geometries.push(renderable.solid.geometry);
2980
+ }
2981
+ return geometries;
2982
+ }
2374
2983
  function decorateConnectivityReport(report) {
2375
2984
  const components = report.components.map((component) => {
2376
2985
  const color = maskColorForIndex(component.index);
@@ -2539,6 +3148,151 @@ function renderCurrentDistance(session) {
2539
3148
  });
2540
3149
  }
2541
3150
  }
3151
+ function analyzeSessionFloating(session) {
3152
+ if (!session.floatingReport) {
3153
+ session.floatingReport = analyzeFloatingInspection(session.connectivityEntries, { groundZ: session.floatingGroundZ });
3154
+ }
3155
+ return session.floatingReport;
3156
+ }
3157
+ function decorateFloatingReport(report) {
3158
+ const components = report.components.map((component) => {
3159
+ const color = component.isFloating ? FLOATING_HIGHLIGHT_COLOR : FLOATING_HIDDEN_COLOR;
3160
+ return {
3161
+ ...component,
3162
+ color,
3163
+ hex: colorHex(color)
3164
+ };
3165
+ });
3166
+ const objectByComponentIndex = new Map(components.map((component) => [component.index, component]));
3167
+ const objects = report.objects.map((object) => {
3168
+ var _a;
3169
+ const color = ((_a = objectByComponentIndex.get(object.componentIndex)) == null ? void 0 : _a.color) ?? FLOATING_HIDDEN_COLOR;
3170
+ return {
3171
+ ...object,
3172
+ color,
3173
+ hex: colorHex(color)
3174
+ };
3175
+ });
3176
+ return {
3177
+ method: report.method,
3178
+ options: report.options,
3179
+ rootComponentIndex: report.rootComponentIndex,
3180
+ objectCount: report.objectCount,
3181
+ componentCount: report.componentCount,
3182
+ floatingComponentCount: report.floatingComponentCount,
3183
+ floatingObjectCount: report.floatingObjectCount,
3184
+ floatingBodyCount: report.floatingBodyCount,
3185
+ objects,
3186
+ components,
3187
+ contacts: report.contacts,
3188
+ warnings: report.warnings,
3189
+ style: {
3190
+ highlightColor: FLOATING_HIGHLIGHT_COLOR,
3191
+ highlightHex: colorHex(FLOATING_HIGHLIGHT_COLOR),
3192
+ contextColor: FLOATING_CONTEXT_COLOR,
3193
+ contextHex: colorHex(FLOATING_CONTEXT_COLOR),
3194
+ hiddenColor: FLOATING_HIDDEN_COLOR,
3195
+ hiddenHex: colorHex(FLOATING_HIDDEN_COLOR)
3196
+ }
3197
+ };
3198
+ }
3199
+ function renderCurrentFloating(session) {
3200
+ const r = getRenderer(session.size, session.pixelRatio);
3201
+ const report = decorateFloatingReport(analyzeSessionFloating(session));
3202
+ const byId = new Map(report.objects.map((object) => [object.id, object]));
3203
+ const vertexColorsById = session.connectivityBodyInput ? meshVertexColorBuffersFor(session.connectivityBodyInput, (entryId) => {
3204
+ var _a;
3205
+ return rgbFloats(((_a = byId.get(entryId)) == null ? void 0 : _a.color) ?? FLOATING_HIDDEN_COLOR);
3206
+ }) : /* @__PURE__ */ new Map();
3207
+ const replacements = session.renderables.map((renderable) => {
3208
+ var _a;
3209
+ const object = byId.get(renderable.id);
3210
+ const colors = vertexColorsById.get(renderable.id);
3211
+ const vertexGeometry = colors ? geometryWithVisibleVertexColors(renderable.solid.geometry, colors) : null;
3212
+ const previousMaterial = renderable.solid.material;
3213
+ const previousGeometry = renderable.solid.geometry;
3214
+ const previousVisible = renderable.solid.visible;
3215
+ const previousWireVisible = renderable.wire.visible;
3216
+ const previousShellVisible = (_a = renderable.shell) == null ? void 0 : _a.visible;
3217
+ const contextMaterial = new MeshBasicMaterial({
3218
+ color: colorHex(FLOATING_CONTEXT_COLOR),
3219
+ transparent: true,
3220
+ opacity: 0.18,
3221
+ side: DoubleSide,
3222
+ depthWrite: false,
3223
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null
3224
+ });
3225
+ contextMaterial.toneMapped = false;
3226
+ const highlightMaterial = new MeshBasicMaterial({
3227
+ color: vertexGeometry ? 16777215 : colorHex(FLOATING_HIGHLIGHT_COLOR),
3228
+ vertexColors: Boolean(vertexGeometry),
3229
+ side: DoubleSide,
3230
+ depthWrite: false,
3231
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null,
3232
+ polygonOffset: true,
3233
+ polygonOffsetFactor: -1,
3234
+ polygonOffsetUnits: -1
3235
+ });
3236
+ highlightMaterial.toneMapped = false;
3237
+ renderable.solid.material = contextMaterial;
3238
+ renderable.solid.geometry = previousGeometry;
3239
+ renderable.solid.visible = true;
3240
+ const highlightGeometry = vertexGeometry ?? ((object == null ? void 0 : object.isFloating) === true ? previousGeometry : null);
3241
+ const highlightMesh = highlightGeometry ? new Mesh(highlightGeometry, highlightMaterial) : null;
3242
+ if (highlightMesh) {
3243
+ highlightMesh.renderOrder = 5;
3244
+ highlightMesh.raycast = () => null;
3245
+ renderable.root.add(highlightMesh);
3246
+ }
3247
+ renderable.wire.visible = false;
3248
+ if (renderable.shell) renderable.shell.visible = false;
3249
+ return {
3250
+ renderable,
3251
+ previousMaterial,
3252
+ previousGeometry,
3253
+ previousVisible,
3254
+ previousWireVisible,
3255
+ previousShellVisible,
3256
+ vertexGeometry,
3257
+ contextMaterial,
3258
+ highlightMaterial,
3259
+ highlightMesh
3260
+ };
3261
+ });
3262
+ try {
3263
+ const png = withTemporarySceneBackground(session, new Color(0), () => {
3264
+ updateSdfRaymarchUniforms(session);
3265
+ r.render(session.scene, session.camera);
3266
+ return captureRenderedPng(session.size);
3267
+ });
3268
+ return { png, report };
3269
+ } finally {
3270
+ replacements.forEach(
3271
+ ({
3272
+ renderable,
3273
+ previousMaterial,
3274
+ previousGeometry,
3275
+ previousVisible,
3276
+ previousWireVisible,
3277
+ previousShellVisible,
3278
+ vertexGeometry,
3279
+ contextMaterial,
3280
+ highlightMaterial,
3281
+ highlightMesh
3282
+ }) => {
3283
+ if (highlightMesh) renderable.root.remove(highlightMesh);
3284
+ renderable.solid.material = previousMaterial;
3285
+ renderable.solid.geometry = previousGeometry;
3286
+ renderable.solid.visible = previousVisible;
3287
+ renderable.wire.visible = previousWireVisible;
3288
+ if (renderable.shell && previousShellVisible !== void 0) renderable.shell.visible = previousShellVisible;
3289
+ vertexGeometry == null ? void 0 : vertexGeometry.dispose();
3290
+ contextMaterial.dispose();
3291
+ highlightMaterial.dispose();
3292
+ }
3293
+ );
3294
+ }
3295
+ }
2542
3296
  function analyzeSessionCollisions(session) {
2543
3297
  if (!session.collisionReport) {
2544
3298
  session.collisionReport = analyzeCollisionIntersections(session.collisionEntries);
@@ -2549,7 +3303,15 @@ function decorateCollisionReport(report) {
2549
3303
  return {
2550
3304
  method: report.method,
2551
3305
  options: report.options,
3306
+ broadphase: report.broadphase,
2552
3307
  objectCount: report.objectCount,
3308
+ candidatePairCount: report.candidatePairCount,
3309
+ bboxVolumePrunedPairCount: report.bboxVolumePrunedPairCount,
3310
+ testedPairCount: report.testedPairCount,
3311
+ skippedPairCount: report.skippedPairCount,
3312
+ pairLimitSkippedPairCount: report.pairLimitSkippedPairCount,
3313
+ timeBudgetSkippedPairCount: report.timeBudgetSkippedPairCount,
3314
+ exactCheckMs: report.exactCheckMs,
2553
3315
  collisionCount: report.collisionCount,
2554
3316
  objects: report.objects,
2555
3317
  collisions: report.collisions.map((finding) => {
@@ -2637,6 +3399,26 @@ function renderCurrentCollisions(session) {
2637
3399
  function inspectionOptionsKey(value) {
2638
3400
  return JSON.stringify(value ?? {});
2639
3401
  }
3402
+ function scalarInspectableObjectCount(session) {
3403
+ return session.renderables.filter((renderable) => !renderable.sdfRaymarch).length;
3404
+ }
3405
+ function withSceneSampleBudget(session, options, explicitMaxSamplesPerObject) {
3406
+ const sampleBudget = resolveScalarSceneSampleBudget({
3407
+ objectCount: scalarInspectableObjectCount(session),
3408
+ maxSamplesPerObject: options.maxSamplesPerObject,
3409
+ explicitMaxSamplesPerObject
3410
+ });
3411
+ return {
3412
+ options: { ...options, maxSamplesPerObject: sampleBudget.effectiveMaxSamplesPerObject },
3413
+ sampleBudget
3414
+ };
3415
+ }
3416
+ function maybePushSceneSampleBudgetWarning(warnings, channel, sampleBudget) {
3417
+ if (!sampleBudget.capped) return;
3418
+ warnings.push(
3419
+ `${channel} inspection scene budget lowered maxSamplesPerObject from ${sampleBudget.requestedMaxSamplesPerObject} to ${sampleBudget.effectiveMaxSamplesPerObject} across ${sampleBudget.objectCount} mesh object(s); pass the sample-count flag to override.`
3420
+ );
3421
+ }
2640
3422
  function bboxFromGeometry(geometry) {
2641
3423
  geometry.computeBoundingBox();
2642
3424
  const bbox = geometry.boundingBox;
@@ -2717,22 +3499,27 @@ function renderScalarPointOverlays(session, overlays) {
2717
3499
  }
2718
3500
  function getSessionThicknessInspection(session, rawOptions) {
2719
3501
  var _a;
2720
- const options = resolveThicknessInspectionOptions(rawOptions);
2721
- const optionsKey = inspectionOptionsKey(options);
3502
+ const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
3503
+ const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
3504
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget });
2722
3505
  if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
2723
3506
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2724
3507
  const warnings = [];
3508
+ maybePushSceneSampleBudgetWarning(warnings, "Thickness", sampleBudget);
2725
3509
  const objects = [];
2726
3510
  const cloudObjects = [];
2727
3511
  const points = [];
2728
3512
  const overlays = [];
3513
+ const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
2729
3514
  session.renderables.forEach((renderable, index) => {
2730
3515
  const sourceObject = byId.get(renderable.id);
2731
3516
  if (renderable.sdfRaymarch) {
2732
3517
  warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh thickness inspection.`);
2733
3518
  return;
2734
3519
  }
2735
- const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options);
3520
+ const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, {
3521
+ connectedGeometries: connectedThicknessGeometriesFor(raycastConnectivity, renderable)
3522
+ });
2736
3523
  const bbox = bboxFromGeometry(analysis.geometry);
2737
3524
  const summary = summarizeThicknessSamples(analysis.samples, options);
2738
3525
  if (analysis.warnings.length > 0) {
@@ -2786,8 +3573,9 @@ function getSessionThicknessInspection(session, rawOptions) {
2786
3573
  points
2787
3574
  },
2788
3575
  report: {
2789
- method: "mesh-normal-raycast",
3576
+ method: "mesh-normal-raycast-contact-aware",
2790
3577
  options,
3578
+ sampleBudget,
2791
3579
  objectCount: objects.length,
2792
3580
  objects,
2793
3581
  warnings,
@@ -2815,11 +3603,13 @@ function renderCurrentRoughness(session, rawOptions) {
2815
3603
  }
2816
3604
  function getSessionRoughnessInspection(session, rawOptions) {
2817
3605
  var _a;
2818
- const options = resolveRoughnessInspectionOptions(rawOptions);
2819
- const optionsKey = inspectionOptionsKey(options);
3606
+ const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
3607
+ const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
3608
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget });
2820
3609
  if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
2821
3610
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2822
3611
  const warnings = [];
3612
+ maybePushSceneSampleBudgetWarning(warnings, "Roughness", sampleBudget);
2823
3613
  const objects = [];
2824
3614
  const cloudObjects = [];
2825
3615
  const points = [];
@@ -2882,6 +3672,7 @@ function getSessionRoughnessInspection(session, rawOptions) {
2882
3672
  report: {
2883
3673
  method: "mesh-dihedral-angle",
2884
3674
  options,
3675
+ sampleBudget,
2885
3676
  objectCount: objects.length,
2886
3677
  objects,
2887
3678
  warnings,
@@ -2900,14 +3691,14 @@ function getSessionRoughnessInspection(session, rawOptions) {
2900
3691
  }
2901
3692
  function emptySectionSvg() {
2902
3693
  return {
2903
- svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 2 2" width="400" height="400"><rect x="-1" y="-1" width="2" height="2" fill="#2a2a2a"/></svg>',
3694
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 2 2" width="400" height="400"><rect x="-1" y="-1" width="2" height="2" fill="#f7f7f4"/></svg>',
2904
3695
  width: 0,
2905
3696
  height: 0,
2906
3697
  area: 0,
2907
3698
  pathCount: 0
2908
3699
  };
2909
3700
  }
2910
- async function svgToPngDataUrl(svg, size, background = "#2a2a2a") {
3701
+ async function svgToPngDataUrl(svg, size, background = "#f7f7f4") {
2911
3702
  if (!exportCtx) {
2912
3703
  throw new Error("Could not create export canvas context.");
2913
3704
  }
@@ -2965,7 +3756,7 @@ async function renderSectionAtlas(session, opts) {
2965
3756
  sectionSketches.push({ name: entry.name, sketch });
2966
3757
  }
2967
3758
  }
2968
- const svgDocument = sectionSketches.length > 0 ? buildSketchSvgDocument(sectionSketches, { edges: "thin" }) : emptySectionSvg();
3759
+ const svgDocument = sectionSketches.length > 0 ? buildSketchSvgDocument(sectionSketches, { edges: "thin", theme: "cad-section" }) : emptySectionSvg();
2969
3760
  slices.push({
2970
3761
  index,
2971
3762
  offset,
@@ -3015,6 +3806,37 @@ function addRenderStyleLights(scene, style) {
3015
3806
  new HemisphereLight(new Color(lights.hemisphereSky), new Color(lights.hemisphereGround), lights.hemisphereIntensity)
3016
3807
  );
3017
3808
  }
3809
+ function fieldKindUniform(kind) {
3810
+ return kind === "hybrid" ? 1 : 0;
3811
+ }
3812
+ function createSurfaceFieldMaterial({
3813
+ field,
3814
+ objectColor,
3815
+ clippingPlanes,
3816
+ fieldScale
3817
+ }) {
3818
+ return new ShaderMaterial({
3819
+ vertexShader: SURFACE_FIELD_VERTEX_SHADER,
3820
+ fragmentShader: SURFACE_FIELD_FRAGMENT_SHADER,
3821
+ uniforms: {
3822
+ uAccentColor: { value: new Color(field.accentColor) },
3823
+ uBaseColor: { value: new Color(field.baseColor) },
3824
+ uDarkColor: { value: new Color(field.darkColor) },
3825
+ uFieldKind: { value: fieldKindUniform(field.kind) },
3826
+ uFieldScale: { value: fieldScale },
3827
+ uGlow: { value: field.glow },
3828
+ uLineColor: { value: new Color(field.lineColor) },
3829
+ uLineWidth: { value: field.lineWidth },
3830
+ uNodeColor: { value: new Color(field.nodeColor) },
3831
+ uObjectColor: { value: objectColor },
3832
+ uObjectColorMix: { value: field.objectColorMix },
3833
+ uSpacing: { value: field.spacing }
3834
+ },
3835
+ side: DoubleSide,
3836
+ toneMapped: false,
3837
+ clippingPlanes
3838
+ });
3839
+ }
3018
3840
  function createSceneLight(def) {
3019
3841
  const color = def.color ? new Color(def.color) : new Color(16777215);
3020
3842
  const intensity = def.intensity ?? 1;
@@ -3703,7 +4525,7 @@ function isFocusVisible(obj, focus, hide) {
3703
4525
  return true;
3704
4526
  }
3705
4527
  function createSession(code, opts) {
3706
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
4528
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
3707
4529
  const size = (opts == null ? void 0 : opts.size) ?? 1024;
3708
4530
  const pixelRatio = (opts == null ? void 0 : opts.pixelRatio) ?? 1;
3709
4531
  const debug = createCaptureDebugLogger(opts == null ? void 0 : opts.debug);
@@ -3785,8 +4607,6 @@ function createSession(code, opts) {
3785
4607
  name: entry.source.name,
3786
4608
  shape: entry.shape
3787
4609
  }));
3788
- const connectivityBodyInput = (opts == null ? void 0 : opts.includeConnectivity) ? buildMeshBodyConnectivityInput(shapeVisibleObjs.map(meshConnectivitySource), { bodyShape: () => new EmptyInspectionShape() }) : null;
3789
- const connectivityEntries = (connectivityBodyInput == null ? void 0 : connectivityBodyInput.entries) ?? [];
3790
4610
  const collisionEntries = (opts == null ? void 0 : opts.includeCollisions) ? shapeVisibleObjs.map((entry) => {
3791
4611
  const bb2 = entry.shape.boundingBox();
3792
4612
  return {
@@ -3843,6 +4663,7 @@ function createSession(code, opts) {
3843
4663
  const bsize = new Vector3();
3844
4664
  bb.getSize(bsize);
3845
4665
  const maxDim = Math.max(1, bsize.x, bsize.y, bsize.z);
4666
+ const surfaceFieldScale = maxDim / 5;
3846
4667
  const fov = 45;
3847
4668
  const distance = maxDim / (2 * Math.tan(fov * Math.PI / 360)) * 1.6;
3848
4669
  const cameraFov = ((_c = requestedSceneState == null ? void 0 : requestedSceneState.camera) == null ? void 0 : _c.fov) ?? ((_d = sceneConfig == null ? void 0 : sceneConfig.camera) == null ? void 0 : _d.fov) ?? fov;
@@ -3949,15 +4770,17 @@ function createSession(code, opts) {
3949
4770
  edgeSegments: Math.floor((((_m = geo.edges.getAttribute("position")) == null ? void 0 : _m.count) ?? 0) / 2)
3950
4771
  });
3951
4772
  const materialDefaults = renderStylePreset.material;
4773
+ const surfaceField = renderStylePreset.surfaceField;
3952
4774
  const authoredMaterialOpacity = mp == null ? void 0 : mp.opacity;
3953
4775
  const authoredMaterialTransmission = mp == null ? void 0 : mp.transmission;
3954
4776
  const hasAuthoredTransparency = authoredMaterialOpacity !== void 0 && authoredMaterialOpacity < 0.99 || authoredMaterialTransmission !== void 0 && authoredMaterialTransmission > 0;
3955
4777
  const transparentDefaults = hasAuthoredTransparency ? materialDefaults.authoredTransparent : materialDefaults;
3956
4778
  const materialOpacity = Math.min(obj.opacity, authoredMaterialOpacity ?? materialDefaults.opacity);
3957
4779
  const materialTransmission = authoredMaterialTransmission ?? transparentDefaults.transmission;
4780
+ const objectColor = parseColor(obj.color, CAD_MATERIAL_PROPS.color);
3958
4781
  const solidMaterialProps = {
3959
4782
  ...CAD_MATERIAL_PROPS,
3960
- color: parseColor(obj.color, CAD_MATERIAL_PROPS.color),
4783
+ color: objectColor,
3961
4784
  metalness: (mp == null ? void 0 : mp.metalness) ?? materialDefaults.metalness,
3962
4785
  roughness: (mp == null ? void 0 : mp.roughness) ?? transparentDefaults.roughness,
3963
4786
  clearcoat: (mp == null ? void 0 : mp.clearcoat) ?? transparentDefaults.clearcoat,
@@ -3973,7 +4796,12 @@ function createSession(code, opts) {
3973
4796
  ...(mp == null ? void 0 : mp.emissiveIntensity) !== void 0 && { emissiveIntensity: mp.emissiveIntensity },
3974
4797
  ...(mp == null ? void 0 : mp.wireframe) && { wireframe: true }
3975
4798
  };
3976
- solidMaterial = new MeshPhysicalMaterial({
4799
+ solidMaterial = surfaceField.enabled ? createSurfaceFieldMaterial({
4800
+ field: surfaceField,
4801
+ objectColor,
4802
+ clippingPlanes: applicableCutPlanes,
4803
+ fieldScale: surfaceFieldScale
4804
+ }) : new MeshPhysicalMaterial({
3977
4805
  ...solidMaterialProps,
3978
4806
  transparent: materialOpacity < 1 || materialTransmission > 0,
3979
4807
  opacity: materialOpacity,
@@ -4051,6 +4879,19 @@ function createSession(code, opts) {
4051
4879
  ...sdfRaymarch ? { sdfRaymarch } : {}
4052
4880
  });
4053
4881
  }
4882
+ const renderableById = new Map(renderables.map((renderable) => [renderable.id, renderable]));
4883
+ const connectivityBodyInput = (opts == null ? void 0 : opts.includeConnectivity) ? buildMeshBodyConnectivityInput(
4884
+ shapeVisibleObjs.map(
4885
+ (entry) => {
4886
+ var _a2;
4887
+ return meshConnectivitySource(entry, cloneGeometryPositions((_a2 = renderableById.get(entry.source.id)) == null ? void 0 : _a2.solid.geometry));
4888
+ }
4889
+ ),
4890
+ { bodyShape: () => new EmptyInspectionShape() }
4891
+ ) : null;
4892
+ const connectivityEntries = (connectivityBodyInput == null ? void 0 : connectivityBodyInput.entries) ?? [];
4893
+ const groundOffset = Number.isFinite((_n = sceneConfig == null ? void 0 : sceneConfig.ground) == null ? void 0 : _n.offset) ? sceneConfig.ground.offset : 0;
4894
+ const floatingGroundZ = bbox.min[2] - groundOffset;
4054
4895
  let sceneConfigCameraState = null;
4055
4896
  if ((sceneConfig == null ? void 0 : sceneConfig.camera) && !(requestedSceneState == null ? void 0 : requestedSceneState.camera)) {
4056
4897
  const cam = sceneConfig.camera;
@@ -4068,7 +4909,7 @@ function createSession(code, opts) {
4068
4909
  }
4069
4910
  const cameraSpec = sceneConfigCameraState && (opts == null ? void 0 : opts.capture) === "section-sweep" ? fitCameraStateToBounds(sceneConfigCameraState, bbox, cameraFov) : (requestedSceneState == null ? void 0 : requestedSceneState.camera) ?? sceneConfigCameraState;
4070
4911
  const cameraRig = buildCameraRig(center, distance, maxDim, cameraSpec, cameraFov);
4071
- const explicitCameraFov = (cameraSpec == null ? void 0 : cameraSpec.fov) ?? ((_n = sceneConfig == null ? void 0 : sceneConfig.camera) == null ? void 0 : _n.fov);
4912
+ const explicitCameraFov = (cameraSpec == null ? void 0 : cameraSpec.fov) ?? ((_o = sceneConfig == null ? void 0 : sceneConfig.camera) == null ? void 0 : _o.fov);
4072
4913
  if (explicitCameraFov && cameraRig.camera instanceof PerspectiveCamera) {
4073
4914
  cameraRig.camera.fov = explicitCameraFov;
4074
4915
  cameraRig.camera.updateProjectionMatrix();
@@ -4090,8 +4931,10 @@ function createSession(code, opts) {
4090
4931
  sectionShapes,
4091
4932
  connectivityEntries,
4092
4933
  connectivityBodyInput,
4934
+ floatingGroundZ,
4093
4935
  physicalConnectivity: null,
4094
4936
  distanceReport: null,
4937
+ floatingReport: null,
4095
4938
  collisionEntries,
4096
4939
  collisionReport: null,
4097
4940
  thicknessInspection: null,
@@ -4121,6 +4964,7 @@ function createSession(code, opts) {
4121
4964
  }
4122
4965
  async function setup() {
4123
4966
  await init();
4967
+ setActiveBackend(CLI_DEFAULT_BACKEND);
4124
4968
  window.__forgeCaptureCapabilities = CAPTURE_RUNTIME_CAPABILITIES;
4125
4969
  window.__forgeReady = true;
4126
4970
  }
@@ -4161,7 +5005,7 @@ window.__forgeRender = async (code, opts) => {
4161
5005
  hide: opts == null ? void 0 : opts.hide,
4162
5006
  paramOverrides: opts == null ? void 0 : opts.paramOverrides,
4163
5007
  renderStyle: opts == null ? void 0 : opts.renderStyle,
4164
- includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("distance"),
5008
+ includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("floating") || requestedChannels.has("distance") || requestedChannels.has("thickness"),
4165
5009
  includeCollisions: requestedChannels.has("collisions"),
4166
5010
  capture: "orbit"
4167
5011
  });
@@ -4188,14 +5032,17 @@ window.__forgeRender = async (code, opts) => {
4188
5032
  const renders = {};
4189
5033
  const depthRenders = {};
4190
5034
  const normalRenders = {};
5035
+ const zebraRenders = {};
4191
5036
  const maskRenders = {};
4192
5037
  const connectivityRenders = {};
5038
+ const floatingRenders = {};
4193
5039
  const distanceRenders = {};
4194
5040
  const collisionRenders = {};
4195
5041
  const thicknessRenders = {};
4196
5042
  const roughnessRenders = {};
4197
5043
  let maskObjects = [];
4198
5044
  let connectivityReport = null;
5045
+ let floatingReport = null;
4199
5046
  let distanceReport = null;
4200
5047
  let collisionReport = null;
4201
5048
  let thicknessReport = null;
@@ -4242,6 +5089,11 @@ window.__forgeRender = async (code, opts) => {
4242
5089
  normalRenders[label] = renderCurrentNormals(session);
4243
5090
  await markChannelViewDone("normals", label);
4244
5091
  }
5092
+ if (requestedChannels.has("zebra")) {
5093
+ await markChannelViewStart("zebra", label);
5094
+ zebraRenders[label] = renderCurrentZebra(session);
5095
+ await markChannelViewDone("zebra", label);
5096
+ }
4245
5097
  if (requestedChannels.has("roughness")) {
4246
5098
  await markChannelViewStart("roughness", label);
4247
5099
  const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
@@ -4267,6 +5119,13 @@ window.__forgeRender = async (code, opts) => {
4267
5119
  connectivityReport = connectivity.report;
4268
5120
  await markChannelViewDone("connectivity", label);
4269
5121
  }
5122
+ if (requestedChannels.has("floating")) {
5123
+ await markChannelViewStart("floating", label);
5124
+ const floating = renderCurrentFloating(session);
5125
+ floatingRenders[label] = floating.png;
5126
+ floatingReport = floating.report;
5127
+ await markChannelViewDone("floating", label);
5128
+ }
4270
5129
  if (requestedChannels.has("distance")) {
4271
5130
  await markChannelViewStart("distance", label);
4272
5131
  const distance = renderCurrentDistance(session);
@@ -4311,6 +5170,7 @@ window.__forgeRender = async (code, opts) => {
4311
5170
  renders,
4312
5171
  depth: depthRenders,
4313
5172
  normals: normalRenders,
5173
+ zebra: zebraRenders,
4314
5174
  roughness: roughnessReport ? {
4315
5175
  ...roughnessReport,
4316
5176
  pointCloud: roughnessPointCloud,
@@ -4328,6 +5188,12 @@ window.__forgeRender = async (code, opts) => {
4328
5188
  } : {
4329
5189
  views: connectivityRenders
4330
5190
  },
5191
+ floating: floatingReport ? {
5192
+ ...floatingReport,
5193
+ views: floatingRenders
5194
+ } : {
5195
+ views: floatingRenders
5196
+ },
4331
5197
  distance: distanceReport ? {
4332
5198
  ...distanceReport,
4333
5199
  views: distanceRenders