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
@@ -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, bX as initSolverWasm, bW as initKernel, S as Scene, bY as BoxGeometry, bs as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bZ as localAabbPlaneRelation, h as Vector2, b_ as ShapeUtils, b$ as analyzePhysicalConnectivity, c0 as meshContactDataFor, c1 as AabbSpatialIndex, c2 as detectPhysicalContact, g as Vector3, c3 as resolveThicknessInspectionOptions, R as Raycaster, c4 as thicknessColor, c5 as thicknessClass, aU as BufferAttribute, c6 as roughnessClassForAngle, a0 as MathUtils, c7 as resolveRoughnessInspectionOptions, G as Box3, c8 as roughnessColorForAngle, c9 as roughnessScoreForAngle, e as Color, aC as resolveForgeRenderStyle, bb as getRenderStylePreset, ax as setParamOverrides, b7 as runScript, ca 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, cb as intersectWithPlane, cc as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, cd as parseCameraCliSpec, ce as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bO as findJointAnimationClip, p as Plane, bk as SURFACE_FIELD_FRAGMENT_SHADER, bl as SURFACE_FIELD_VERTEX_SHADER, Y as Vector4, $ as Matrix4, bv as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bu as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bP as resolveJointAnimation, bQ as resolveJointViewValues, bo as ROUGHNESS_COLORS, cf as PointsMaterial, cg as Points, b2 as analyzeCollisionIntersections, ch as serializeCollisionFinding, ci as summarizeThicknessSamples, br as THICKNESS_COLORS, cj as worldAuthorPlaneToLocal, a$ as SpotLight, bV as resolveScalarSceneSampleBudget } from "../scalar-sampling-budget-Bmewod18.js";
5
+ import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-CWO8rHlt.js";
6
6
  const CAD_MATERIAL_PROPS = {
7
7
  color: 6003669,
8
8
  metalness: 0.05,
@@ -457,234 +457,6 @@ function computeMeshSectionCap(mesh, planeInput) {
457
457
  warnings: stitched.warnings.length > 0 ? stitched.warnings : void 0
458
458
  };
459
459
  }
460
- const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
461
- contactTolerance: 0.05,
462
- minOverlapVolume: 0.1,
463
- exactGeometry: true
464
- };
465
- const AXIS_NAMES = ["x", "y", "z"];
466
- let UnionFind$1 = class UnionFind {
467
- constructor(size) {
468
- __publicField(this, "parent");
469
- __publicField(this, "rank");
470
- this.parent = Array.from({ length: size }, (_, index) => index);
471
- this.rank = Array.from({ length: size }, () => 0);
472
- }
473
- find(value) {
474
- const parent = this.parent[value];
475
- if (parent === value) return value;
476
- const root = this.find(parent);
477
- this.parent[value] = root;
478
- return root;
479
- }
480
- union(a, b) {
481
- const rootA = this.find(a);
482
- const rootB = this.find(b);
483
- if (rootA === rootB) return;
484
- if (this.rank[rootA] < this.rank[rootB]) {
485
- this.parent[rootA] = rootB;
486
- return;
487
- }
488
- if (this.rank[rootA] > this.rank[rootB]) {
489
- this.parent[rootB] = rootA;
490
- return;
491
- }
492
- this.parent[rootB] = rootA;
493
- this.rank[rootA] += 1;
494
- }
495
- };
496
- function cloneVec3(value) {
497
- return [value[0], value[1], value[2]];
498
- }
499
- function emptyBBox$1() {
500
- return {
501
- min: [Infinity, Infinity, Infinity],
502
- max: [-Infinity, -Infinity, -Infinity]
503
- };
504
- }
505
- function expandBBox$1(target, min, max) {
506
- for (let axis = 0; axis < 3; axis += 1) {
507
- target.min[axis] = Math.min(target.min[axis], min[axis]);
508
- target.max[axis] = Math.max(target.max[axis], max[axis]);
509
- }
510
- }
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
- function collectCandidatePairs(entries, tolerance) {
518
- if (entries.length < 2) return [];
519
- const index = new AabbSpatialIndex(entries);
520
- const pairs = index.overlapPairs({ padding: tolerance }).pairs.map((pair) => ({
521
- sourceIndex: pair.sourceIndex,
522
- targetIndex: pair.targetIndex,
523
- gaps: aabbGaps(entries[pair.sourceIndex], entries[pair.targetIndex])
524
- })).filter((pair) => Math.max(pair.gaps[0], pair.gaps[1], pair.gaps[2]) <= tolerance);
525
- pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
526
- return pairs;
527
- }
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) {
558
- if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
559
- return Math.max(0, Math.round(entry.bodyCount));
560
- }
561
- return 1;
562
- }
563
- function makeEdge(entries, sourceIndex, targetIndex, edge) {
564
- const source = entries[sourceIndex];
565
- const target = entries[targetIndex];
566
- return {
567
- sourceIndex,
568
- targetIndex,
569
- sourceId: source.id,
570
- targetId: target.id,
571
- sourceName: source.name,
572
- targetName: target.name,
573
- ...edge
574
- };
575
- }
576
- function analyzePhysicalConnectivity(entries, rawOptions = {}) {
577
- const exactGeometry = rawOptions.exactGeometry ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.exactGeometry;
578
- const options = {
579
- contactTolerance: rawOptions.contactTolerance ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance,
580
- minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.minOverlapVolume,
581
- exactGeometry,
582
- mergeOverlappingBBoxes: rawOptions.mergeOverlappingBBoxes ?? !exactGeometry,
583
- mergeTouchingBBoxes: rawOptions.mergeTouchingBBoxes ?? !exactGeometry
584
- };
585
- const warnings = [];
586
- const edges = [];
587
- const unionFind = new UnionFind$1(entries.length);
588
- for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
589
- const i = pair.sourceIndex;
590
- 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
- }
634
- }
635
- const objects = entries.map((entry, index) => ({
636
- index,
637
- id: entry.id,
638
- name: entry.name,
639
- groupName: entry.groupName,
640
- treePath: entry.treePath,
641
- mock: entry.mock === true,
642
- bodyCount: bodyCountForEntry(entry),
643
- bbox: {
644
- min: cloneVec3(entry.min),
645
- max: cloneVec3(entry.max)
646
- },
647
- componentIndex: 0
648
- }));
649
- const componentByRoot = /* @__PURE__ */ new Map();
650
- const rootToComponentIndex = /* @__PURE__ */ new Map();
651
- for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
652
- const root = unionFind.find(objectIndex);
653
- let component = componentByRoot.get(root);
654
- if (!component) {
655
- component = {
656
- index: componentByRoot.size + 1,
657
- objectIndexes: [],
658
- objectIds: [],
659
- objectNames: [],
660
- objectCount: 0,
661
- bodyCount: 0,
662
- bbox: emptyBBox$1()
663
- };
664
- componentByRoot.set(root, component);
665
- rootToComponentIndex.set(root, component.index);
666
- }
667
- const object = objects[objectIndex];
668
- object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
669
- component.objectIndexes.push(object.index);
670
- component.objectIds.push(object.id);
671
- component.objectNames.push(object.name);
672
- component.objectCount += 1;
673
- component.bodyCount += object.bodyCount;
674
- expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
675
- }
676
- const components = [...componentByRoot.values()];
677
- return {
678
- method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
679
- options,
680
- objectCount: objects.length,
681
- componentCount: components.length,
682
- objects,
683
- components,
684
- edges,
685
- warnings
686
- };
687
- }
688
460
  const EPSILON = 1e-9;
689
461
  function intervalGap(aMin, aMax, bMin, bMax) {
690
462
  if (aMax < bMin) return bMin - aMax;
@@ -876,6 +648,185 @@ function analyzeDistanceInspection(entries, rawOptions = {}) {
876
648
  warnings: [...connectivity.warnings]
877
649
  };
878
650
  }
651
+ const DEFAULT_CONTACT_TOLERANCE = 0.05;
652
+ const DEFAULT_BED_TOLERANCE = 0.05;
653
+ const DEFAULT_GROUND_Z = 0;
654
+ const MIN_SHAPE_OVERLAP_VOLUME = 1e-9;
655
+ let UnionFind$1 = class UnionFind {
656
+ constructor(size) {
657
+ __publicField(this, "parent");
658
+ __publicField(this, "rank");
659
+ this.parent = Array.from({ length: size }, (_, index) => index);
660
+ this.rank = Array.from({ length: size }, () => 0);
661
+ }
662
+ find(value) {
663
+ const parent = this.parent[value];
664
+ if (parent === value) return value;
665
+ const root = this.find(parent);
666
+ this.parent[value] = root;
667
+ return root;
668
+ }
669
+ union(a, b) {
670
+ const rootA = this.find(a);
671
+ const rootB = this.find(b);
672
+ if (rootA === rootB) return;
673
+ if (this.rank[rootA] < this.rank[rootB]) {
674
+ this.parent[rootA] = rootB;
675
+ return;
676
+ }
677
+ if (this.rank[rootA] > this.rank[rootB]) {
678
+ this.parent[rootB] = rootA;
679
+ return;
680
+ }
681
+ this.parent[rootB] = rootA;
682
+ this.rank[rootA] += 1;
683
+ }
684
+ };
685
+ function cloneVec3(value) {
686
+ return [value[0], value[1], value[2]];
687
+ }
688
+ function emptyBBox$1() {
689
+ return {
690
+ min: [Infinity, Infinity, Infinity],
691
+ max: [-Infinity, -Infinity, -Infinity]
692
+ };
693
+ }
694
+ function expandBBox$1(target, min, max) {
695
+ for (let axis = 0; axis < 3; axis += 1) {
696
+ target.min[axis] = Math.min(target.min[axis], min[axis]);
697
+ target.max[axis] = Math.max(target.max[axis], max[axis]);
698
+ }
699
+ }
700
+ function bodyCountForEntry(entry) {
701
+ if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
702
+ return Math.max(0, Math.round(entry.bodyCount));
703
+ }
704
+ return 1;
705
+ }
706
+ function resolveNonNegative(value, fallback) {
707
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
708
+ }
709
+ function resolveFinite(value, fallback) {
710
+ return Number.isFinite(value) ? value : fallback;
711
+ }
712
+ function analyzeFloatingInspection(entries, rawOptions = {}) {
713
+ const options = {
714
+ contactTolerance: resolveNonNegative(rawOptions.contactTolerance, DEFAULT_CONTACT_TOLERANCE),
715
+ bedTolerance: resolveNonNegative(rawOptions.bedTolerance, DEFAULT_BED_TOLERANCE),
716
+ groundZ: resolveFinite(rawOptions.groundZ, DEFAULT_GROUND_Z)
717
+ };
718
+ const unionFind = new UnionFind$1(entries.length);
719
+ const contacts = [];
720
+ const warnings = [];
721
+ const meshDataByIndex = /* @__PURE__ */ new Map();
722
+ entries.forEach((entry, index2) => {
723
+ const meshData = meshContactDataFor(entry);
724
+ if (meshData) meshDataByIndex.set(index2, meshData);
725
+ });
726
+ const candidateTolerance = options.contactTolerance > 0 ? options.contactTolerance : Number.EPSILON;
727
+ const index = new AabbSpatialIndex(entries);
728
+ const contactOptions = {
729
+ contactTolerance: options.contactTolerance,
730
+ minOverlapVolume: MIN_SHAPE_OVERLAP_VOLUME,
731
+ exactGeometry: true,
732
+ mergeOverlappingBBoxes: false,
733
+ mergeTouchingBBoxes: false
734
+ };
735
+ for (const pair of index.overlapPairs({ padding: candidateTolerance }).pairs) {
736
+ const source = entries[pair.sourceIndex];
737
+ const target = entries[pair.targetIndex];
738
+ const detection = detectPhysicalContact(source, target, contactOptions, {
739
+ sourceMesh: meshDataByIndex.get(pair.sourceIndex) ?? null,
740
+ targetMesh: meshDataByIndex.get(pair.targetIndex) ?? null
741
+ });
742
+ if (detection.warning) warnings.push(detection.warning);
743
+ if (!detection.contact) continue;
744
+ unionFind.union(pair.sourceIndex, pair.targetIndex);
745
+ contacts.push({
746
+ sourceIndex: pair.sourceIndex,
747
+ targetIndex: pair.targetIndex,
748
+ sourceId: source.id,
749
+ targetId: target.id,
750
+ sourceName: source.name,
751
+ targetName: target.name,
752
+ gap: detection.contact.gap
753
+ });
754
+ }
755
+ const objects = entries.map((entry, entryIndex) => ({
756
+ index: entryIndex,
757
+ id: entry.id,
758
+ name: entry.name,
759
+ groupName: entry.groupName,
760
+ treePath: entry.treePath,
761
+ mock: entry.mock === true,
762
+ bodyCount: bodyCountForEntry(entry),
763
+ bbox: {
764
+ min: cloneVec3(entry.min),
765
+ max: cloneVec3(entry.max)
766
+ },
767
+ componentIndex: 0,
768
+ isRootComponent: false,
769
+ isBedSupported: entry.min[2] <= options.groundZ + options.bedTolerance,
770
+ isFloating: false
771
+ }));
772
+ const componentByRoot = /* @__PURE__ */ new Map();
773
+ const rootToComponentIndex = /* @__PURE__ */ new Map();
774
+ for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
775
+ const root = unionFind.find(objectIndex);
776
+ let component = componentByRoot.get(root);
777
+ if (!component) {
778
+ component = {
779
+ index: componentByRoot.size + 1,
780
+ objectIndexes: [],
781
+ objectIds: [],
782
+ objectNames: [],
783
+ objectCount: 0,
784
+ bodyCount: 0,
785
+ bbox: emptyBBox$1(),
786
+ isRoot: false,
787
+ isBedSupported: false,
788
+ isFloating: false
789
+ };
790
+ componentByRoot.set(root, component);
791
+ rootToComponentIndex.set(root, component.index);
792
+ }
793
+ const object = objects[objectIndex];
794
+ object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
795
+ component.objectIndexes.push(object.index);
796
+ component.objectIds.push(object.id);
797
+ component.objectNames.push(object.name);
798
+ component.objectCount += 1;
799
+ component.bodyCount += object.bodyCount;
800
+ component.isBedSupported || (component.isBedSupported = object.isBedSupported);
801
+ expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
802
+ }
803
+ const components = [...componentByRoot.values()];
804
+ for (const component of components) {
805
+ component.isRoot = false;
806
+ component.isFloating = !component.isBedSupported;
807
+ }
808
+ const componentByIndex = new Map(components.map((component) => [component.index, component]));
809
+ for (const object of objects) {
810
+ const component = componentByIndex.get(object.componentIndex);
811
+ object.isRootComponent = false;
812
+ object.isBedSupported = (component == null ? void 0 : component.isBedSupported) ?? object.isBedSupported;
813
+ object.isFloating = (component == null ? void 0 : component.isFloating) ?? false;
814
+ }
815
+ return {
816
+ method: "mesh-contact-ground-reachability",
817
+ options,
818
+ rootComponentIndex: null,
819
+ objectCount: objects.length,
820
+ componentCount: components.length,
821
+ floatingComponentCount: components.filter((component) => component.isFloating).length,
822
+ floatingObjectCount: objects.filter((object) => object.isFloating).length,
823
+ floatingBodyCount: components.filter((component) => component.isFloating).reduce((total, component) => total + component.bodyCount, 0),
824
+ contacts,
825
+ objects,
826
+ components,
827
+ warnings
828
+ };
829
+ }
879
830
  const CAMERA_TOKEN_DIRECTIONS = {
880
831
  front: [0, -1, 0.2],
881
832
  back: [0, 1, 0.2],
@@ -908,6 +859,7 @@ function parseCameraToken(token) {
908
859
  `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
909
860
  );
910
861
  }
862
+ const CLI_DEFAULT_BACKEND = "manifold";
911
863
  function formatAvailableViews(views) {
912
864
  const names = Object.keys(views ?? {}).sort();
913
865
  if (names.length === 0) {
@@ -936,14 +888,36 @@ function resolveNamedSceneViewCamera(sceneConfig, viewName) {
936
888
  }
937
889
  return sceneViewCameraToViewportCameraState(view.camera);
938
890
  }
939
- function edgesStrokeAttrs(preset) {
891
+ function paletteForTheme(theme) {
892
+ if (theme === "cad-section") {
893
+ return {
894
+ background: "#f7f7f4",
895
+ fill: "#e6e8eb",
896
+ stroke: "#20262e",
897
+ hatchStroke: "#a8afb7",
898
+ strokeThin: 0.22,
899
+ strokeBold: 0.55,
900
+ useHatch: true
901
+ };
902
+ }
903
+ return {
904
+ background: "#2a2a2a",
905
+ fill: "#4488cc",
906
+ stroke: "#224466",
907
+ hatchStroke: "#224466",
908
+ strokeThin: 0.3,
909
+ strokeBold: 0.8,
910
+ useHatch: false
911
+ };
912
+ }
913
+ function edgesStrokeAttrs(preset, palette) {
940
914
  switch (preset) {
941
915
  case "off":
942
916
  return 'stroke="none"';
943
917
  case "bold":
944
- return 'stroke="#224466" stroke-width="0.8"';
918
+ return `stroke="${palette.stroke}" stroke-width="${palette.strokeBold}"`;
945
919
  default:
946
- return 'stroke="#224466" stroke-width="0.3"';
920
+ return `stroke="${palette.stroke}" stroke-width="${palette.strokeThin}"`;
947
921
  }
948
922
  }
949
923
  function combineBounds(left, right) {
@@ -958,6 +932,19 @@ function escapeAttribute(value) {
958
932
  function polygonPath(poly) {
959
933
  return `${poly.map((point, index) => `${index === 0 ? "M" : "L"}${point[0].toFixed(3)},${(-point[1]).toFixed(3)}`).join(" ")} Z`;
960
934
  }
935
+ function compoundPolygonPath(polygons) {
936
+ return polygons.map(polygonPath).join(" ");
937
+ }
938
+ function hatchDefs(palette) {
939
+ if (!palette.useHatch) return "";
940
+ return ` <defs>
941
+ <pattern id="forge-section-hatch" patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)">
942
+ <rect width="4" height="4" fill="${palette.fill}"/>
943
+ <line x1="0" y1="0" x2="0" y2="4" stroke="${palette.hatchStroke}" stroke-width="0.28"/>
944
+ </pattern>
945
+ </defs>
946
+ `;
947
+ }
961
948
  function prepareEntry(entry) {
962
949
  const polygons = entry.sketch.toPolygons();
963
950
  if (polygons.length === 0) {
@@ -979,7 +966,9 @@ function buildSketchSvgDocument(entries, options = {}) {
979
966
  throw new Error("Sketch SVG export requires at least one sketch payload.");
980
967
  }
981
968
  const edges = options.edges ?? "thin";
982
- const strokeAttrs = edgesStrokeAttrs(edges);
969
+ const palette = paletteForTheme(options.theme ?? "debug");
970
+ const strokeAttrs = edgesStrokeAttrs(edges, palette);
971
+ const fill = palette.useHatch ? "url(#forge-section-hatch)" : palette.fill;
983
972
  const prepared = entries.map(prepareEntry);
984
973
  const combinedBounds = prepared.reduce((acc, entry) => combineBounds(acc, entry.bounds), prepared[0].bounds);
985
974
  const margin = 2;
@@ -992,17 +981,15 @@ function buildSketchSvgDocument(entries, options = {}) {
992
981
  let pathCount = 0;
993
982
  const body = prepared.map((entry) => {
994
983
  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");
984
+ pathCount += entry.polygons.length;
999
985
  return ` <g${label}>
1000
- ${paths}
986
+ <path d="${compoundPolygonPath(entry.polygons)}" fill="${fill}" fill-rule="evenodd" ${strokeAttrs}/>
1001
987
  </g>`;
1002
988
  }).join("\n");
1003
989
  return {
1004
990
  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"/>
991
+ <rect x="${minX.toFixed(1)}" y="${(-maxY).toFixed(1)}" width="${width.toFixed(1)}" height="${height.toFixed(1)}" fill="${palette.background}"/>
992
+ ${hatchDefs(palette)}
1006
993
  ${body}
1007
994
  </svg>`,
1008
995
  width,
@@ -1011,136 +998,6 @@ ${body}
1011
998
  pathCount
1012
999
  };
1013
1000
  }
1014
- const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
1015
- minThickness: 1.2,
1016
- warnThickness: 2,
1017
- maxThickness: 6,
1018
- maxSamplesPerObject: 5e3
1019
- };
1020
- const THICKNESS_COLORS = {
1021
- critical: [255, 28, 28],
1022
- warning: [255, 150, 0],
1023
- ok: [60, 220, 90],
1024
- thick: [70, 145, 255],
1025
- unknown: [90, 90, 90]
1026
- };
1027
- function finitePositive(value, fallback, label) {
1028
- if (value === void 0) return fallback;
1029
- if (!Number.isFinite(value) || value <= 0) {
1030
- throw new Error(`${label} must be a positive finite number.`);
1031
- }
1032
- return value;
1033
- }
1034
- function resolveThicknessInspectionOptions(raw = {}) {
1035
- const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
1036
- const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
1037
- const maxThickness = finitePositive(raw.maxThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxThickness, "maxThickness");
1038
- const maxSamplesPerObject = finitePositive(
1039
- raw.maxSamplesPerObject,
1040
- DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
1041
- "maxSamplesPerObject"
1042
- );
1043
- if (minThickness > warnThickness) {
1044
- throw new Error("minThickness must be less than or equal to warnThickness.");
1045
- }
1046
- if (warnThickness > maxThickness) {
1047
- throw new Error("warnThickness must be less than or equal to maxThickness.");
1048
- }
1049
- return {
1050
- minThickness,
1051
- warnThickness,
1052
- maxThickness,
1053
- maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
1054
- };
1055
- }
1056
- function lerp(a, b, t) {
1057
- return a + (b - a) * Math.max(0, Math.min(1, t));
1058
- }
1059
- function lerpColor$1(a, b, t) {
1060
- return [Math.round(lerp(a[0], b[0], t)), Math.round(lerp(a[1], b[1], t)), Math.round(lerp(a[2], b[2], t))];
1061
- }
1062
- function thicknessClass(thickness, options) {
1063
- if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return "unknown";
1064
- if (thickness <= options.minThickness) return "critical";
1065
- if (thickness <= options.warnThickness) return "warning";
1066
- if (thickness <= options.maxThickness) return "ok";
1067
- return "thick";
1068
- }
1069
- function thicknessColor(thickness, options) {
1070
- const cls = thicknessClass(thickness, options);
1071
- if (cls === "unknown") return THICKNESS_COLORS.unknown;
1072
- if (cls === "critical") return THICKNESS_COLORS.critical;
1073
- if (cls === "warning") {
1074
- const span = Math.max(1e-9, options.warnThickness - options.minThickness);
1075
- return lerpColor$1(THICKNESS_COLORS.critical, THICKNESS_COLORS.warning, ((thickness ?? 0) - options.minThickness) / span);
1076
- }
1077
- if (cls === "ok") {
1078
- const span = Math.max(1e-9, options.maxThickness - options.warnThickness);
1079
- return lerpColor$1(THICKNESS_COLORS.ok, THICKNESS_COLORS.thick, ((thickness ?? 0) - options.warnThickness) / span);
1080
- }
1081
- return THICKNESS_COLORS.thick;
1082
- }
1083
- function sampleArea(sample) {
1084
- const area = sample.area ?? 1;
1085
- return Number.isFinite(area) && area > 0 ? area : 1;
1086
- }
1087
- function weightedQuantile(samples, q) {
1088
- if (samples.length === 0) return null;
1089
- const sorted = [...samples].sort((a, b) => a.thickness - b.thickness);
1090
- const totalArea = sorted.reduce((sum, sample) => sum + sample.area, 0);
1091
- const target = totalArea * Math.max(0, Math.min(1, q));
1092
- let cumulative = 0;
1093
- for (const sample of sorted) {
1094
- cumulative += sample.area;
1095
- if (cumulative >= target) return sample.thickness;
1096
- }
1097
- return sorted[sorted.length - 1].thickness;
1098
- }
1099
- function percent(part, total) {
1100
- if (total <= 0) return 0;
1101
- return part / total * 100;
1102
- }
1103
- function summarizeThicknessSamples(samples, options) {
1104
- const resolved = [];
1105
- let totalArea = 0;
1106
- let resolvedArea = 0;
1107
- let unresolvedArea = 0;
1108
- let criticalArea = 0;
1109
- let warningArea = 0;
1110
- let weightedSum = 0;
1111
- for (const sample of samples) {
1112
- const area = sampleArea(sample);
1113
- totalArea += area;
1114
- const value = sample.thickness;
1115
- if (value == null || !Number.isFinite(value) || value <= 0) {
1116
- unresolvedArea += area;
1117
- continue;
1118
- }
1119
- resolved.push({ thickness: value, area });
1120
- resolvedArea += area;
1121
- weightedSum += value * area;
1122
- if (value <= options.minThickness) {
1123
- criticalArea += area;
1124
- } else if (value <= options.warnThickness) {
1125
- warningArea += area;
1126
- }
1127
- }
1128
- const values = resolved.map((sample) => sample.thickness);
1129
- return {
1130
- sampleCount: samples.length,
1131
- resolvedCount: resolved.length,
1132
- unresolvedCount: samples.length - resolved.length,
1133
- minThickness: values.length > 0 ? Math.min(...values) : null,
1134
- p05Thickness: weightedQuantile(resolved, 0.05),
1135
- medianThickness: weightedQuantile(resolved, 0.5),
1136
- meanThickness: resolvedArea > 0 ? weightedSum / resolvedArea : null,
1137
- maxThickness: values.length > 0 ? Math.max(...values) : null,
1138
- criticalAreaPercent: percent(criticalArea, resolvedArea),
1139
- warningAreaPercent: percent(warningArea, resolvedArea),
1140
- belowWarnAreaPercent: percent(criticalArea + warningArea, resolvedArea),
1141
- unresolvedAreaPercent: percent(unresolvedArea, totalArea)
1142
- };
1143
- }
1144
1001
  const MIN_TRIANGLE_AREA = 1e-12;
1145
1002
  const R2_ALPHA = 0.7548776662466927;
1146
1003
  const R2_BETA = 0.5698402909980532;
@@ -1249,22 +1106,45 @@ function geometryMaxDimension(geometry) {
1249
1106
  box.getSize(size);
1250
1107
  return Math.max(1, size.x, size.y, size.z);
1251
1108
  }
1252
- function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
1109
+ function firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, jumpableMeshes, point, direction, epsilon, far, contactTolerance) {
1253
1110
  const origin = point.clone().addScaledVector(direction, epsilon);
1254
1111
  raycaster.set(origin, direction);
1255
1112
  raycaster.near = epsilon;
1256
1113
  raycaster.far = far;
1257
- const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
1258
- return hit ? hit.distance + epsilon : null;
1114
+ const hits = raycaster.intersectObjects(rayTargetMeshes, false);
1115
+ for (const hit of hits) {
1116
+ if (hit.distance <= epsilon) continue;
1117
+ if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
1118
+ return hit.distance + epsilon;
1119
+ }
1120
+ return null;
1259
1121
  }
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);
1122
+ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid, normal, epsilon, far, contactTolerance) {
1123
+ const forward = firstOppositeSurfaceDistance(
1124
+ raycaster,
1125
+ rayTargetMeshes,
1126
+ jumpableMeshes,
1127
+ centroid,
1128
+ normal,
1129
+ epsilon,
1130
+ far,
1131
+ contactTolerance
1132
+ );
1133
+ const backward = firstOppositeSurfaceDistance(
1134
+ raycaster,
1135
+ rayTargetMeshes,
1136
+ jumpableMeshes,
1137
+ centroid,
1138
+ normal.clone().negate(),
1139
+ epsilon,
1140
+ far,
1141
+ contactTolerance
1142
+ );
1263
1143
  if (forward == null) return backward;
1264
1144
  if (backward == null) return forward;
1265
1145
  return Math.min(forward, backward);
1266
1146
  }
1267
- function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1147
+ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
1268
1148
  const options = resolveThicknessInspectionOptions(rawOptions);
1269
1149
  const geometry = cloneGeometryForFaceColors(sourceGeometry);
1270
1150
  const position = geometry.getAttribute("position");
@@ -1282,7 +1162,8 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1282
1162
  const triangleCount = Math.floor(position.count / 3);
1283
1163
  const surfaceTriangles = readSurfaceTriangles(position);
1284
1164
  const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
1285
- const maxDim = geometryMaxDimension(geometry);
1165
+ const connectedGeometries = context.connectedGeometries ?? [];
1166
+ const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
1286
1167
  const epsilon = Math.max(1e-4, maxDim * 1e-6);
1287
1168
  const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1288
1169
  const colors = new Float32Array(position.count * 3);
@@ -1291,7 +1172,15 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1291
1172
  const pointSamples = [];
1292
1173
  const warnings = [];
1293
1174
  const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1294
- const rayMesh = new Mesh(geometry, rayMaterial);
1175
+ const rayTargets = [
1176
+ { mesh: new Mesh(geometry, rayMaterial), jumpable: false },
1177
+ ...connectedGeometries.map((connectedGeometry) => ({
1178
+ mesh: new Mesh(connectedGeometry, rayMaterial),
1179
+ jumpable: true
1180
+ }))
1181
+ ];
1182
+ const rayTargetMeshes = rayTargets.map((target) => target.mesh);
1183
+ const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
1295
1184
  const raycaster = new Raycaster();
1296
1185
  if (surfaceTriangles.length === 0) {
1297
1186
  warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
@@ -1303,7 +1192,16 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1303
1192
  const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1304
1193
  for (const sample of surfaceSamples) {
1305
1194
  sampledTriangleIndexes.add(sample.triangle.index);
1306
- const thickness = triangleThickness(raycaster, rayMesh, sample.position, sample.normal, epsilon, far);
1195
+ const thickness = triangleThickness(
1196
+ raycaster,
1197
+ rayTargetMeshes,
1198
+ jumpableMeshes,
1199
+ sample.position,
1200
+ sample.normal,
1201
+ epsilon,
1202
+ far,
1203
+ options.contactTolerance
1204
+ );
1307
1205
  samples.push({ thickness, area: sample.area });
1308
1206
  const previous = triangleThicknessValues[sample.triangle.index];
1309
1207
  if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
@@ -1340,79 +1238,6 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1340
1238
  warnings
1341
1239
  };
1342
1240
  }
1343
- const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1344
- smoothAngleDeg: 5,
1345
- sharpAngleDeg: 30,
1346
- harshAngleDeg: 90,
1347
- maxSamplesPerObject: 5e3
1348
- };
1349
- const ROUGHNESS_COLORS = {
1350
- smooth: [62, 72, 84],
1351
- moderate: [255, 214, 0],
1352
- sharp: [255, 124, 34],
1353
- harsh: [255, 42, 96]
1354
- };
1355
- function resolveRoughnessInspectionOptions(raw = {}) {
1356
- const options = {
1357
- smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
1358
- sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
1359
- harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg,
1360
- maxSamplesPerObject: raw.maxSamplesPerObject ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.maxSamplesPerObject
1361
- };
1362
- if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
1363
- throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
1364
- }
1365
- if (!Number.isFinite(options.sharpAngleDeg) || options.sharpAngleDeg <= options.smoothAngleDeg) {
1366
- throw new Error(`sharpAngleDeg must be greater than smoothAngleDeg (got ${options.sharpAngleDeg}).`);
1367
- }
1368
- if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
1369
- throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
1370
- }
1371
- if (!Number.isFinite(options.maxSamplesPerObject) || options.maxSamplesPerObject <= 0) {
1372
- throw new Error(`maxSamplesPerObject must be a positive finite number (got ${options.maxSamplesPerObject}).`);
1373
- }
1374
- return {
1375
- ...options,
1376
- maxSamplesPerObject: Math.max(1, Math.floor(options.maxSamplesPerObject))
1377
- };
1378
- }
1379
- function roughnessClassForAngle(angleDeg, options) {
1380
- if (angleDeg >= options.harshAngleDeg) return "harsh";
1381
- if (angleDeg >= options.sharpAngleDeg) return "sharp";
1382
- if (angleDeg >= options.smoothAngleDeg) return "moderate";
1383
- return "smooth";
1384
- }
1385
- function roughnessScoreForAngle(angleDeg, options) {
1386
- if (angleDeg < options.sharpAngleDeg) return 0;
1387
- if (angleDeg < options.harshAngleDeg) {
1388
- return MathUtils.lerp(0.48, 0.82, (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg));
1389
- }
1390
- return 1;
1391
- }
1392
- function roughnessColorForAngle(angleDeg, options) {
1393
- const cls = roughnessClassForAngle(angleDeg, options);
1394
- if (cls === "smooth" || cls === "harsh") return ROUGHNESS_COLORS[cls];
1395
- if (cls === "moderate") {
1396
- return lerpRgb(
1397
- ROUGHNESS_COLORS.moderate,
1398
- ROUGHNESS_COLORS.sharp,
1399
- (angleDeg - options.smoothAngleDeg) / (options.sharpAngleDeg - options.smoothAngleDeg)
1400
- );
1401
- }
1402
- return lerpRgb(
1403
- ROUGHNESS_COLORS.sharp,
1404
- ROUGHNESS_COLORS.harsh,
1405
- (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg)
1406
- );
1407
- }
1408
- function lerpRgb(a, b, t) {
1409
- const clamped = MathUtils.clamp(t, 0, 1);
1410
- return [
1411
- Math.round(MathUtils.lerp(a[0], b[0], clamped)),
1412
- Math.round(MathUtils.lerp(a[1], b[1], clamped)),
1413
- Math.round(MathUtils.lerp(a[2], b[2], clamped))
1414
- ];
1415
- }
1416
1241
  function emptyRoughnessSummary() {
1417
1242
  return {
1418
1243
  triangleCount: 0,
@@ -1747,12 +1572,25 @@ function objectConnectivityEntry(object) {
1747
1572
  shape: object.shape,
1748
1573
  min: object.min,
1749
1574
  max: object.max,
1575
+ positions: object.positions,
1750
1576
  groupName: object.groupName,
1751
1577
  treePath: object.treePath,
1752
1578
  mock: object.mock,
1753
1579
  bodyCount: 1
1754
1580
  };
1755
1581
  }
1582
+ function componentPositions(sourcePositions, component) {
1583
+ const out = new Float32Array(component.triangleIndexes.length * 9);
1584
+ let outOffset = 0;
1585
+ for (const triangleIndex of component.triangleIndexes) {
1586
+ const sourceOffset = triangleIndex * 9;
1587
+ for (let offset = 0; offset < 9; offset += 1) {
1588
+ out[outOffset + offset] = sourcePositions[sourceOffset + offset];
1589
+ }
1590
+ outOffset += 9;
1591
+ }
1592
+ return out;
1593
+ }
1756
1594
  function buildMeshBodyConnectivityInput(objects, options) {
1757
1595
  const entries = [];
1758
1596
  const bodyIdsByObjectId = /* @__PURE__ */ new Map();
@@ -1776,6 +1614,7 @@ function buildMeshBodyConnectivityInput(objects, options) {
1776
1614
  shape: options.bodyShape(object, component),
1777
1615
  min: component.bbox.min,
1778
1616
  max: component.bbox.max,
1617
+ positions: componentPositions(object.positions, component),
1779
1618
  groupName: object.groupName,
1780
1619
  treePath: object.treePath,
1781
1620
  mock: object.mock,
@@ -1854,6 +1693,9 @@ class EmptyInspectionShape {
1854
1693
  const COLLISION_SOURCE_OPACITY = 0.22;
1855
1694
  const COLLISION_SOURCE_COLOR = [180, 200, 220];
1856
1695
  const COLLISION_HIGHLIGHT_COLOR = [255, 68, 16];
1696
+ const FLOATING_HIGHLIGHT_COLOR = [255, 68, 16];
1697
+ const FLOATING_CONTEXT_COLOR = [38, 49, 58];
1698
+ const FLOATING_HIDDEN_COLOR = [0, 0, 0];
1857
1699
  const COLLISION_PALETTE = [
1858
1700
  COLLISION_HIGHLIGHT_COLOR,
1859
1701
  [0, 204, 255],
@@ -1894,18 +1736,13 @@ const MASK_PALETTE = [
1894
1736
  [0, 0, 128],
1895
1737
  [128, 128, 128]
1896
1738
  ];
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
- }
1739
+ function cloneGeometryPositions(geometry) {
1740
+ if (!geometry) return void 0;
1741
+ const position = geometry.getAttribute("position");
1742
+ if (!position) return void 0;
1743
+ return new Float32Array(position.array);
1907
1744
  }
1908
- function meshConnectivitySource(entry) {
1745
+ function meshConnectivitySource(entry, positions) {
1909
1746
  const bb = entry.shape.boundingBox();
1910
1747
  return {
1911
1748
  id: entry.source.id,
@@ -1916,7 +1753,7 @@ function meshConnectivitySource(entry) {
1916
1753
  groupName: entry.source.groupName,
1917
1754
  treePath: entry.source.treePath,
1918
1755
  mock: entry.source.mock,
1919
- positions: cloneShapePositions(entry.shape)
1756
+ positions
1920
1757
  };
1921
1758
  }
1922
1759
  function summarizeSceneGeometry(entries) {
@@ -2268,6 +2105,37 @@ function renderCurrentNormals(session) {
2268
2105
  normalMaterial.dispose();
2269
2106
  }
2270
2107
  }
2108
+ function renderCurrentZebra(session) {
2109
+ const r = getRenderer(session.size, session.pixelRatio);
2110
+ const zebraMaterial = new ShaderMaterial({
2111
+ uniforms: {
2112
+ uAccentColor: { value: new Color(ZEBRA_ACCENT_COLOR) },
2113
+ uDarkColor: { value: new Color(ZEBRA_DARK_COLOR) },
2114
+ uLightColor: { value: new Color(ZEBRA_LIGHT_COLOR) },
2115
+ uStripeScale: { value: ZEBRA_STRIPE_SCALE },
2116
+ uStripeSoftness: { value: ZEBRA_STRIPE_SOFTNESS }
2117
+ },
2118
+ vertexShader: ZEBRA_STRIPE_VERTEX_SHADER,
2119
+ fragmentShader: ZEBRA_STRIPE_FRAGMENT_SHADER,
2120
+ side: DoubleSide
2121
+ });
2122
+ zebraMaterial.toneMapped = false;
2123
+ const prevOverride = session.scene.overrideMaterial;
2124
+ session.scene.overrideMaterial = zebraMaterial;
2125
+ try {
2126
+ return withSolidOnlyVisibility(
2127
+ session,
2128
+ () => withTemporarySceneBackground(session, new Color(0), () => {
2129
+ updateSdfRaymarchUniforms(session);
2130
+ r.render(session.scene, session.camera);
2131
+ return captureRenderedPng(session.size);
2132
+ })
2133
+ );
2134
+ } finally {
2135
+ session.scene.overrideMaterial = prevOverride;
2136
+ zebraMaterial.dispose();
2137
+ }
2138
+ }
2271
2139
  function maskColorForIndex(index) {
2272
2140
  return MASK_PALETTE[(index - 1) % MASK_PALETTE.length];
2273
2141
  }
@@ -2371,6 +2239,47 @@ function analyzeSessionConnectivity(session) {
2371
2239
  }
2372
2240
  return session.physicalConnectivity;
2373
2241
  }
2242
+ function addContactNeighbor(target, sourceId, targetId) {
2243
+ let neighbors = target.get(sourceId);
2244
+ if (!neighbors) {
2245
+ neighbors = /* @__PURE__ */ new Set();
2246
+ target.set(sourceId, neighbors);
2247
+ }
2248
+ neighbors.add(targetId);
2249
+ }
2250
+ function baseObjectIdForConnectivityEntry(entryId, renderableIds) {
2251
+ if (renderableIds.has(entryId)) return entryId;
2252
+ const bodySuffixIndex = entryId.lastIndexOf(":body:");
2253
+ if (bodySuffixIndex <= 0) return null;
2254
+ const baseId = entryId.slice(0, bodySuffixIndex);
2255
+ return renderableIds.has(baseId) ? baseId : null;
2256
+ }
2257
+ function buildThicknessRaycastConnectivityContext(session) {
2258
+ const report = analyzeSessionConnectivity(session);
2259
+ const renderableById = new Map(
2260
+ session.renderables.filter((renderable) => !renderable.sdfRaymarch).map((renderable) => [renderable.id, renderable])
2261
+ );
2262
+ const renderableIds = new Set(renderableById.keys());
2263
+ const neighborIdsByObjectId = /* @__PURE__ */ new Map();
2264
+ for (const edge of report.edges) {
2265
+ const sourceId = baseObjectIdForConnectivityEntry(edge.sourceId, renderableIds);
2266
+ const targetId = baseObjectIdForConnectivityEntry(edge.targetId, renderableIds);
2267
+ if (!sourceId || !targetId || sourceId === targetId) continue;
2268
+ addContactNeighbor(neighborIdsByObjectId, sourceId, targetId);
2269
+ addContactNeighbor(neighborIdsByObjectId, targetId, sourceId);
2270
+ }
2271
+ return { neighborIdsByObjectId, renderableById };
2272
+ }
2273
+ function connectedThicknessGeometriesFor(context, source) {
2274
+ const neighborIds = context.neighborIdsByObjectId.get(source.id);
2275
+ if (!neighborIds) return [];
2276
+ const geometries = [];
2277
+ for (const neighborId of neighborIds) {
2278
+ const renderable = context.renderableById.get(neighborId);
2279
+ if (renderable) geometries.push(renderable.solid.geometry);
2280
+ }
2281
+ return geometries;
2282
+ }
2374
2283
  function decorateConnectivityReport(report) {
2375
2284
  const components = report.components.map((component) => {
2376
2285
  const color = maskColorForIndex(component.index);
@@ -2539,6 +2448,151 @@ function renderCurrentDistance(session) {
2539
2448
  });
2540
2449
  }
2541
2450
  }
2451
+ function analyzeSessionFloating(session) {
2452
+ if (!session.floatingReport) {
2453
+ session.floatingReport = analyzeFloatingInspection(session.connectivityEntries, { groundZ: session.floatingGroundZ });
2454
+ }
2455
+ return session.floatingReport;
2456
+ }
2457
+ function decorateFloatingReport(report) {
2458
+ const components = report.components.map((component) => {
2459
+ const color = component.isFloating ? FLOATING_HIGHLIGHT_COLOR : FLOATING_HIDDEN_COLOR;
2460
+ return {
2461
+ ...component,
2462
+ color,
2463
+ hex: colorHex(color)
2464
+ };
2465
+ });
2466
+ const objectByComponentIndex = new Map(components.map((component) => [component.index, component]));
2467
+ const objects = report.objects.map((object) => {
2468
+ var _a;
2469
+ const color = ((_a = objectByComponentIndex.get(object.componentIndex)) == null ? void 0 : _a.color) ?? FLOATING_HIDDEN_COLOR;
2470
+ return {
2471
+ ...object,
2472
+ color,
2473
+ hex: colorHex(color)
2474
+ };
2475
+ });
2476
+ return {
2477
+ method: report.method,
2478
+ options: report.options,
2479
+ rootComponentIndex: report.rootComponentIndex,
2480
+ objectCount: report.objectCount,
2481
+ componentCount: report.componentCount,
2482
+ floatingComponentCount: report.floatingComponentCount,
2483
+ floatingObjectCount: report.floatingObjectCount,
2484
+ floatingBodyCount: report.floatingBodyCount,
2485
+ objects,
2486
+ components,
2487
+ contacts: report.contacts,
2488
+ warnings: report.warnings,
2489
+ style: {
2490
+ highlightColor: FLOATING_HIGHLIGHT_COLOR,
2491
+ highlightHex: colorHex(FLOATING_HIGHLIGHT_COLOR),
2492
+ contextColor: FLOATING_CONTEXT_COLOR,
2493
+ contextHex: colorHex(FLOATING_CONTEXT_COLOR),
2494
+ hiddenColor: FLOATING_HIDDEN_COLOR,
2495
+ hiddenHex: colorHex(FLOATING_HIDDEN_COLOR)
2496
+ }
2497
+ };
2498
+ }
2499
+ function renderCurrentFloating(session) {
2500
+ const r = getRenderer(session.size, session.pixelRatio);
2501
+ const report = decorateFloatingReport(analyzeSessionFloating(session));
2502
+ const byId = new Map(report.objects.map((object) => [object.id, object]));
2503
+ const vertexColorsById = session.connectivityBodyInput ? meshVertexColorBuffersFor(session.connectivityBodyInput, (entryId) => {
2504
+ var _a;
2505
+ return rgbFloats(((_a = byId.get(entryId)) == null ? void 0 : _a.color) ?? FLOATING_HIDDEN_COLOR);
2506
+ }) : /* @__PURE__ */ new Map();
2507
+ const replacements = session.renderables.map((renderable) => {
2508
+ var _a;
2509
+ const object = byId.get(renderable.id);
2510
+ const colors = vertexColorsById.get(renderable.id);
2511
+ const vertexGeometry = colors ? geometryWithVisibleVertexColors(renderable.solid.geometry, colors) : null;
2512
+ const previousMaterial = renderable.solid.material;
2513
+ const previousGeometry = renderable.solid.geometry;
2514
+ const previousVisible = renderable.solid.visible;
2515
+ const previousWireVisible = renderable.wire.visible;
2516
+ const previousShellVisible = (_a = renderable.shell) == null ? void 0 : _a.visible;
2517
+ const contextMaterial = new MeshBasicMaterial({
2518
+ color: colorHex(FLOATING_CONTEXT_COLOR),
2519
+ transparent: true,
2520
+ opacity: 0.18,
2521
+ side: DoubleSide,
2522
+ depthWrite: false,
2523
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null
2524
+ });
2525
+ contextMaterial.toneMapped = false;
2526
+ const highlightMaterial = new MeshBasicMaterial({
2527
+ color: vertexGeometry ? 16777215 : colorHex(FLOATING_HIGHLIGHT_COLOR),
2528
+ vertexColors: Boolean(vertexGeometry),
2529
+ side: DoubleSide,
2530
+ depthWrite: false,
2531
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null,
2532
+ polygonOffset: true,
2533
+ polygonOffsetFactor: -1,
2534
+ polygonOffsetUnits: -1
2535
+ });
2536
+ highlightMaterial.toneMapped = false;
2537
+ renderable.solid.material = contextMaterial;
2538
+ renderable.solid.geometry = previousGeometry;
2539
+ renderable.solid.visible = true;
2540
+ const highlightGeometry = vertexGeometry ?? ((object == null ? void 0 : object.isFloating) === true ? previousGeometry : null);
2541
+ const highlightMesh = highlightGeometry ? new Mesh(highlightGeometry, highlightMaterial) : null;
2542
+ if (highlightMesh) {
2543
+ highlightMesh.renderOrder = 5;
2544
+ highlightMesh.raycast = () => null;
2545
+ renderable.root.add(highlightMesh);
2546
+ }
2547
+ renderable.wire.visible = false;
2548
+ if (renderable.shell) renderable.shell.visible = false;
2549
+ return {
2550
+ renderable,
2551
+ previousMaterial,
2552
+ previousGeometry,
2553
+ previousVisible,
2554
+ previousWireVisible,
2555
+ previousShellVisible,
2556
+ vertexGeometry,
2557
+ contextMaterial,
2558
+ highlightMaterial,
2559
+ highlightMesh
2560
+ };
2561
+ });
2562
+ try {
2563
+ const png = withTemporarySceneBackground(session, new Color(0), () => {
2564
+ updateSdfRaymarchUniforms(session);
2565
+ r.render(session.scene, session.camera);
2566
+ return captureRenderedPng(session.size);
2567
+ });
2568
+ return { png, report };
2569
+ } finally {
2570
+ replacements.forEach(
2571
+ ({
2572
+ renderable,
2573
+ previousMaterial,
2574
+ previousGeometry,
2575
+ previousVisible,
2576
+ previousWireVisible,
2577
+ previousShellVisible,
2578
+ vertexGeometry,
2579
+ contextMaterial,
2580
+ highlightMaterial,
2581
+ highlightMesh
2582
+ }) => {
2583
+ if (highlightMesh) renderable.root.remove(highlightMesh);
2584
+ renderable.solid.material = previousMaterial;
2585
+ renderable.solid.geometry = previousGeometry;
2586
+ renderable.solid.visible = previousVisible;
2587
+ renderable.wire.visible = previousWireVisible;
2588
+ if (renderable.shell && previousShellVisible !== void 0) renderable.shell.visible = previousShellVisible;
2589
+ vertexGeometry == null ? void 0 : vertexGeometry.dispose();
2590
+ contextMaterial.dispose();
2591
+ highlightMaterial.dispose();
2592
+ }
2593
+ );
2594
+ }
2595
+ }
2542
2596
  function analyzeSessionCollisions(session) {
2543
2597
  if (!session.collisionReport) {
2544
2598
  session.collisionReport = analyzeCollisionIntersections(session.collisionEntries);
@@ -2549,7 +2603,15 @@ function decorateCollisionReport(report) {
2549
2603
  return {
2550
2604
  method: report.method,
2551
2605
  options: report.options,
2606
+ broadphase: report.broadphase,
2552
2607
  objectCount: report.objectCount,
2608
+ candidatePairCount: report.candidatePairCount,
2609
+ bboxVolumePrunedPairCount: report.bboxVolumePrunedPairCount,
2610
+ testedPairCount: report.testedPairCount,
2611
+ skippedPairCount: report.skippedPairCount,
2612
+ pairLimitSkippedPairCount: report.pairLimitSkippedPairCount,
2613
+ timeBudgetSkippedPairCount: report.timeBudgetSkippedPairCount,
2614
+ exactCheckMs: report.exactCheckMs,
2553
2615
  collisionCount: report.collisionCount,
2554
2616
  objects: report.objects,
2555
2617
  collisions: report.collisions.map((finding) => {
@@ -2637,6 +2699,26 @@ function renderCurrentCollisions(session) {
2637
2699
  function inspectionOptionsKey(value) {
2638
2700
  return JSON.stringify(value ?? {});
2639
2701
  }
2702
+ function scalarInspectableObjectCount(session) {
2703
+ return session.renderables.filter((renderable) => !renderable.sdfRaymarch).length;
2704
+ }
2705
+ function withSceneSampleBudget(session, options, explicitMaxSamplesPerObject) {
2706
+ const sampleBudget = resolveScalarSceneSampleBudget({
2707
+ objectCount: scalarInspectableObjectCount(session),
2708
+ maxSamplesPerObject: options.maxSamplesPerObject,
2709
+ explicitMaxSamplesPerObject
2710
+ });
2711
+ return {
2712
+ options: { ...options, maxSamplesPerObject: sampleBudget.effectiveMaxSamplesPerObject },
2713
+ sampleBudget
2714
+ };
2715
+ }
2716
+ function maybePushSceneSampleBudgetWarning(warnings, channel, sampleBudget) {
2717
+ if (!sampleBudget.capped) return;
2718
+ warnings.push(
2719
+ `${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.`
2720
+ );
2721
+ }
2640
2722
  function bboxFromGeometry(geometry) {
2641
2723
  geometry.computeBoundingBox();
2642
2724
  const bbox = geometry.boundingBox;
@@ -2717,22 +2799,27 @@ function renderScalarPointOverlays(session, overlays) {
2717
2799
  }
2718
2800
  function getSessionThicknessInspection(session, rawOptions) {
2719
2801
  var _a;
2720
- const options = resolveThicknessInspectionOptions(rawOptions);
2721
- const optionsKey = inspectionOptionsKey(options);
2802
+ const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
2803
+ const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
2804
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget });
2722
2805
  if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
2723
2806
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2724
2807
  const warnings = [];
2808
+ maybePushSceneSampleBudgetWarning(warnings, "Thickness", sampleBudget);
2725
2809
  const objects = [];
2726
2810
  const cloudObjects = [];
2727
2811
  const points = [];
2728
2812
  const overlays = [];
2813
+ const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
2729
2814
  session.renderables.forEach((renderable, index) => {
2730
2815
  const sourceObject = byId.get(renderable.id);
2731
2816
  if (renderable.sdfRaymarch) {
2732
2817
  warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh thickness inspection.`);
2733
2818
  return;
2734
2819
  }
2735
- const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options);
2820
+ const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, {
2821
+ connectedGeometries: connectedThicknessGeometriesFor(raycastConnectivity, renderable)
2822
+ });
2736
2823
  const bbox = bboxFromGeometry(analysis.geometry);
2737
2824
  const summary = summarizeThicknessSamples(analysis.samples, options);
2738
2825
  if (analysis.warnings.length > 0) {
@@ -2786,8 +2873,9 @@ function getSessionThicknessInspection(session, rawOptions) {
2786
2873
  points
2787
2874
  },
2788
2875
  report: {
2789
- method: "mesh-normal-raycast",
2876
+ method: "mesh-normal-raycast-contact-aware",
2790
2877
  options,
2878
+ sampleBudget,
2791
2879
  objectCount: objects.length,
2792
2880
  objects,
2793
2881
  warnings,
@@ -2815,11 +2903,13 @@ function renderCurrentRoughness(session, rawOptions) {
2815
2903
  }
2816
2904
  function getSessionRoughnessInspection(session, rawOptions) {
2817
2905
  var _a;
2818
- const options = resolveRoughnessInspectionOptions(rawOptions);
2819
- const optionsKey = inspectionOptionsKey(options);
2906
+ const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
2907
+ const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
2908
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget });
2820
2909
  if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
2821
2910
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2822
2911
  const warnings = [];
2912
+ maybePushSceneSampleBudgetWarning(warnings, "Roughness", sampleBudget);
2823
2913
  const objects = [];
2824
2914
  const cloudObjects = [];
2825
2915
  const points = [];
@@ -2882,6 +2972,7 @@ function getSessionRoughnessInspection(session, rawOptions) {
2882
2972
  report: {
2883
2973
  method: "mesh-dihedral-angle",
2884
2974
  options,
2975
+ sampleBudget,
2885
2976
  objectCount: objects.length,
2886
2977
  objects,
2887
2978
  warnings,
@@ -2900,14 +2991,14 @@ function getSessionRoughnessInspection(session, rawOptions) {
2900
2991
  }
2901
2992
  function emptySectionSvg() {
2902
2993
  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>',
2994
+ 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
2995
  width: 0,
2905
2996
  height: 0,
2906
2997
  area: 0,
2907
2998
  pathCount: 0
2908
2999
  };
2909
3000
  }
2910
- async function svgToPngDataUrl(svg, size, background = "#2a2a2a") {
3001
+ async function svgToPngDataUrl(svg, size, background = "#f7f7f4") {
2911
3002
  if (!exportCtx) {
2912
3003
  throw new Error("Could not create export canvas context.");
2913
3004
  }
@@ -2965,7 +3056,7 @@ async function renderSectionAtlas(session, opts) {
2965
3056
  sectionSketches.push({ name: entry.name, sketch });
2966
3057
  }
2967
3058
  }
2968
- const svgDocument = sectionSketches.length > 0 ? buildSketchSvgDocument(sectionSketches, { edges: "thin" }) : emptySectionSvg();
3059
+ const svgDocument = sectionSketches.length > 0 ? buildSketchSvgDocument(sectionSketches, { edges: "thin", theme: "cad-section" }) : emptySectionSvg();
2969
3060
  slices.push({
2970
3061
  index,
2971
3062
  offset,
@@ -3015,6 +3106,37 @@ function addRenderStyleLights(scene, style) {
3015
3106
  new HemisphereLight(new Color(lights.hemisphereSky), new Color(lights.hemisphereGround), lights.hemisphereIntensity)
3016
3107
  );
3017
3108
  }
3109
+ function fieldKindUniform(kind) {
3110
+ return kind === "hybrid" ? 1 : 0;
3111
+ }
3112
+ function createSurfaceFieldMaterial({
3113
+ field,
3114
+ objectColor,
3115
+ clippingPlanes,
3116
+ fieldScale
3117
+ }) {
3118
+ return new ShaderMaterial({
3119
+ vertexShader: SURFACE_FIELD_VERTEX_SHADER,
3120
+ fragmentShader: SURFACE_FIELD_FRAGMENT_SHADER,
3121
+ uniforms: {
3122
+ uAccentColor: { value: new Color(field.accentColor) },
3123
+ uBaseColor: { value: new Color(field.baseColor) },
3124
+ uDarkColor: { value: new Color(field.darkColor) },
3125
+ uFieldKind: { value: fieldKindUniform(field.kind) },
3126
+ uFieldScale: { value: fieldScale },
3127
+ uGlow: { value: field.glow },
3128
+ uLineColor: { value: new Color(field.lineColor) },
3129
+ uLineWidth: { value: field.lineWidth },
3130
+ uNodeColor: { value: new Color(field.nodeColor) },
3131
+ uObjectColor: { value: objectColor },
3132
+ uObjectColorMix: { value: field.objectColorMix },
3133
+ uSpacing: { value: field.spacing }
3134
+ },
3135
+ side: DoubleSide,
3136
+ toneMapped: false,
3137
+ clippingPlanes
3138
+ });
3139
+ }
3018
3140
  function createSceneLight(def) {
3019
3141
  const color = def.color ? new Color(def.color) : new Color(16777215);
3020
3142
  const intensity = def.intensity ?? 1;
@@ -3703,7 +3825,7 @@ function isFocusVisible(obj, focus, hide) {
3703
3825
  return true;
3704
3826
  }
3705
3827
  function createSession(code, opts) {
3706
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
3828
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
3707
3829
  const size = (opts == null ? void 0 : opts.size) ?? 1024;
3708
3830
  const pixelRatio = (opts == null ? void 0 : opts.pixelRatio) ?? 1;
3709
3831
  const debug = createCaptureDebugLogger(opts == null ? void 0 : opts.debug);
@@ -3785,8 +3907,6 @@ function createSession(code, opts) {
3785
3907
  name: entry.source.name,
3786
3908
  shape: entry.shape
3787
3909
  }));
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
3910
  const collisionEntries = (opts == null ? void 0 : opts.includeCollisions) ? shapeVisibleObjs.map((entry) => {
3791
3911
  const bb2 = entry.shape.boundingBox();
3792
3912
  return {
@@ -3843,6 +3963,7 @@ function createSession(code, opts) {
3843
3963
  const bsize = new Vector3();
3844
3964
  bb.getSize(bsize);
3845
3965
  const maxDim = Math.max(1, bsize.x, bsize.y, bsize.z);
3966
+ const surfaceFieldScale = maxDim / 5;
3846
3967
  const fov = 45;
3847
3968
  const distance = maxDim / (2 * Math.tan(fov * Math.PI / 360)) * 1.6;
3848
3969
  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 +4070,17 @@ function createSession(code, opts) {
3949
4070
  edgeSegments: Math.floor((((_m = geo.edges.getAttribute("position")) == null ? void 0 : _m.count) ?? 0) / 2)
3950
4071
  });
3951
4072
  const materialDefaults = renderStylePreset.material;
4073
+ const surfaceField = renderStylePreset.surfaceField;
3952
4074
  const authoredMaterialOpacity = mp == null ? void 0 : mp.opacity;
3953
4075
  const authoredMaterialTransmission = mp == null ? void 0 : mp.transmission;
3954
4076
  const hasAuthoredTransparency = authoredMaterialOpacity !== void 0 && authoredMaterialOpacity < 0.99 || authoredMaterialTransmission !== void 0 && authoredMaterialTransmission > 0;
3955
4077
  const transparentDefaults = hasAuthoredTransparency ? materialDefaults.authoredTransparent : materialDefaults;
3956
4078
  const materialOpacity = Math.min(obj.opacity, authoredMaterialOpacity ?? materialDefaults.opacity);
3957
4079
  const materialTransmission = authoredMaterialTransmission ?? transparentDefaults.transmission;
4080
+ const objectColor = parseColor(obj.color, CAD_MATERIAL_PROPS.color);
3958
4081
  const solidMaterialProps = {
3959
4082
  ...CAD_MATERIAL_PROPS,
3960
- color: parseColor(obj.color, CAD_MATERIAL_PROPS.color),
4083
+ color: objectColor,
3961
4084
  metalness: (mp == null ? void 0 : mp.metalness) ?? materialDefaults.metalness,
3962
4085
  roughness: (mp == null ? void 0 : mp.roughness) ?? transparentDefaults.roughness,
3963
4086
  clearcoat: (mp == null ? void 0 : mp.clearcoat) ?? transparentDefaults.clearcoat,
@@ -3973,7 +4096,12 @@ function createSession(code, opts) {
3973
4096
  ...(mp == null ? void 0 : mp.emissiveIntensity) !== void 0 && { emissiveIntensity: mp.emissiveIntensity },
3974
4097
  ...(mp == null ? void 0 : mp.wireframe) && { wireframe: true }
3975
4098
  };
3976
- solidMaterial = new MeshPhysicalMaterial({
4099
+ solidMaterial = surfaceField.enabled ? createSurfaceFieldMaterial({
4100
+ field: surfaceField,
4101
+ objectColor,
4102
+ clippingPlanes: applicableCutPlanes,
4103
+ fieldScale: surfaceFieldScale
4104
+ }) : new MeshPhysicalMaterial({
3977
4105
  ...solidMaterialProps,
3978
4106
  transparent: materialOpacity < 1 || materialTransmission > 0,
3979
4107
  opacity: materialOpacity,
@@ -4051,6 +4179,19 @@ function createSession(code, opts) {
4051
4179
  ...sdfRaymarch ? { sdfRaymarch } : {}
4052
4180
  });
4053
4181
  }
4182
+ const renderableById = new Map(renderables.map((renderable) => [renderable.id, renderable]));
4183
+ const connectivityBodyInput = (opts == null ? void 0 : opts.includeConnectivity) ? buildMeshBodyConnectivityInput(
4184
+ shapeVisibleObjs.map(
4185
+ (entry) => {
4186
+ var _a2;
4187
+ return meshConnectivitySource(entry, cloneGeometryPositions((_a2 = renderableById.get(entry.source.id)) == null ? void 0 : _a2.solid.geometry));
4188
+ }
4189
+ ),
4190
+ { bodyShape: () => new EmptyInspectionShape() }
4191
+ ) : null;
4192
+ const connectivityEntries = (connectivityBodyInput == null ? void 0 : connectivityBodyInput.entries) ?? [];
4193
+ const groundOffset = Number.isFinite((_n = sceneConfig == null ? void 0 : sceneConfig.ground) == null ? void 0 : _n.offset) ? sceneConfig.ground.offset : 0;
4194
+ const floatingGroundZ = bbox.min[2] - groundOffset;
4054
4195
  let sceneConfigCameraState = null;
4055
4196
  if ((sceneConfig == null ? void 0 : sceneConfig.camera) && !(requestedSceneState == null ? void 0 : requestedSceneState.camera)) {
4056
4197
  const cam = sceneConfig.camera;
@@ -4068,7 +4209,7 @@ function createSession(code, opts) {
4068
4209
  }
4069
4210
  const cameraSpec = sceneConfigCameraState && (opts == null ? void 0 : opts.capture) === "section-sweep" ? fitCameraStateToBounds(sceneConfigCameraState, bbox, cameraFov) : (requestedSceneState == null ? void 0 : requestedSceneState.camera) ?? sceneConfigCameraState;
4070
4211
  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);
4212
+ const explicitCameraFov = (cameraSpec == null ? void 0 : cameraSpec.fov) ?? ((_o = sceneConfig == null ? void 0 : sceneConfig.camera) == null ? void 0 : _o.fov);
4072
4213
  if (explicitCameraFov && cameraRig.camera instanceof PerspectiveCamera) {
4073
4214
  cameraRig.camera.fov = explicitCameraFov;
4074
4215
  cameraRig.camera.updateProjectionMatrix();
@@ -4090,8 +4231,10 @@ function createSession(code, opts) {
4090
4231
  sectionShapes,
4091
4232
  connectivityEntries,
4092
4233
  connectivityBodyInput,
4234
+ floatingGroundZ,
4093
4235
  physicalConnectivity: null,
4094
4236
  distanceReport: null,
4237
+ floatingReport: null,
4095
4238
  collisionEntries,
4096
4239
  collisionReport: null,
4097
4240
  thicknessInspection: null,
@@ -4121,6 +4264,7 @@ function createSession(code, opts) {
4121
4264
  }
4122
4265
  async function setup() {
4123
4266
  await init();
4267
+ setActiveBackend(CLI_DEFAULT_BACKEND);
4124
4268
  window.__forgeCaptureCapabilities = CAPTURE_RUNTIME_CAPABILITIES;
4125
4269
  window.__forgeReady = true;
4126
4270
  }
@@ -4161,7 +4305,7 @@ window.__forgeRender = async (code, opts) => {
4161
4305
  hide: opts == null ? void 0 : opts.hide,
4162
4306
  paramOverrides: opts == null ? void 0 : opts.paramOverrides,
4163
4307
  renderStyle: opts == null ? void 0 : opts.renderStyle,
4164
- includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("distance"),
4308
+ includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("floating") || requestedChannels.has("distance") || requestedChannels.has("thickness"),
4165
4309
  includeCollisions: requestedChannels.has("collisions"),
4166
4310
  capture: "orbit"
4167
4311
  });
@@ -4188,14 +4332,17 @@ window.__forgeRender = async (code, opts) => {
4188
4332
  const renders = {};
4189
4333
  const depthRenders = {};
4190
4334
  const normalRenders = {};
4335
+ const zebraRenders = {};
4191
4336
  const maskRenders = {};
4192
4337
  const connectivityRenders = {};
4338
+ const floatingRenders = {};
4193
4339
  const distanceRenders = {};
4194
4340
  const collisionRenders = {};
4195
4341
  const thicknessRenders = {};
4196
4342
  const roughnessRenders = {};
4197
4343
  let maskObjects = [];
4198
4344
  let connectivityReport = null;
4345
+ let floatingReport = null;
4199
4346
  let distanceReport = null;
4200
4347
  let collisionReport = null;
4201
4348
  let thicknessReport = null;
@@ -4242,6 +4389,11 @@ window.__forgeRender = async (code, opts) => {
4242
4389
  normalRenders[label] = renderCurrentNormals(session);
4243
4390
  await markChannelViewDone("normals", label);
4244
4391
  }
4392
+ if (requestedChannels.has("zebra")) {
4393
+ await markChannelViewStart("zebra", label);
4394
+ zebraRenders[label] = renderCurrentZebra(session);
4395
+ await markChannelViewDone("zebra", label);
4396
+ }
4245
4397
  if (requestedChannels.has("roughness")) {
4246
4398
  await markChannelViewStart("roughness", label);
4247
4399
  const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
@@ -4267,6 +4419,13 @@ window.__forgeRender = async (code, opts) => {
4267
4419
  connectivityReport = connectivity.report;
4268
4420
  await markChannelViewDone("connectivity", label);
4269
4421
  }
4422
+ if (requestedChannels.has("floating")) {
4423
+ await markChannelViewStart("floating", label);
4424
+ const floating = renderCurrentFloating(session);
4425
+ floatingRenders[label] = floating.png;
4426
+ floatingReport = floating.report;
4427
+ await markChannelViewDone("floating", label);
4428
+ }
4270
4429
  if (requestedChannels.has("distance")) {
4271
4430
  await markChannelViewStart("distance", label);
4272
4431
  const distance = renderCurrentDistance(session);
@@ -4311,6 +4470,7 @@ window.__forgeRender = async (code, opts) => {
4311
4470
  renders,
4312
4471
  depth: depthRenders,
4313
4472
  normals: normalRenders,
4473
+ zebra: zebraRenders,
4314
4474
  roughness: roughnessReport ? {
4315
4475
  ...roughnessReport,
4316
4476
  pointCloud: roughnessPointCloud,
@@ -4328,6 +4488,12 @@ window.__forgeRender = async (code, opts) => {
4328
4488
  } : {
4329
4489
  views: connectivityRenders
4330
4490
  },
4491
+ floating: floatingReport ? {
4492
+ ...floatingReport,
4493
+ views: floatingRenders
4494
+ } : {
4495
+ views: floatingRenders
4496
+ },
4331
4497
  distance: distanceReport ? {
4332
4498
  ...distanceReport,
4333
4499
  views: distanceRenders