forgecad 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/assets/{AdminPage-uTtcSXtn.js → AdminPage-DX0mpSZT.js} +1 -1
  2. package/dist/assets/{BlogPage-DYJMjWx3.js → BlogPage-CI_P0_Pf.js} +1 -1
  3. package/dist/assets/{DocsPage-C58f0K5v.js → DocsPage-DLhIIZyJ.js} +3 -3
  4. package/dist/assets/EditorApp-BujZvuwX.js +12874 -0
  5. package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
  6. package/dist/assets/{EmbedViewer-CMXWA2LX.js → EmbedViewer-0S0qXKog.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-CAu2OZFn.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
  8. package/dist/assets/{PricingPage-BIgW7m3X.js → PricingPage-DGkX3Ahr.js} +1 -1
  9. package/dist/assets/{SettingsPage-N1l1tMXO.js → SettingsPage-DBsqTB_y.js} +82 -22
  10. package/dist/assets/{app-CFy7g5WP.js → app-BE2nD6Yz.js} +1246 -191
  11. package/dist/assets/cli/{render-BrVVdj_T.js → render-iP9qh475.js} +841 -586
  12. package/dist/assets/{evalWorker-c_SB9gg3.js → evalWorker-Ds5U4xtN.js} +2732 -112
  13. package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
  14. package/dist/assets/{manifold-Dp6pvFr6.js → manifold-Bk26ViCr.js} +1 -1
  15. package/dist/assets/{manifold-CRoBhJKH.js → manifold-DjYsd7A_.js} +2 -2
  16. package/dist/assets/{manifold-Cjk7WhRs.js → manifold-sJ-axdXM.js} +1 -1
  17. package/dist/assets/{renderSceneState-3DfsSASX.js → renderSceneState-Bngp5MrQ.js} +1 -1
  18. package/dist/assets/{reportWorker-BLkuIoS8.js → reportWorker-CU8RZ4O0.js} +2715 -112
  19. package/dist/assets/{sectionPlaneMath-CykEnkvQ.js → sectionPlaneMath-BdTjyVfs.js} +3213 -252
  20. package/dist/cli/render.html +1 -1
  21. package/dist/docs/index.html +1 -1
  22. package/dist/docs-raw/AI/usage.md +7 -2
  23. package/dist/docs-raw/CLI.md +82 -53
  24. package/dist/docs-raw/beta-operations.md +9 -0
  25. package/dist/docs-raw/coding.md +1 -1
  26. package/dist/docs-raw/deployment.md +38 -23
  27. package/dist/docs-raw/generated/concepts.md +141 -7
  28. package/dist/docs-raw/generated/core.md +206 -1
  29. package/dist/docs-raw/generated/curves.md +97 -5
  30. package/dist/docs-raw/generated/lib.md +17 -1
  31. package/dist/docs-raw/generated/sketch.md +9 -1
  32. package/dist/docs-raw/generated/viewport.md +1 -1
  33. package/dist/docs-raw/guides/inspection-bundles.md +45 -16
  34. package/dist/docs-raw/platform/auth.md +2 -0
  35. package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
  36. package/dist/docs-raw/runbook.md +3 -3
  37. package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
  38. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
  39. package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
  40. package/dist/docs-raw/skills/index.md +2 -2
  41. package/dist/index.html +1 -1
  42. package/dist/sitemap.xml +6 -6
  43. package/dist-cli/forgecad.js +8725 -4747
  44. package/dist-cli/forgecad.js.map +1 -1
  45. package/dist-skill/CONTEXT.md +375 -25
  46. package/dist-skill/docs/CLI.md +82 -53
  47. package/dist-skill/docs/generated/core.md +206 -1
  48. package/dist-skill/docs/generated/curves.md +97 -5
  49. package/dist-skill/docs/generated/lib.md +17 -1
  50. package/dist-skill/docs/generated/sketch.md +9 -1
  51. package/dist-skill/docs/generated/viewport.md +1 -1
  52. package/dist-skill/docs/guides/inspection-bundles.md +45 -16
  53. package/dist-skill/docs-dev/CLI.md +82 -53
  54. package/dist-skill/docs-dev/coding.md +1 -1
  55. package/dist-skill/docs-dev/generated/core.md +206 -1
  56. package/dist-skill/docs-dev/generated/curves.md +97 -5
  57. package/dist-skill/docs-dev/generated/lib.md +17 -1
  58. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  59. package/dist-skill/docs-dev/generated/viewport.md +1 -1
  60. package/dist-skill/docs-dev/guides/inspection-bundles.md +45 -16
  61. package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
  62. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
  63. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
  64. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
  65. package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
  66. package/examples/api/bolted-service-cover.forge.js +17 -0
  67. package/examples/api/cable-gland-anchor.forge.js +14 -0
  68. package/examples/api/captured-cartridge-guide.forge.js +14 -0
  69. package/examples/api/captured-linear-slide.forge.js +13 -0
  70. package/examples/api/clevis-pin-joint.forge.js +13 -0
  71. package/examples/api/datum-enclosure.forge.js +16 -0
  72. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  73. package/examples/api/hose-barb-port.forge.js +14 -0
  74. package/examples/api/intentional-overlap-overmold.forge.js +16 -0
  75. package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
  76. package/examples/api/living-hinge-cover.forge.js +14 -0
  77. package/examples/api/pcb-terminal-block.forge.js +22 -0
  78. package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
  79. package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
  80. package/examples/api/routed-tube-clip.forge.js +15 -0
  81. package/examples/api/seated-bearing-stack.forge.js +30 -0
  82. package/examples/api/snap-latch-cover.forge.js +14 -0
  83. package/examples/api/static-assembly-connectors.forge.js +14 -16
  84. package/examples/api/thumb-screw-clamp.forge.js +15 -0
  85. package/package.json +20 -2
  86. package/dist/assets/EditorApp-DNH1TEz1.js +0 -12729
@@ -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, bH as initSolverWasm, bG as initKernel, S as Scene, bI as BoxGeometry, bd as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bJ as localAabbPlaneRelation, h as Vector2, bK as ShapeUtils, a0 as MathUtils, g as Vector3, G as Box3, aU as BufferAttribute, e as Color, aC as resolveForgeRenderStyle, b9 as getRenderStylePreset, ax as setParamOverrides, b6 as runScript, bL as Group, b3 as shapeToGeometry, b7 as MeshPhysicalMaterial, ba as AdditiveBlending, aH as LineBasicMaterial, b8 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bM as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bN as parseCameraCliSpec, bO as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bz as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bg as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bf as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bA as resolveJointAnimation, bB as resolveJointViewValues, R as Raycaster, bP as worldAuthorPlaneToLocal, a$ as SpotLight } from "../sectionPlaneMath-CykEnkvQ.js";
5
- import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-3DfsSASX.js";
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";
6
6
  const CAD_MATERIAL_PROPS = {
7
7
  color: 6003669,
8
8
  metalness: 0.05,
@@ -457,146 +457,13 @@ function computeMeshSectionCap(mesh, planeInput) {
457
457
  warnings: stitched.warnings.length > 0 ? stitched.warnings : void 0
458
458
  };
459
459
  }
460
- const DEFAULT_COLLISION_INSPECTION_OPTIONS = {
461
- minOverlapVolume: 0.1
462
- };
463
- function cloneVec3$1(value) {
464
- return [value[0], value[1], value[2]];
465
- }
466
- function isIdentityTransform(matrix) {
467
- if (!matrix) return true;
468
- const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
469
- return identity.every((value, index) => Math.abs(matrix[index] - value) <= 1e-12);
470
- }
471
- function transformPoint(matrix, point) {
472
- const [x, y, z] = point;
473
- return [
474
- matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
475
- matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
476
- matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
477
- ];
478
- }
479
- function transformBBox(min, max, matrix) {
480
- const corners = [
481
- [min[0], min[1], min[2]],
482
- [min[0], min[1], max[2]],
483
- [min[0], max[1], min[2]],
484
- [min[0], max[1], max[2]],
485
- [max[0], min[1], min[2]],
486
- [max[0], min[1], max[2]],
487
- [max[0], max[1], min[2]],
488
- [max[0], max[1], max[2]]
489
- ];
490
- const outMin = [Infinity, Infinity, Infinity];
491
- const outMax = [-Infinity, -Infinity, -Infinity];
492
- for (const corner of corners) {
493
- const transformed = transformPoint(matrix, corner);
494
- for (let axis = 0; axis < 3; axis += 1) {
495
- outMin[axis] = Math.min(outMin[axis], transformed[axis]);
496
- outMax[axis] = Math.max(outMax[axis], transformed[axis]);
497
- }
498
- }
499
- return { min: outMin, max: outMax };
500
- }
501
- function prepareEntry$1(entry) {
502
- if (isIdentityTransform(entry.transform)) {
503
- return {
504
- ...entry,
505
- min: cloneVec3$1(entry.min),
506
- max: cloneVec3$1(entry.max)
507
- };
508
- }
509
- const bbox = transformBBox(entry.min, entry.max, entry.transform);
510
- return {
511
- ...entry,
512
- shape: entry.shape.transform(entry.transform),
513
- min: bbox.min,
514
- max: bbox.max
515
- };
516
- }
517
- function bboxOverlaps(a, b) {
518
- return [0, 1, 2].every((axis) => a.min[axis] < b.max[axis] && a.max[axis] > b.min[axis]);
519
- }
520
- function collisionId(a, b) {
521
- return `${a.id}__${b.id}`;
522
- }
523
- function serializeCollisionFinding(finding) {
524
- return {
525
- index: finding.index,
526
- id: finding.id,
527
- sourceIndex: finding.sourceIndex,
528
- targetIndex: finding.targetIndex,
529
- sourceId: finding.sourceId,
530
- targetId: finding.targetId,
531
- sourceName: finding.sourceName,
532
- targetName: finding.targetName,
533
- overlapVolume: finding.overlapVolume
534
- };
535
- }
536
- function analyzeCollisionIntersections(entries, rawOptions = {}) {
537
- const options = {
538
- minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume
539
- };
540
- const warnings = [];
541
- const collisions = [];
542
- const preparedEntries = entries.map((entry) => prepareEntry$1(entry));
543
- for (let i = 0; i < preparedEntries.length; i += 1) {
544
- for (let j = i + 1; j < preparedEntries.length; j += 1) {
545
- const a = preparedEntries[i];
546
- const b = preparedEntries[j];
547
- if (!bboxOverlaps(a, b)) continue;
548
- try {
549
- const hit = a.shape.intersect(b.shape);
550
- if (hit.isEmpty()) continue;
551
- const overlapVolume = hit.volume();
552
- if (!Number.isFinite(overlapVolume) || overlapVolume <= options.minOverlapVolume) continue;
553
- collisions.push({
554
- index: collisions.length + 1,
555
- id: collisionId(a, b),
556
- sourceIndex: i,
557
- targetIndex: j,
558
- sourceId: a.id,
559
- targetId: b.id,
560
- sourceName: a.name,
561
- targetName: b.name,
562
- overlapVolume,
563
- shape: hit
564
- });
565
- } catch (err) {
566
- const message = err instanceof Error ? err.message : String(err);
567
- warnings.push(`Could not boolean-test ${a.name} against ${b.name}: ${message}`);
568
- }
569
- }
570
- }
571
- const objects = preparedEntries.map((entry, index) => ({
572
- index,
573
- id: entry.id,
574
- name: entry.name,
575
- groupName: entry.groupName,
576
- treePath: entry.treePath,
577
- mock: entry.mock === true,
578
- bbox: {
579
- min: cloneVec3$1(entry.min),
580
- max: cloneVec3$1(entry.max)
581
- }
582
- }));
583
- return {
584
- method: "boolean-intersection",
585
- options,
586
- objectCount: objects.length,
587
- collisionCount: collisions.length,
588
- objects,
589
- collisions,
590
- warnings
591
- };
592
- }
593
460
  const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
594
461
  contactTolerance: 0.05,
595
462
  minOverlapVolume: 0.1,
596
- exactGeometry: false
463
+ exactGeometry: true
597
464
  };
598
465
  const AXIS_NAMES = ["x", "y", "z"];
599
- class UnionFind {
466
+ let UnionFind$1 = class UnionFind {
600
467
  constructor(size) {
601
468
  __publicField(this, "parent");
602
469
  __publicField(this, "rank");
@@ -625,103 +492,42 @@ class UnionFind {
625
492
  this.parent[rootB] = rootA;
626
493
  this.rank[rootA] += 1;
627
494
  }
628
- }
495
+ };
629
496
  function cloneVec3(value) {
630
497
  return [value[0], value[1], value[2]];
631
498
  }
632
- function emptyBBox() {
499
+ function emptyBBox$1() {
633
500
  return {
634
501
  min: [Infinity, Infinity, Infinity],
635
502
  max: [-Infinity, -Infinity, -Infinity]
636
503
  };
637
504
  }
638
- function expandBBox(target, min, max) {
505
+ function expandBBox$1(target, min, max) {
639
506
  for (let axis = 0; axis < 3; axis += 1) {
640
507
  target.min[axis] = Math.min(target.min[axis], min[axis]);
641
508
  target.max[axis] = Math.max(target.max[axis], max[axis]);
642
509
  }
643
510
  }
644
- function intervalGap$1(aMin, aMax, bMin, bMax) {
645
- if (aMax < bMin) return bMin - aMax;
646
- if (bMax < aMin) return aMin - bMax;
647
- return 0;
648
- }
649
511
  function nearestBoundaryGap(a, b, axis) {
650
512
  return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
651
513
  }
652
- function bboxGaps(a, b) {
653
- return [
654
- intervalGap$1(a.min[0], a.max[0], b.min[0], b.max[0]),
655
- intervalGap$1(a.min[1], a.max[1], b.min[1], b.max[1]),
656
- intervalGap$1(a.min[2], a.max[2], b.min[2], b.max[2])
657
- ];
658
- }
659
- function maxGap(gaps) {
660
- return Math.max(gaps[0], gaps[1], gaps[2]);
661
- }
662
514
  function hasPositiveGap(gaps) {
663
515
  return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
664
516
  }
665
- function bboxInteriorOverlaps(a, b) {
666
- for (let axis = 0; axis < 3; axis += 1) {
667
- if (Math.min(a.max[axis], b.max[axis]) - Math.max(a.min[axis], b.min[axis]) <= 0) return false;
668
- }
669
- return true;
670
- }
671
- function bboxOverlapVolume(a, b) {
672
- let volume = 1;
673
- for (let axis = 0; axis < 3; axis += 1) {
674
- volume *= Math.max(0, Math.min(a.max[axis], b.max[axis]) - Math.max(a.min[axis], b.min[axis]));
675
- }
676
- return volume;
677
- }
678
- function estimateSweepPairCount(entries, axis, tolerance) {
679
- const ordered = entries.map((entry) => ({ min: entry.min[axis], max: entry.max[axis] })).sort((a, b) => a.min - b.min || a.max - b.max);
680
- const endValues = ordered.map((entry) => entry.max + tolerance).sort((a, b) => a - b);
681
- let expired = 0;
682
- let count = 0;
683
- for (let seen = 0; seen < ordered.length; seen += 1) {
684
- const currentMin = ordered[seen].min;
685
- while (expired < seen && endValues[expired] < currentMin) expired += 1;
686
- count += seen - expired;
687
- }
688
- return count;
689
- }
690
- function chooseSweepAxis(entries, tolerance) {
691
- let bestAxis = 0;
692
- let bestCount = estimateSweepPairCount(entries, bestAxis, tolerance);
693
- for (const axis of [1, 2]) {
694
- const count = estimateSweepPairCount(entries, axis, tolerance);
695
- if (count < bestCount) {
696
- bestAxis = axis;
697
- bestCount = count;
698
- }
699
- }
700
- return bestAxis;
701
- }
702
517
  function collectCandidatePairs(entries, tolerance) {
703
518
  if (entries.length < 2) return [];
704
- const axis = chooseSweepAxis(entries, tolerance);
705
- const ordered = entries.map((entry, index) => ({ entry, index })).sort((a, b) => a.entry.min[axis] - b.entry.min[axis] || a.entry.max[axis] - b.entry.max[axis] || a.index - b.index);
706
- let active = [];
707
- const pairs = [];
708
- for (const current of ordered) {
709
- active = active.filter((candidate) => candidate.entry.max[axis] + tolerance >= current.entry.min[axis]);
710
- for (const candidate of active) {
711
- const gaps = bboxGaps(candidate.entry, current.entry);
712
- if (maxGap(gaps) > tolerance) continue;
713
- const sourceIndex = Math.min(candidate.index, current.index);
714
- const targetIndex = Math.max(candidate.index, current.index);
715
- pairs.push({ sourceIndex, targetIndex, gaps });
716
- }
717
- active.push(current);
718
- }
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);
719
525
  pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
720
526
  return pairs;
721
527
  }
722
528
  function contactFromBBoxes(a, b, tolerance) {
723
- const gaps = bboxGaps(a, b);
724
- const largestGap = maxGap(gaps);
529
+ const gaps = aabbGaps(a, b);
530
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
725
531
  if (largestGap > tolerance) return { touching: false, gap: largestGap };
726
532
  const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
727
533
  if (separatedAxes.length > 0) {
@@ -768,21 +574,24 @@ function makeEdge(entries, sourceIndex, targetIndex, edge) {
768
574
  };
769
575
  }
770
576
  function analyzePhysicalConnectivity(entries, rawOptions = {}) {
577
+ const exactGeometry = rawOptions.exactGeometry ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.exactGeometry;
771
578
  const options = {
772
579
  contactTolerance: rawOptions.contactTolerance ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance,
773
580
  minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.minOverlapVolume,
774
- exactGeometry: rawOptions.exactGeometry ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.exactGeometry
581
+ exactGeometry,
582
+ mergeOverlappingBBoxes: rawOptions.mergeOverlappingBBoxes ?? !exactGeometry,
583
+ mergeTouchingBBoxes: rawOptions.mergeTouchingBBoxes ?? !exactGeometry
775
584
  };
776
585
  const warnings = [];
777
586
  const edges = [];
778
- const unionFind = new UnionFind(entries.length);
587
+ const unionFind = new UnionFind$1(entries.length);
779
588
  for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
780
589
  const i = pair.sourceIndex;
781
590
  const j = pair.targetIndex;
782
591
  const a = entries[i];
783
592
  const b = entries[j];
784
- const bboxOverlaps2 = !hasPositiveGap(pair.gaps) && bboxInteriorOverlaps(a, b);
785
- if (options.exactGeometry && bboxOverlaps2) {
593
+ const bboxOverlaps = !hasPositiveGap(pair.gaps) && aabbInteriorOverlaps(a, b);
594
+ if (options.exactGeometry && bboxOverlaps) {
786
595
  const overlap = intersectionVolume(a, b);
787
596
  if (overlap.warning) warnings.push(overlap.warning);
788
597
  if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
@@ -797,32 +606,21 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
797
606
  );
798
607
  continue;
799
608
  }
800
- if (overlap.volume != null && overlap.volume > 0) {
801
- unionFind.union(i, j);
802
- edges.push(
803
- makeEdge(entries, i, j, {
804
- kind: "touching",
805
- method: "boolean-intersection",
806
- gap: 0,
807
- overlapVolume: overlap.volume
808
- })
809
- );
810
- continue;
811
- }
609
+ continue;
812
610
  }
813
- if (bboxOverlaps2) {
611
+ if (bboxOverlaps && options.mergeOverlappingBBoxes) {
814
612
  unionFind.union(i, j);
815
613
  edges.push(
816
614
  makeEdge(entries, i, j, {
817
615
  kind: "overlap",
818
616
  method: "bbox-overlap",
819
617
  gap: 0,
820
- overlapVolume: bboxOverlapVolume(a, b)
618
+ overlapVolume: aabbOverlapVolume(a, b)
821
619
  })
822
620
  );
823
621
  } else {
824
622
  const contact = contactFromBBoxes(a, b, options.contactTolerance);
825
- if (!contact.touching) continue;
623
+ if (!contact.touching || !options.mergeTouchingBBoxes) continue;
826
624
  unionFind.union(i, j);
827
625
  edges.push(
828
626
  makeEdge(entries, i, j, {
@@ -861,7 +659,7 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
861
659
  objectNames: [],
862
660
  objectCount: 0,
863
661
  bodyCount: 0,
864
- bbox: emptyBBox()
662
+ bbox: emptyBBox$1()
865
663
  };
866
664
  componentByRoot.set(root, component);
867
665
  rootToComponentIndex.set(root, component.index);
@@ -873,7 +671,7 @@ function analyzePhysicalConnectivity(entries, rawOptions = {}) {
873
671
  component.objectNames.push(object.name);
874
672
  component.objectCount += 1;
875
673
  component.bodyCount += object.bodyCount;
876
- expandBBox(component.bbox, object.bbox.min, object.bbox.max);
674
+ expandBBox$1(component.bbox, object.bbox.min, object.bbox.max);
877
675
  }
878
676
  const components = [...componentByRoot.values()];
879
677
  return {
@@ -919,64 +717,38 @@ function defaultRootComponentIndex(components) {
919
717
  if (components.length === 0) return null;
920
718
  return components.reduce((best, component) => compareDefaultRoot(component, best) > 0 ? component : best, components[0]).index;
921
719
  }
922
- function buildGapEdges(components) {
923
- const edges = [];
924
- for (let i = 0; i < components.length; i += 1) {
925
- for (let j = i + 1; j < components.length; j += 1) {
926
- const source = components[i];
927
- const target = components[j];
928
- const gap = bboxGap(source, target);
929
- if (!Number.isFinite(gap.gap)) continue;
930
- edges.push({
931
- sourceComponentIndex: source.index,
932
- targetComponentIndex: target.index,
933
- sourceObjectNames: [...source.objectNames],
934
- targetObjectNames: [...target.objectNames],
935
- gap: gap.gap,
936
- axisGaps: gap.axisGaps
937
- });
938
- }
939
- }
940
- return edges;
941
- }
942
720
  function componentPositionByIndex(components) {
943
721
  return new Map(components.map((component, position) => [component.index, position]));
944
722
  }
945
- function computeNearestComponents(components, gapEdges) {
723
+ function computeNearestComponents(components) {
946
724
  const nearest = components.map(() => ({ nearestGap: null, nearestComponentIndex: null }));
947
- const positions = componentPositionByIndex(components);
948
- for (const edge of gapEdges) {
949
- const sourcePosition = positions.get(edge.sourceComponentIndex);
950
- const targetPosition = positions.get(edge.targetComponentIndex);
951
- if (sourcePosition == null || targetPosition == null) continue;
952
- const source = nearest[sourcePosition];
953
- if (source.nearestGap == null || edge.gap < source.nearestGap - EPSILON || Math.abs(edge.gap - source.nearestGap) <= EPSILON && edge.targetComponentIndex < (source.nearestComponentIndex ?? Infinity)) {
954
- source.nearestGap = edge.gap;
955
- source.nearestComponentIndex = edge.targetComponentIndex;
956
- }
957
- const target = nearest[targetPosition];
958
- if (target.nearestGap == null || edge.gap < target.nearestGap - EPSILON || Math.abs(edge.gap - target.nearestGap) <= EPSILON && edge.sourceComponentIndex < (target.nearestComponentIndex ?? Infinity)) {
959
- target.nearestGap = edge.gap;
960
- target.nearestComponentIndex = edge.sourceComponentIndex;
725
+ for (let sourcePosition = 0; sourcePosition < components.length; sourcePosition += 1) {
726
+ for (let targetPosition = sourcePosition + 1; targetPosition < components.length; targetPosition += 1) {
727
+ const sourceComponent = components[sourcePosition];
728
+ const targetComponent = components[targetPosition];
729
+ const edge = bboxGap(sourceComponent, targetComponent);
730
+ if (!Number.isFinite(edge.gap)) continue;
731
+ const source = nearest[sourcePosition];
732
+ if (source.nearestGap == null || edge.gap < source.nearestGap - EPSILON || Math.abs(edge.gap - source.nearestGap) <= EPSILON && targetComponent.index < (source.nearestComponentIndex ?? Infinity)) {
733
+ source.nearestGap = edge.gap;
734
+ source.nearestComponentIndex = targetComponent.index;
735
+ }
736
+ const target = nearest[targetPosition];
737
+ if (target.nearestGap == null || edge.gap < target.nearestGap - EPSILON || Math.abs(edge.gap - target.nearestGap) <= EPSILON && sourceComponent.index < (target.nearestComponentIndex ?? Infinity)) {
738
+ target.nearestGap = edge.gap;
739
+ target.nearestComponentIndex = sourceComponent.index;
740
+ }
961
741
  }
962
742
  }
963
743
  return nearest;
964
744
  }
965
- function computeRootDistances(components, gapEdges, rootComponentIndex) {
745
+ function computeRootDistances(components, rootComponentIndex) {
966
746
  if (rootComponentIndex == null) return [];
967
747
  const positions = componentPositionByIndex(components);
968
748
  const rootPosition = positions.get(rootComponentIndex);
969
749
  if (rootPosition == null) {
970
750
  throw new Error(`rootComponentIndex ${rootComponentIndex} does not match any physical component`);
971
751
  }
972
- const adjacency = components.map(() => []);
973
- for (const edge of gapEdges) {
974
- const sourcePosition = positions.get(edge.sourceComponentIndex);
975
- const targetPosition = positions.get(edge.targetComponentIndex);
976
- if (sourcePosition == null || targetPosition == null) continue;
977
- adjacency[sourcePosition].push({ to: targetPosition, gap: edge.gap });
978
- adjacency[targetPosition].push({ to: sourcePosition, gap: edge.gap });
979
- }
980
752
  const visited = components.map(() => false);
981
753
  const distances = components.map(() => Infinity);
982
754
  const parents = components.map(() => null);
@@ -992,13 +764,15 @@ function computeRootDistances(components, gapEdges, rootComponentIndex) {
992
764
  }
993
765
  if (current === -1 || !Number.isFinite(distances[current])) break;
994
766
  visited[current] = true;
995
- for (const edge of adjacency[current]) {
996
- if (visited[edge.to]) continue;
767
+ for (let targetPosition = 0; targetPosition < components.length; targetPosition += 1) {
768
+ if (visited[targetPosition] || targetPosition === current) continue;
769
+ const edge = bboxGap(components[current], components[targetPosition]);
770
+ if (!Number.isFinite(edge.gap)) continue;
997
771
  const nextDistance = distances[current] + edge.gap;
998
- if (nextDistance < distances[edge.to] - EPSILON || Math.abs(nextDistance - distances[edge.to]) <= EPSILON && components[current].index < (parents[edge.to] ?? Infinity)) {
999
- distances[edge.to] = nextDistance;
1000
- parents[edge.to] = components[current].index;
1001
- parentGaps[edge.to] = edge.gap;
772
+ if (nextDistance < distances[targetPosition] - EPSILON || Math.abs(nextDistance - distances[targetPosition]) <= EPSILON && components[current].index < (parents[targetPosition] ?? Infinity)) {
773
+ distances[targetPosition] = nextDistance;
774
+ parents[targetPosition] = components[current].index;
775
+ parentGaps[targetPosition] = edge.gap;
1002
776
  }
1003
777
  }
1004
778
  }
@@ -1008,12 +782,44 @@ function computeRootDistances(components, gapEdges, rootComponentIndex) {
1008
782
  parentGap: parentGaps[position]
1009
783
  }));
1010
784
  }
785
+ function makeGapEdge(source, target) {
786
+ const gap = bboxGap(source, target);
787
+ return {
788
+ sourceComponentIndex: source.index,
789
+ targetComponentIndex: target.index,
790
+ sourceObjectNames: [...source.objectNames],
791
+ targetObjectNames: [...target.objectNames],
792
+ gap: gap.gap,
793
+ axisGaps: gap.axisGaps
794
+ };
795
+ }
796
+ function compactGapEdges(components, nearest, rooted) {
797
+ const componentByIndex = new Map(components.map((component) => [component.index, component]));
798
+ const seen = /* @__PURE__ */ new Set();
799
+ const edges = [];
800
+ const add = (sourceIndex, targetIndex) => {
801
+ if (targetIndex == null || sourceIndex === targetIndex) return;
802
+ const source = componentByIndex.get(Math.min(sourceIndex, targetIndex));
803
+ const target = componentByIndex.get(Math.max(sourceIndex, targetIndex));
804
+ if (!source || !target) return;
805
+ const key = `${source.index}:${target.index}`;
806
+ if (seen.has(key)) return;
807
+ seen.add(key);
808
+ edges.push(makeGapEdge(source, target));
809
+ };
810
+ components.forEach((component, position) => {
811
+ var _a, _b;
812
+ add(component.index, ((_a = nearest[position]) == null ? void 0 : _a.nearestComponentIndex) ?? null);
813
+ add(component.index, ((_b = rooted[position]) == null ? void 0 : _b.parentComponentIndex) ?? null);
814
+ });
815
+ return edges.sort((a, b) => a.sourceComponentIndex - b.sourceComponentIndex || a.targetComponentIndex - b.targetComponentIndex);
816
+ }
1011
817
  function analyzeDistanceInspection(entries, rawOptions = {}) {
1012
818
  const connectivity = analyzePhysicalConnectivity(entries, rawOptions);
1013
819
  const rootComponentIndex = rawOptions.rootComponentIndex ?? defaultRootComponentIndex(connectivity.components);
1014
- const gapEdges = buildGapEdges(connectivity.components);
1015
- const nearest = computeNearestComponents(connectivity.components, gapEdges);
1016
- const rooted = computeRootDistances(connectivity.components, gapEdges, rootComponentIndex);
820
+ const nearest = computeNearestComponents(connectivity.components);
821
+ const rooted = computeRootDistances(connectivity.components, rootComponentIndex);
822
+ const gapEdges = compactGapEdges(connectivity.components, nearest, rooted);
1017
823
  const componentByIndex = /* @__PURE__ */ new Map();
1018
824
  const components = connectivity.components.map((component, position) => {
1019
825
  var _a, _b;
@@ -1059,6 +865,7 @@ function analyzeDistanceInspection(entries, rawOptions = {}) {
1059
865
  componentCount: connectivity.componentCount,
1060
866
  rootComponentIndex,
1061
867
  maxRootDistance,
868
+ gapEdgeCount: connectivity.components.length * (connectivity.components.length - 1) / 2,
1062
869
  objects,
1063
870
  components,
1064
871
  gapEdges,
@@ -1334,10 +1141,210 @@ function summarizeThicknessSamples(samples, options) {
1334
1141
  unresolvedAreaPercent: percent(unresolvedArea, totalArea)
1335
1142
  };
1336
1143
  }
1144
+ const MIN_TRIANGLE_AREA = 1e-12;
1145
+ const R2_ALPHA = 0.7548776662466927;
1146
+ const R2_BETA = 0.5698402909980532;
1147
+ function readSurfaceTriangles(position) {
1148
+ const triangles = [];
1149
+ const a = new Vector3();
1150
+ const b = new Vector3();
1151
+ const c = new Vector3();
1152
+ const ac = new Vector3();
1153
+ const normal = new Vector3();
1154
+ const triangleCount = Math.floor(position.count / 3);
1155
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1156
+ const offset = tri * 3;
1157
+ a.fromBufferAttribute(position, offset);
1158
+ b.fromBufferAttribute(position, offset + 1);
1159
+ c.fromBufferAttribute(position, offset + 2);
1160
+ normal.subVectors(b, a).cross(ac.subVectors(c, a));
1161
+ const areaTwice = normal.length();
1162
+ if (areaTwice <= MIN_TRIANGLE_AREA) continue;
1163
+ triangles.push({
1164
+ index: tri,
1165
+ a: a.clone(),
1166
+ b: b.clone(),
1167
+ c: c.clone(),
1168
+ normal: normal.clone().multiplyScalar(1 / areaTwice),
1169
+ area: areaTwice * 0.5
1170
+ });
1171
+ }
1172
+ return triangles;
1173
+ }
1174
+ function allocateAreaSampleCounts(triangles, maxSamples) {
1175
+ const sampleBudget = Math.max(1, Math.floor(maxSamples));
1176
+ const counts = new Array(triangles.length).fill(0);
1177
+ if (triangles.length === 0) return counts;
1178
+ const totalArea = triangles.reduce((sum, triangle) => sum + triangle.area, 0);
1179
+ if (!(totalArea > 0)) return counts;
1180
+ const remainders = triangles.map((triangle, index) => {
1181
+ const quota = triangle.area / totalArea * sampleBudget;
1182
+ const whole = Math.floor(quota);
1183
+ counts[index] += whole;
1184
+ return { index, fraction: quota - whole, area: triangle.area };
1185
+ });
1186
+ let left = sampleBudget - counts.reduce((sum, count) => sum + count, 0);
1187
+ remainders.sort((a, b) => b.fraction - a.fraction || b.area - a.area || a.index - b.index);
1188
+ for (const entry of remainders) {
1189
+ if (left <= 0) break;
1190
+ counts[entry.index] += 1;
1191
+ left -= 1;
1192
+ }
1193
+ return counts;
1194
+ }
1195
+ function sampleSurfaceTriangles(triangles, maxSamples) {
1196
+ const counts = allocateAreaSampleCounts(triangles, maxSamples);
1197
+ const samples = [];
1198
+ const position = new Vector3();
1199
+ let sampleIndex = 0;
1200
+ triangles.forEach((triangle, triangleListIndex) => {
1201
+ const count = counts[triangleListIndex];
1202
+ if (count <= 0) return;
1203
+ for (let localIndex = 0; localIndex < count; localIndex += 1) {
1204
+ const barycentric = triangleBarycentricSample(triangle.index, localIndex, sampleIndex);
1205
+ position.copy(triangle.a).multiplyScalar(barycentric[0]).addScaledVector(triangle.b, barycentric[1]).addScaledVector(triangle.c, barycentric[2]);
1206
+ samples.push({
1207
+ triangle,
1208
+ position: position.clone(),
1209
+ normal: triangle.normal.clone(),
1210
+ area: triangle.area / count,
1211
+ barycentric,
1212
+ sampleIndex,
1213
+ triangleSampleIndex: localIndex,
1214
+ triangleSampleCount: count
1215
+ });
1216
+ sampleIndex += 1;
1217
+ }
1218
+ });
1219
+ return samples;
1220
+ }
1221
+ function totalSurfaceArea(triangles) {
1222
+ return triangles.reduce((sum, triangle) => sum + triangle.area, 0);
1223
+ }
1224
+ function triangleBarycentricSample(triangleIndex, triangleSampleIndex, sampleIndex) {
1225
+ const seedA = hash01((triangleIndex + 1) * 1013);
1226
+ const seedB = hash01((triangleIndex + 1) * 9176);
1227
+ const u = clampUnit(fract(seedA + (sampleIndex + 1) * R2_ALPHA));
1228
+ const v = clampUnit(fract(seedB + (triangleSampleIndex + 1) * R2_BETA));
1229
+ const root = Math.sqrt(u);
1230
+ return [1 - root, root * (1 - v), root * v];
1231
+ }
1232
+ function hash01(value) {
1233
+ return fract(Math.sin(value * 12.9898) * 43758.5453123);
1234
+ }
1235
+ function fract(value) {
1236
+ return value - Math.floor(value);
1237
+ }
1238
+ function clampUnit(value) {
1239
+ return Math.min(1 - 1e-9, Math.max(1e-9, value));
1240
+ }
1241
+ function cloneGeometryForFaceColors(geometry) {
1242
+ return geometry.index ? geometry.toNonIndexed() : geometry.clone();
1243
+ }
1244
+ function geometryMaxDimension(geometry) {
1245
+ geometry.computeBoundingBox();
1246
+ const box = geometry.boundingBox;
1247
+ if (!box) return 1;
1248
+ const size = new Vector3();
1249
+ box.getSize(size);
1250
+ return Math.max(1, size.x, size.y, size.z);
1251
+ }
1252
+ function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
1253
+ const origin = point.clone().addScaledVector(direction, epsilon);
1254
+ raycaster.set(origin, direction);
1255
+ raycaster.near = epsilon;
1256
+ raycaster.far = far;
1257
+ const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
1258
+ return hit ? hit.distance + epsilon : null;
1259
+ }
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);
1263
+ if (forward == null) return backward;
1264
+ if (backward == null) return forward;
1265
+ return Math.min(forward, backward);
1266
+ }
1267
+ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
1268
+ const options = resolveThicknessInspectionOptions(rawOptions);
1269
+ const geometry = cloneGeometryForFaceColors(sourceGeometry);
1270
+ const position = geometry.getAttribute("position");
1271
+ if (!position || position.count < 3) {
1272
+ return {
1273
+ geometry,
1274
+ samples: [],
1275
+ pointSamples: [],
1276
+ triangleCount: 0,
1277
+ sampledTriangleCount: 0,
1278
+ sampleStride: 1,
1279
+ warnings: ["No triangle geometry."]
1280
+ };
1281
+ }
1282
+ const triangleCount = Math.floor(position.count / 3);
1283
+ const surfaceTriangles = readSurfaceTriangles(position);
1284
+ const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
1285
+ const maxDim = geometryMaxDimension(geometry);
1286
+ const epsilon = Math.max(1e-4, maxDim * 1e-6);
1287
+ const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1288
+ const colors = new Float32Array(position.count * 3);
1289
+ const triangleThicknessValues = new Array(triangleCount).fill(void 0);
1290
+ const samples = [];
1291
+ const pointSamples = [];
1292
+ const warnings = [];
1293
+ const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1294
+ const rayMesh = new Mesh(geometry, rayMaterial);
1295
+ const raycaster = new Raycaster();
1296
+ if (surfaceTriangles.length === 0) {
1297
+ warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
1298
+ } else if (surfaceSamples.length < surfaceTriangles.length) {
1299
+ warnings.push(
1300
+ `Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
1301
+ );
1302
+ }
1303
+ const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1304
+ for (const sample of surfaceSamples) {
1305
+ sampledTriangleIndexes.add(sample.triangle.index);
1306
+ const thickness = triangleThickness(raycaster, rayMesh, sample.position, sample.normal, epsilon, far);
1307
+ samples.push({ thickness, area: sample.area });
1308
+ const previous = triangleThicknessValues[sample.triangle.index];
1309
+ if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
1310
+ triangleThicknessValues[sample.triangle.index] = thickness;
1311
+ }
1312
+ pointSamples.push({
1313
+ position: [sample.position.x, sample.position.y, sample.position.z],
1314
+ normal: [sample.normal.x, sample.normal.y, sample.normal.z],
1315
+ value: thickness,
1316
+ className: thicknessClass(thickness, options),
1317
+ color: thicknessColor(thickness, options),
1318
+ area: sample.area
1319
+ });
1320
+ }
1321
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1322
+ const color = thicknessColor(triangleThicknessValues[tri], options);
1323
+ const offset = tri * 3;
1324
+ for (let vertex = 0; vertex < 3; vertex += 1) {
1325
+ const colorOffset = (offset + vertex) * 3;
1326
+ colors[colorOffset] = color[0] / 255;
1327
+ colors[colorOffset + 1] = color[1] / 255;
1328
+ colors[colorOffset + 2] = color[2] / 255;
1329
+ }
1330
+ }
1331
+ geometry.setAttribute("color", new BufferAttribute(colors, 3));
1332
+ rayMaterial.dispose();
1333
+ return {
1334
+ geometry,
1335
+ samples,
1336
+ pointSamples,
1337
+ triangleCount,
1338
+ sampledTriangleCount: sampledTriangleIndexes.size,
1339
+ sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
1340
+ warnings
1341
+ };
1342
+ }
1337
1343
  const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1338
1344
  smoothAngleDeg: 5,
1339
1345
  sharpAngleDeg: 30,
1340
- harshAngleDeg: 90
1346
+ harshAngleDeg: 90,
1347
+ maxSamplesPerObject: 5e3
1341
1348
  };
1342
1349
  const ROUGHNESS_COLORS = {
1343
1350
  smooth: [62, 72, 84],
@@ -1349,7 +1356,8 @@ function resolveRoughnessInspectionOptions(raw = {}) {
1349
1356
  const options = {
1350
1357
  smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
1351
1358
  sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
1352
- harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg
1359
+ harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg,
1360
+ maxSamplesPerObject: raw.maxSamplesPerObject ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.maxSamplesPerObject
1353
1361
  };
1354
1362
  if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
1355
1363
  throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
@@ -1360,7 +1368,13 @@ function resolveRoughnessInspectionOptions(raw = {}) {
1360
1368
  if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
1361
1369
  throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
1362
1370
  }
1363
- return options;
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
+ };
1364
1378
  }
1365
1379
  function roughnessClassForAngle(angleDeg, options) {
1366
1380
  if (angleDeg >= options.harshAngleDeg) return "harsh";
@@ -1473,11 +1487,12 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1473
1487
  const position = geometry.getAttribute("position");
1474
1488
  const warnings = [];
1475
1489
  if (!position || position.count < 3) {
1476
- return { geometry, summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
1490
+ return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
1477
1491
  }
1478
1492
  const triangleCount = Math.floor(position.count / 3);
1479
1493
  const normals = new Array(triangleCount);
1480
1494
  const triangles = new Array(triangleCount);
1495
+ const triangleEdgeAngles = Array.from({ length: triangleCount }, () => [0, 0, 0]);
1481
1496
  const edges = /* @__PURE__ */ new Map();
1482
1497
  const colors = new Float32Array(position.count * 3);
1483
1498
  const scores = new Float32Array(position.count);
@@ -1497,18 +1512,19 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1497
1512
  const areaTwice = normal.length();
1498
1513
  triangles[tri] = { area: areaTwice * 0.5, maxAngleDeg: 0 };
1499
1514
  normals[tri] = areaTwice > 1e-12 ? normal.multiplyScalar(1 / areaTwice).clone() : new Vector3(0, 0, 1);
1500
- const keys = [vertexKey(a, snap), vertexKey(b, snap), vertexKey(c, snap)];
1515
+ const keys = [vertexKey$1(a, snap), vertexKey$1(b, snap), vertexKey$1(c, snap)];
1501
1516
  for (let edge = 0; edge < 3; edge += 1) {
1502
1517
  const key = edgeKey(keys[edge], keys[(edge + 1) % 3]);
1503
1518
  let record = edges.get(key);
1504
1519
  if (!record) {
1505
- record = { triangles: [] };
1520
+ record = { triangles: [], triangleEdges: [] };
1506
1521
  edges.set(key, record);
1507
1522
  }
1508
1523
  record.triangles.push(tri);
1524
+ record.triangleEdges.push({ triangle: tri, edge });
1509
1525
  }
1510
1526
  }
1511
- const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals);
1527
+ const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles);
1512
1528
  if (boundaryEdgeCount > 0) warnings.push(`${boundaryEdgeCount} boundary edge(s) were treated as harsh roughness.`);
1513
1529
  if (nonManifoldEdgeCount > 0) warnings.push(`${nonManifoldEdgeCount} non-manifold edge(s) were treated as harsh roughness.`);
1514
1530
  for (let tri = 0; tri < triangleCount; tri += 1) {
@@ -1524,36 +1540,57 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1524
1540
  scores[offset + vertex] = score;
1525
1541
  }
1526
1542
  }
1543
+ const surfaceTriangles = readSurfaceTriangles(position);
1544
+ const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
1545
+ const edgeInfluenceRadius = roughnessEdgeInfluenceRadius(surfaceTriangles, surfaceSamples.length);
1546
+ const pointSamples = surfaceSamples.map((sample) => {
1547
+ const angleDeg = roughnessAngleAtSample(
1548
+ sample.position,
1549
+ sample.triangle,
1550
+ triangleEdgeAngles[sample.triangle.index],
1551
+ edgeInfluenceRadius
1552
+ );
1553
+ const color = roughnessColorForAngle(angleDeg, options);
1554
+ return {
1555
+ position: [sample.position.x, sample.position.y, sample.position.z],
1556
+ normal: [sample.normal.x, sample.normal.y, sample.normal.z],
1557
+ value: angleDeg,
1558
+ className: roughnessClassForAngle(angleDeg, options),
1559
+ color,
1560
+ area: sample.area
1561
+ };
1562
+ });
1527
1563
  geometry.setAttribute("color", new BufferAttribute(colors, 3));
1528
1564
  geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
1529
1565
  geometry.computeBoundingBox();
1530
1566
  return {
1531
1567
  geometry,
1568
+ pointSamples,
1532
1569
  summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
1533
1570
  warnings
1534
1571
  };
1535
1572
  }
1536
- function markTriangleRoughness(edges, triangles, normals) {
1573
+ function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
1537
1574
  const edgeAngles = [];
1538
1575
  let boundaryEdgeCount = 0;
1539
1576
  let nonManifoldEdgeCount = 0;
1540
1577
  for (const edge of edges.values()) {
1541
1578
  if (edge.triangles.length === 1) {
1542
1579
  boundaryEdgeCount += 1;
1543
- markTriangles(edge.triangles, triangles, 180);
1580
+ markEdge(edge, triangles, triangleEdgeAngles, 180);
1544
1581
  edgeAngles.push(180);
1545
1582
  continue;
1546
1583
  }
1547
1584
  if (edge.triangles.length > 2) {
1548
1585
  nonManifoldEdgeCount += 1;
1549
- markTriangles(edge.triangles, triangles, 180);
1586
+ markEdge(edge, triangles, triangleEdgeAngles, 180);
1550
1587
  edgeAngles.push(180);
1551
1588
  continue;
1552
1589
  }
1553
1590
  const [first, second] = edge.triangles;
1554
1591
  const dot = MathUtils.clamp(normals[first].dot(normals[second]), -1, 1);
1555
1592
  const angleDeg = Math.acos(dot) * DEG_PER_RAD;
1556
- markTriangles(edge.triangles, triangles, angleDeg);
1593
+ markEdge(edge, triangles, triangleEdgeAngles, angleDeg);
1557
1594
  edgeAngles.push(angleDeg);
1558
1595
  }
1559
1596
  return { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles };
@@ -1561,17 +1598,218 @@ function markTriangleRoughness(edges, triangles, normals) {
1561
1598
  function readVertex(position, index, target) {
1562
1599
  target.set(position.getX(index), position.getY(index), position.getZ(index));
1563
1600
  }
1564
- function vertexKey(point, snap) {
1601
+ function vertexKey$1(point, snap) {
1565
1602
  return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
1566
1603
  }
1567
1604
  function edgeKey(a, b) {
1568
1605
  return a < b ? `${a}|${b}` : `${b}|${a}`;
1569
1606
  }
1570
- function markTriangles(indices, triangles, angleDeg) {
1571
- for (const index of indices) {
1572
- triangles[index].maxAngleDeg = Math.max(triangles[index].maxAngleDeg, angleDeg);
1607
+ function markEdge(edge, triangles, triangleEdgeAngles, angleDeg) {
1608
+ for (const { triangle, edge: edgeIndex } of edge.triangleEdges) {
1609
+ triangles[triangle].maxAngleDeg = Math.max(triangles[triangle].maxAngleDeg, angleDeg);
1610
+ triangleEdgeAngles[triangle][edgeIndex] = Math.max(triangleEdgeAngles[triangle][edgeIndex], angleDeg);
1573
1611
  }
1574
1612
  }
1613
+ function roughnessEdgeInfluenceRadius(triangles, sampleCount) {
1614
+ if (triangles.length === 0 || sampleCount <= 0) return 0;
1615
+ return Math.sqrt(totalSurfaceArea(triangles) / sampleCount);
1616
+ }
1617
+ function roughnessAngleAtSample(point, triangle, edgeAngles, influenceRadius) {
1618
+ let angle = 0;
1619
+ for (let edge = 0; edge < 3; edge += 1) {
1620
+ const edgeAngle = edgeAngles[edge] ?? 0;
1621
+ if (edgeAngle <= 0) continue;
1622
+ const distance = pointDistanceToTriangleEdge(point, triangle, edge);
1623
+ if (distance <= influenceRadius) angle = Math.max(angle, edgeAngle);
1624
+ }
1625
+ return angle;
1626
+ }
1627
+ function pointDistanceToTriangleEdge(point, triangle, edge) {
1628
+ if (edge === 0) return pointSegmentDistance(point, triangle.a, triangle.b);
1629
+ if (edge === 1) return pointSegmentDistance(point, triangle.b, triangle.c);
1630
+ return pointSegmentDistance(point, triangle.c, triangle.a);
1631
+ }
1632
+ function pointSegmentDistance(point, start, end) {
1633
+ const segment = end.clone().sub(start);
1634
+ const lengthSq = segment.lengthSq();
1635
+ if (lengthSq <= 1e-18) return point.distanceTo(start);
1636
+ const t = MathUtils.clamp(point.clone().sub(start).dot(segment) / lengthSq, 0, 1);
1637
+ return point.distanceTo(segment.multiplyScalar(t).add(start));
1638
+ }
1639
+ const DEFAULT_VERTEX_TOLERANCE = 1e-5;
1640
+ class UnionFind2 {
1641
+ constructor(size) {
1642
+ __publicField(this, "parent");
1643
+ __publicField(this, "rank");
1644
+ this.parent = Array.from({ length: size }, (_, index) => index);
1645
+ this.rank = Array.from({ length: size }, () => 0);
1646
+ }
1647
+ find(value) {
1648
+ const parent = this.parent[value];
1649
+ if (parent === value) return value;
1650
+ const root = this.find(parent);
1651
+ this.parent[value] = root;
1652
+ return root;
1653
+ }
1654
+ union(a, b) {
1655
+ const rootA = this.find(a);
1656
+ const rootB = this.find(b);
1657
+ if (rootA === rootB) return;
1658
+ if (this.rank[rootA] < this.rank[rootB]) {
1659
+ this.parent[rootA] = rootB;
1660
+ return;
1661
+ }
1662
+ if (this.rank[rootA] > this.rank[rootB]) {
1663
+ this.parent[rootB] = rootA;
1664
+ return;
1665
+ }
1666
+ this.parent[rootB] = rootA;
1667
+ this.rank[rootA] += 1;
1668
+ }
1669
+ }
1670
+ function emptyBBox() {
1671
+ return {
1672
+ min: [Infinity, Infinity, Infinity],
1673
+ max: [-Infinity, -Infinity, -Infinity]
1674
+ };
1675
+ }
1676
+ function expandBBox(bbox, x, y, z) {
1677
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return;
1678
+ bbox.min[0] = Math.min(bbox.min[0], x);
1679
+ bbox.min[1] = Math.min(bbox.min[1], y);
1680
+ bbox.min[2] = Math.min(bbox.min[2], z);
1681
+ bbox.max[0] = Math.max(bbox.max[0], x);
1682
+ bbox.max[1] = Math.max(bbox.max[1], y);
1683
+ bbox.max[2] = Math.max(bbox.max[2], z);
1684
+ }
1685
+ function vertexKey(positions, vertexIndex, tolerance) {
1686
+ const base = vertexIndex * 3;
1687
+ const x = positions[base];
1688
+ const y = positions[base + 1];
1689
+ const z = positions[base + 2];
1690
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return null;
1691
+ return `${Math.round(x / tolerance)},${Math.round(y / tolerance)},${Math.round(z / tolerance)}`;
1692
+ }
1693
+ function analyzeMeshConnectedComponents(positions, vertexTolerance = DEFAULT_VERTEX_TOLERANCE) {
1694
+ const vertexCount = Math.floor(positions.length / 3);
1695
+ const triangleCount = Math.floor(vertexCount / 3);
1696
+ const vertexComponentIndexes = new Int32Array(vertexCount);
1697
+ if (triangleCount === 0) return { components: [], vertexComponentIndexes };
1698
+ const tolerance = Number.isFinite(vertexTolerance) && vertexTolerance > 0 ? vertexTolerance : DEFAULT_VERTEX_TOLERANCE;
1699
+ const unionFind = new UnionFind2(triangleCount);
1700
+ const vertexOwnerByKey = /* @__PURE__ */ new Map();
1701
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
1702
+ for (let corner = 0; corner < 3; corner += 1) {
1703
+ const key = vertexKey(positions, triangleIndex * 3 + corner, tolerance);
1704
+ if (key == null) continue;
1705
+ const owner = vertexOwnerByKey.get(key);
1706
+ if (owner === void 0) {
1707
+ vertexOwnerByKey.set(key, triangleIndex);
1708
+ } else {
1709
+ unionFind.union(triangleIndex, owner);
1710
+ }
1711
+ }
1712
+ }
1713
+ const componentIndexByRoot = /* @__PURE__ */ new Map();
1714
+ const components = [];
1715
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
1716
+ const root = unionFind.find(triangleIndex);
1717
+ let componentIndex = componentIndexByRoot.get(root);
1718
+ if (componentIndex === void 0) {
1719
+ componentIndex = components.length;
1720
+ componentIndexByRoot.set(root, componentIndex);
1721
+ components.push({
1722
+ index: componentIndex,
1723
+ triangleIndexes: [],
1724
+ vertexCount: 0,
1725
+ bbox: emptyBBox()
1726
+ });
1727
+ }
1728
+ const component = components[componentIndex];
1729
+ component.triangleIndexes.push(triangleIndex);
1730
+ component.vertexCount += 3;
1731
+ for (let corner = 0; corner < 3; corner += 1) {
1732
+ const vertexIndex = triangleIndex * 3 + corner;
1733
+ const base = vertexIndex * 3;
1734
+ vertexComponentIndexes[vertexIndex] = componentIndex;
1735
+ expandBBox(component.bbox, positions[base], positions[base + 1], positions[base + 2]);
1736
+ }
1737
+ }
1738
+ return { components, vertexComponentIndexes };
1739
+ }
1740
+ function isFiniteComponent(component) {
1741
+ return component.bbox.min.every(Number.isFinite) && component.bbox.max.every(Number.isFinite);
1742
+ }
1743
+ function objectConnectivityEntry(object) {
1744
+ return {
1745
+ id: object.id,
1746
+ name: object.name,
1747
+ shape: object.shape,
1748
+ min: object.min,
1749
+ max: object.max,
1750
+ groupName: object.groupName,
1751
+ treePath: object.treePath,
1752
+ mock: object.mock,
1753
+ bodyCount: 1
1754
+ };
1755
+ }
1756
+ function buildMeshBodyConnectivityInput(objects, options) {
1757
+ const entries = [];
1758
+ const bodyIdsByObjectId = /* @__PURE__ */ new Map();
1759
+ const vertexComponentIndexesByObjectId = /* @__PURE__ */ new Map();
1760
+ for (const object of objects) {
1761
+ if (!object.positions || object.positions.length < 9) {
1762
+ entries.push(objectConnectivityEntry(object));
1763
+ continue;
1764
+ }
1765
+ const meshComponents = analyzeMeshConnectedComponents(object.positions);
1766
+ const finiteComponents = meshComponents.components.filter(isFiniteComponent);
1767
+ if (finiteComponents.length <= 1) {
1768
+ entries.push(objectConnectivityEntry(object));
1769
+ continue;
1770
+ }
1771
+ const bodyIds = Array.from({ length: meshComponents.components.length }, (_, index) => `${object.id}:body:${index + 1}`);
1772
+ for (const component of finiteComponents) {
1773
+ entries.push({
1774
+ id: bodyIds[component.index],
1775
+ name: `${object.name} body ${component.index + 1}`,
1776
+ shape: options.bodyShape(object, component),
1777
+ min: component.bbox.min,
1778
+ max: component.bbox.max,
1779
+ groupName: object.groupName,
1780
+ treePath: object.treePath,
1781
+ mock: object.mock,
1782
+ bodyCount: 1
1783
+ });
1784
+ }
1785
+ bodyIdsByObjectId.set(object.id, bodyIds);
1786
+ vertexComponentIndexesByObjectId.set(object.id, meshComponents.vertexComponentIndexes);
1787
+ }
1788
+ return { entries, bodyIdsByObjectId, vertexComponentIndexesByObjectId };
1789
+ }
1790
+ function meshVertexColorBuffersFor(input, colorForEntryId) {
1791
+ const out = /* @__PURE__ */ new Map();
1792
+ const colorByEntryId = /* @__PURE__ */ new Map();
1793
+ for (const [objectId, vertexComponentIndexes] of input.vertexComponentIndexesByObjectId) {
1794
+ const bodyIds = input.bodyIdsByObjectId.get(objectId);
1795
+ if (!bodyIds) continue;
1796
+ const colors = new Float32Array(vertexComponentIndexes.length * 3);
1797
+ for (let vertexIndex = 0; vertexIndex < vertexComponentIndexes.length; vertexIndex += 1) {
1798
+ const bodyId = bodyIds[vertexComponentIndexes[vertexIndex]];
1799
+ let color = colorByEntryId.get(bodyId);
1800
+ if (!color) {
1801
+ color = colorForEntryId(bodyId);
1802
+ colorByEntryId.set(bodyId, color);
1803
+ }
1804
+ const offset = vertexIndex * 3;
1805
+ colors[offset] = color[0];
1806
+ colors[offset + 1] = color[1];
1807
+ colors[offset + 2] = color[2];
1808
+ }
1809
+ out.set(objectId, colors);
1810
+ }
1811
+ return out;
1812
+ }
1575
1813
  const canvas = document.getElementById("canvas");
1576
1814
  const exportCanvas = document.createElement("canvas");
1577
1815
  const exportCtx = exportCanvas.getContext("2d");
@@ -1605,6 +1843,14 @@ function createCaptureDebugLogger(enabled) {
1605
1843
  console.info(`[forge-capture:debug] +${sinceStart}ms Δ${delta}ms ${phase}${detailText}`);
1606
1844
  };
1607
1845
  }
1846
+ class EmptyInspectionShape {
1847
+ intersect() {
1848
+ return {
1849
+ isEmpty: () => true,
1850
+ volume: () => 0
1851
+ };
1852
+ }
1853
+ }
1608
1854
  const COLLISION_SOURCE_OPACITY = 0.22;
1609
1855
  const COLLISION_SOURCE_COLOR = [180, 200, 220];
1610
1856
  const COLLISION_HIGHLIGHT_COLOR = [255, 68, 16];
@@ -1648,6 +1894,31 @@ const MASK_PALETTE = [
1648
1894
  [0, 0, 128],
1649
1895
  [128, 128, 128]
1650
1896
  ];
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
+ }
1907
+ }
1908
+ function meshConnectivitySource(entry) {
1909
+ const bb = entry.shape.boundingBox();
1910
+ return {
1911
+ id: entry.source.id,
1912
+ name: entry.source.name,
1913
+ shape: entry.shape,
1914
+ min: [bb.min[0], bb.min[1], bb.min[2]],
1915
+ max: [bb.max[0], bb.max[1], bb.max[2]],
1916
+ groupName: entry.source.groupName,
1917
+ treePath: entry.source.treePath,
1918
+ mock: entry.source.mock,
1919
+ positions: cloneShapePositions(entry.shape)
1920
+ };
1921
+ }
1651
1922
  function summarizeSceneGeometry(entries) {
1652
1923
  const min = [Infinity, Infinity, Infinity];
1653
1924
  const max = [-Infinity, -Infinity, -Infinity];
@@ -2021,91 +2292,16 @@ function distanceColorForRootDistance(distance, maxDistance) {
2021
2292
  if (t <= 0.5) return lerpColor(DISTANCE_NEAR_COLOR, DISTANCE_MID_COLOR, t * 2);
2022
2293
  return lerpColor(DISTANCE_MID_COLOR, DISTANCE_FAR_COLOR, (t - 0.5) * 2);
2023
2294
  }
2024
- function cloneGeometryForFaceColors(geometry) {
2025
- return geometry.index ? geometry.toNonIndexed() : geometry.clone();
2026
- }
2027
- function geometryMaxDimension(geometry) {
2028
- geometry.computeBoundingBox();
2029
- const box = geometry.boundingBox;
2030
- if (!box) return 1;
2031
- const size = new Vector3();
2032
- box.getSize(size);
2033
- return Math.max(1, size.x, size.y, size.z);
2034
- }
2035
- function firstOppositeSurfaceDistance(raycaster, mesh, point, direction, epsilon, far) {
2036
- const origin = point.clone().addScaledVector(direction, epsilon);
2037
- raycaster.set(origin, direction);
2038
- raycaster.near = epsilon;
2039
- raycaster.far = far;
2040
- const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon);
2041
- return hit ? hit.distance + epsilon : null;
2042
- }
2043
- function triangleThickness(raycaster, mesh, centroid, normal, epsilon, far) {
2044
- const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon, far);
2045
- const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon, far);
2046
- if (forward == null) return backward;
2047
- if (backward == null) return forward;
2048
- return Math.min(forward, backward);
2295
+ function rgbFloats(color) {
2296
+ return [color[0] / 255, color[1] / 255, color[2] / 255];
2049
2297
  }
2050
- function analyzeThicknessGeometry(sourceGeometry, options) {
2051
- const geometry = cloneGeometryForFaceColors(sourceGeometry);
2052
- const position = geometry.getAttribute("position");
2053
- if (!position || position.count < 3) {
2054
- return { geometry, samples: [], triangleCount: 0, sampledTriangleCount: 0, sampleStride: 1, warnings: ["No triangle geometry."] };
2055
- }
2056
- const triangleCount = Math.floor(position.count / 3);
2057
- const sampleStride = Math.max(1, Math.ceil(triangleCount / options.maxSamplesPerObject));
2058
- const maxDim = geometryMaxDimension(geometry);
2059
- const epsilon = Math.max(1e-4, maxDim * 1e-6);
2060
- const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
2061
- const colors = new Float32Array(position.count * 3);
2062
- const samples = [];
2063
- const warnings = [];
2064
- const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
2065
- const rayMesh = new Mesh(geometry, rayMaterial);
2066
- const raycaster = new Raycaster();
2067
- if (sampleStride > 1) {
2068
- warnings.push(`Triangle sampling stride ${sampleStride}; increase --thickness-samples for denser analysis.`);
2069
- }
2070
- const a = new Vector3();
2071
- const b = new Vector3();
2072
- const c = new Vector3();
2073
- const normal = new Vector3();
2074
- const centroid = new Vector3();
2075
- let sampledTriangleCount = 0;
2076
- let lastThickness = null;
2077
- for (let tri = 0; tri < triangleCount; tri += 1) {
2078
- const offset = tri * 3;
2079
- a.fromBufferAttribute(position, offset);
2080
- b.fromBufferAttribute(position, offset + 1);
2081
- c.fromBufferAttribute(position, offset + 2);
2082
- normal.subVectors(b, a).cross(new Vector3().subVectors(c, a));
2083
- const areaTwice = normal.length();
2084
- const area = areaTwice * 0.5;
2085
- let thickness = lastThickness;
2086
- if (tri % sampleStride === 0) {
2087
- sampledTriangleCount += 1;
2088
- if (areaTwice <= 1e-12) {
2089
- thickness = null;
2090
- } else {
2091
- normal.multiplyScalar(1 / areaTwice);
2092
- centroid.copy(a).add(b).add(c).multiplyScalar(1 / 3);
2093
- thickness = triangleThickness(raycaster, rayMesh, centroid, normal, epsilon, far);
2094
- }
2095
- lastThickness = thickness;
2096
- samples.push({ thickness, area });
2097
- }
2098
- const color = thicknessColor(thickness, options);
2099
- for (let vertex = 0; vertex < 3; vertex += 1) {
2100
- const colorOffset = (offset + vertex) * 3;
2101
- colors[colorOffset] = color[0] / 255;
2102
- colors[colorOffset + 1] = color[1] / 255;
2103
- colors[colorOffset + 2] = color[2] / 255;
2104
- }
2105
- }
2298
+ function geometryWithVertexColors(renderable, colors) {
2299
+ if (!colors) return null;
2300
+ const position = renderable.solid.geometry.getAttribute("position");
2301
+ if (!position || colors.length !== position.count * 3) return null;
2302
+ const geometry = renderable.solid.geometry.clone();
2106
2303
  geometry.setAttribute("color", new BufferAttribute(colors, 3));
2107
- rayMaterial.dispose();
2108
- return { geometry, samples, triangleCount, sampledTriangleCount, sampleStride, warnings };
2304
+ return geometry;
2109
2305
  }
2110
2306
  function buildMaskEntries(session) {
2111
2307
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
@@ -2196,6 +2392,7 @@ function decorateConnectivityReport(report) {
2196
2392
  return {
2197
2393
  method: report.method,
2198
2394
  options: report.options,
2395
+ broadphase: report.broadphase,
2199
2396
  objectCount: report.objectCount,
2200
2397
  componentCount: report.componentCount,
2201
2398
  objects,
@@ -2208,30 +2405,26 @@ function renderCurrentConnectivity(session, rawReport) {
2208
2405
  const r = getRenderer(session.size, session.pixelRatio);
2209
2406
  const report = rawReport ?? analyzeSessionConnectivity(session);
2210
2407
  const byId = new Map(report.objects.map((object) => [object.id, object]));
2408
+ const vertexColorsById = session.connectivityBodyInput ? meshVertexColorBuffersFor(session.connectivityBodyInput, (entryId) => {
2409
+ const object = byId.get(entryId);
2410
+ return rgbFloats(object ? maskColorForIndex(object.componentIndex) : [128, 128, 128]);
2411
+ }) : /* @__PURE__ */ new Map();
2211
2412
  const replacements = session.renderables.map((renderable) => {
2212
2413
  const object = byId.get(renderable.id);
2213
2414
  const color = object ? maskColorForIndex(object.componentIndex) : [128, 128, 128];
2214
- const material = new ShaderMaterial({
2215
- uniforms: {
2216
- connectivityColor: { value: new Vector3(color[0] / 255, color[1] / 255, color[2] / 255) }
2217
- },
2218
- vertexShader: `
2219
- void main() {
2220
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
2221
- }
2222
- `,
2223
- fragmentShader: `
2224
- precision highp float;
2225
- uniform vec3 connectivityColor;
2226
- void main() {
2227
- gl_FragColor = vec4(connectivityColor, 1.0);
2228
- }
2229
- `
2415
+ const previousMaterial = renderable.solid.material;
2416
+ const previousGeometry = renderable.solid.geometry;
2417
+ const vertexGeometry = geometryWithVertexColors(renderable, vertexColorsById.get(renderable.id));
2418
+ const material = new MeshBasicMaterial({
2419
+ color: vertexGeometry ? 16777215 : new Color(color[0] / 255, color[1] / 255, color[2] / 255),
2420
+ vertexColors: Boolean(vertexGeometry),
2421
+ side: DoubleSide,
2422
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null
2230
2423
  });
2231
2424
  material.toneMapped = false;
2232
- const previousMaterial = renderable.solid.material;
2425
+ if (vertexGeometry) renderable.solid.geometry = vertexGeometry;
2233
2426
  renderable.solid.material = material;
2234
- return { renderable, previousMaterial, material };
2427
+ return { renderable, previousMaterial, previousGeometry, vertexGeometry, material };
2235
2428
  });
2236
2429
  try {
2237
2430
  const png = withSolidOnlyVisibility(
@@ -2244,8 +2437,10 @@ function renderCurrentConnectivity(session, rawReport) {
2244
2437
  );
2245
2438
  return { png, report: decorateConnectivityReport(report) };
2246
2439
  } finally {
2247
- replacements.forEach(({ renderable, previousMaterial, material }) => {
2440
+ replacements.forEach(({ renderable, previousMaterial, previousGeometry, vertexGeometry, material }) => {
2248
2441
  renderable.solid.material = previousMaterial;
2442
+ renderable.solid.geometry = previousGeometry;
2443
+ vertexGeometry == null ? void 0 : vertexGeometry.dispose();
2249
2444
  material.dispose();
2250
2445
  });
2251
2446
  }
@@ -2284,6 +2479,7 @@ function decorateDistanceReport(report) {
2284
2479
  maxRootDistance: report.maxRootDistance,
2285
2480
  objects,
2286
2481
  components,
2482
+ gapEdgeCount: report.gapEdgeCount,
2287
2483
  gapEdges: report.gapEdges,
2288
2484
  connectivity: report.connectivity,
2289
2485
  warnings: report.warnings,
@@ -2303,30 +2499,26 @@ function renderCurrentDistance(session) {
2303
2499
  const r = getRenderer(session.size, session.pixelRatio);
2304
2500
  const report = decorateDistanceReport(analyzeSessionDistance(session));
2305
2501
  const byId = new Map(report.objects.map((object) => [object.id, object]));
2502
+ const vertexColorsById = session.connectivityBodyInput ? meshVertexColorBuffersFor(session.connectivityBodyInput, (entryId) => {
2503
+ var _a;
2504
+ return rgbFloats(((_a = byId.get(entryId)) == null ? void 0 : _a.color) ?? DISTANCE_UNKNOWN_COLOR);
2505
+ }) : /* @__PURE__ */ new Map();
2306
2506
  const replacements = session.renderables.map((renderable) => {
2307
2507
  const object = byId.get(renderable.id);
2308
2508
  const color = (object == null ? void 0 : object.color) ?? DISTANCE_UNKNOWN_COLOR;
2309
- const material = new ShaderMaterial({
2310
- uniforms: {
2311
- distanceColor: { value: new Vector3(color[0] / 255, color[1] / 255, color[2] / 255) }
2312
- },
2313
- vertexShader: `
2314
- void main() {
2315
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
2316
- }
2317
- `,
2318
- fragmentShader: `
2319
- precision highp float;
2320
- uniform vec3 distanceColor;
2321
- void main() {
2322
- gl_FragColor = vec4(distanceColor, 1.0);
2323
- }
2324
- `
2509
+ const previousMaterial = renderable.solid.material;
2510
+ const previousGeometry = renderable.solid.geometry;
2511
+ const vertexGeometry = geometryWithVertexColors(renderable, vertexColorsById.get(renderable.id));
2512
+ const material = new MeshBasicMaterial({
2513
+ color: vertexGeometry ? 16777215 : new Color(color[0] / 255, color[1] / 255, color[2] / 255),
2514
+ vertexColors: Boolean(vertexGeometry),
2515
+ side: DoubleSide,
2516
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? null
2325
2517
  });
2326
2518
  material.toneMapped = false;
2327
- const previousMaterial = renderable.solid.material;
2519
+ if (vertexGeometry) renderable.solid.geometry = vertexGeometry;
2328
2520
  renderable.solid.material = material;
2329
- return { renderable, previousMaterial, material };
2521
+ return { renderable, previousMaterial, previousGeometry, vertexGeometry, material };
2330
2522
  });
2331
2523
  try {
2332
2524
  const png = withSolidOnlyVisibility(
@@ -2339,8 +2531,10 @@ function renderCurrentDistance(session) {
2339
2531
  );
2340
2532
  return { png, report };
2341
2533
  } finally {
2342
- replacements.forEach(({ renderable, previousMaterial, material }) => {
2534
+ replacements.forEach(({ renderable, previousMaterial, previousGeometry, vertexGeometry, material }) => {
2343
2535
  renderable.solid.material = previousMaterial;
2536
+ renderable.solid.geometry = previousGeometry;
2537
+ vertexGeometry == null ? void 0 : vertexGeometry.dispose();
2344
2538
  material.dispose();
2345
2539
  });
2346
2540
  }
@@ -2440,26 +2634,113 @@ function renderCurrentCollisions(session) {
2440
2634
  collisionMaterials.forEach((material) => material.dispose());
2441
2635
  }
2442
2636
  }
2443
- function renderCurrentThickness(session, rawOptions) {
2637
+ function inspectionOptionsKey(value) {
2638
+ return JSON.stringify(value ?? {});
2639
+ }
2640
+ function bboxFromGeometry(geometry) {
2641
+ geometry.computeBoundingBox();
2642
+ const bbox = geometry.boundingBox;
2643
+ return {
2644
+ min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
2645
+ max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
2646
+ };
2647
+ }
2648
+ function pointBuffersFromSamples(samples, offset = 0.025) {
2649
+ const positions = new Float32Array(samples.length * 3);
2650
+ const colors = new Float32Array(samples.length * 3);
2651
+ samples.forEach((sample, index) => {
2652
+ const base = index * 3;
2653
+ positions[base] = sample.position[0] + sample.normal[0] * offset;
2654
+ positions[base + 1] = sample.position[1] + sample.normal[1] * offset;
2655
+ positions[base + 2] = sample.position[2] + sample.normal[2] * offset;
2656
+ colors[base] = sample.color[0] / 255;
2657
+ colors[base + 1] = sample.color[1] / 255;
2658
+ colors[base + 2] = sample.color[2] / 255;
2659
+ });
2660
+ return { positions, colors };
2661
+ }
2662
+ function renderScalarPointOverlays(session, overlays) {
2444
2663
  const r = getRenderer(session.size, session.pixelRatio);
2664
+ const overlayById = new Map(overlays.map((overlay) => [overlay.renderable.id, overlay]));
2665
+ const replacements = session.renderables.map((renderable) => {
2666
+ const previousMaterial = renderable.solid.material;
2667
+ const ghostMaterial = new MeshBasicMaterial({
2668
+ color: new Color(2502970),
2669
+ transparent: true,
2670
+ opacity: 0.24,
2671
+ depthWrite: true,
2672
+ depthTest: true,
2673
+ side: DoubleSide,
2674
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? void 0
2675
+ });
2676
+ ghostMaterial.toneMapped = false;
2677
+ renderable.solid.material = ghostMaterial;
2678
+ const overlay = overlayById.get(renderable.id);
2679
+ if (!overlay || overlay.positions.length === 0)
2680
+ return { renderable, previousMaterial, ghostMaterial, points: null, geometry: null, material: null };
2681
+ const geometry = new BufferGeometry();
2682
+ geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
2683
+ geometry.setAttribute("color", new BufferAttribute(overlay.colors, 3));
2684
+ const material = new PointsMaterial({
2685
+ size: 3,
2686
+ sizeAttenuation: false,
2687
+ vertexColors: true,
2688
+ depthTest: true,
2689
+ depthWrite: false,
2690
+ clippingPlanes: renderable.solidMaterial.clippingPlanes ?? void 0
2691
+ });
2692
+ material.toneMapped = false;
2693
+ const points = new Points(geometry, material);
2694
+ points.renderOrder = 5;
2695
+ points.raycast = () => null;
2696
+ renderable.root.add(points);
2697
+ return { renderable, previousMaterial, ghostMaterial, points, geometry, material };
2698
+ });
2699
+ try {
2700
+ return withSolidOnlyVisibility(
2701
+ session,
2702
+ () => withTemporarySceneBackground(session, new Color(0), () => {
2703
+ updateSdfRaymarchUniforms(session);
2704
+ r.render(session.scene, session.camera);
2705
+ return captureRenderedPng(session.size);
2706
+ })
2707
+ );
2708
+ } finally {
2709
+ replacements.forEach(({ renderable, previousMaterial, ghostMaterial, points, geometry, material }) => {
2710
+ if (points) renderable.root.remove(points);
2711
+ renderable.solid.material = previousMaterial;
2712
+ ghostMaterial.dispose();
2713
+ geometry == null ? void 0 : geometry.dispose();
2714
+ material == null ? void 0 : material.dispose();
2715
+ });
2716
+ }
2717
+ }
2718
+ function getSessionThicknessInspection(session, rawOptions) {
2719
+ var _a;
2445
2720
  const options = resolveThicknessInspectionOptions(rawOptions);
2446
- const material = new MeshBasicMaterial({ vertexColors: true });
2447
- material.toneMapped = false;
2721
+ const optionsKey = inspectionOptionsKey(options);
2722
+ if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
2448
2723
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2449
2724
  const warnings = [];
2450
2725
  const objects = [];
2451
- const replacements = session.renderables.map((renderable, index) => {
2452
- const previousMaterial = renderable.solid.material;
2453
- const previousGeometry = renderable.solid.geometry;
2454
- const analysis = analyzeThicknessGeometry(previousGeometry, options);
2455
- const bbox = analysis.geometry.boundingBox;
2456
- const summary = summarizeThicknessSamples(analysis.samples, options);
2726
+ const cloudObjects = [];
2727
+ const points = [];
2728
+ const overlays = [];
2729
+ session.renderables.forEach((renderable, index) => {
2457
2730
  const sourceObject = byId.get(renderable.id);
2731
+ if (renderable.sdfRaymarch) {
2732
+ warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh thickness inspection.`);
2733
+ return;
2734
+ }
2735
+ const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options);
2736
+ const bbox = bboxFromGeometry(analysis.geometry);
2737
+ const summary = summarizeThicknessSamples(analysis.samples, options);
2458
2738
  if (analysis.warnings.length > 0) {
2459
2739
  analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
2460
2740
  }
2741
+ const objectIndex = index + 1;
2461
2742
  objects.push({
2462
- index: index + 1,
2743
+ index: objectIndex,
2463
2744
  id: renderable.id,
2464
2745
  name: renderable.name,
2465
2746
  groupName: renderable.groupName,
@@ -2468,174 +2749,154 @@ function renderCurrentThickness(session, rawOptions) {
2468
2749
  triangleCount: analysis.triangleCount,
2469
2750
  sampledTriangleCount: analysis.sampledTriangleCount,
2470
2751
  sampleStride: analysis.sampleStride,
2471
- bbox: {
2472
- min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
2473
- max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
2474
- },
2752
+ bbox,
2475
2753
  ...summary
2476
2754
  });
2477
- renderable.solid.geometry = analysis.geometry;
2478
- renderable.solid.material = material;
2479
- return { renderable, previousMaterial, previousGeometry, geometry: analysis.geometry };
2755
+ cloudObjects.push({
2756
+ index: objectIndex,
2757
+ id: renderable.id,
2758
+ name: renderable.name,
2759
+ groupName: renderable.groupName,
2760
+ treePath: sourceObject == null ? void 0 : sourceObject.treePath,
2761
+ mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
2762
+ sampleCount: analysis.pointSamples.length,
2763
+ bbox
2764
+ });
2765
+ analysis.pointSamples.forEach((sample) => {
2766
+ points.push({
2767
+ objectIndex,
2768
+ objectId: renderable.id,
2769
+ objectName: renderable.name,
2770
+ ...sample
2771
+ });
2772
+ });
2773
+ overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples) });
2774
+ analysis.geometry.dispose();
2480
2775
  });
2481
- try {
2482
- const png = withSolidOnlyVisibility(
2483
- session,
2484
- () => withTemporarySceneBackground(session, new Color(0), () => {
2485
- updateSdfRaymarchUniforms(session);
2486
- r.render(session.scene, session.camera);
2487
- return captureRenderedPng(session.size);
2488
- })
2489
- );
2490
- return {
2491
- png,
2492
- report: {
2493
- method: "mesh-normal-raycast",
2494
- options,
2495
- objectCount: objects.length,
2496
- objects,
2497
- warnings,
2498
- style: {
2499
- criticalColor: THICKNESS_COLORS.critical,
2500
- warningColor: THICKNESS_COLORS.warning,
2501
- okColor: THICKNESS_COLORS.ok,
2502
- thickColor: THICKNESS_COLORS.thick,
2503
- unknownColor: THICKNESS_COLORS.unknown
2504
- }
2776
+ const state = {
2777
+ optionsKey,
2778
+ overlays,
2779
+ pointCloud: {
2780
+ schemaVersion: 1,
2781
+ property: "thickness",
2782
+ coordinateSpace: "object-local",
2783
+ units: "model",
2784
+ sampleCount: points.length,
2785
+ objects: cloudObjects,
2786
+ points
2787
+ },
2788
+ report: {
2789
+ method: "mesh-normal-raycast",
2790
+ options,
2791
+ objectCount: objects.length,
2792
+ objects,
2793
+ warnings,
2794
+ style: {
2795
+ criticalColor: THICKNESS_COLORS.critical,
2796
+ warningColor: THICKNESS_COLORS.warning,
2797
+ okColor: THICKNESS_COLORS.ok,
2798
+ thickColor: THICKNESS_COLORS.thick,
2799
+ unknownColor: THICKNESS_COLORS.unknown
2505
2800
  }
2506
- };
2507
- } finally {
2508
- replacements.forEach(({ renderable, previousMaterial, previousGeometry, geometry }) => {
2509
- renderable.solid.geometry = previousGeometry;
2510
- renderable.solid.material = previousMaterial;
2511
- geometry.dispose();
2512
- });
2513
- material.dispose();
2514
- }
2801
+ }
2802
+ };
2803
+ session.thicknessInspection = state;
2804
+ return state;
2805
+ }
2806
+ function renderCurrentThickness(session, rawOptions) {
2807
+ const state = getSessionThicknessInspection(session, rawOptions);
2808
+ return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
2515
2809
  }
2516
2810
  const ROUGHNESS_SMOOTH_OPACITY = 0.16;
2517
2811
  const ROUGHNESS_HARSH_OPACITY = 1;
2518
- function createRoughnessMaterial(clippingPlanes) {
2519
- const material = new ShaderMaterial({
2520
- transparent: true,
2521
- depthTest: true,
2522
- depthWrite: true,
2523
- clippingPlanes: clippingPlanes ?? void 0,
2524
- uniforms: {
2525
- smoothOpacity: { value: ROUGHNESS_SMOOTH_OPACITY },
2526
- harshOpacity: { value: ROUGHNESS_HARSH_OPACITY }
2527
- },
2528
- vertexShader: `
2529
- attribute vec3 color;
2530
- attribute float roughnessScore;
2531
- varying vec3 vColor;
2532
- varying float vRoughnessScore;
2533
- varying vec3 vViewNormal;
2534
-
2535
- void main() {
2536
- vColor = color;
2537
- vRoughnessScore = roughnessScore;
2538
- vViewNormal = normalize(normalMatrix * normal);
2539
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
2540
- }
2541
- `,
2542
- fragmentShader: `
2543
- precision highp float;
2544
- uniform float smoothOpacity;
2545
- uniform float harshOpacity;
2546
- varying vec3 vColor;
2547
- varying float vRoughnessScore;
2548
- varying vec3 vViewNormal;
2549
-
2550
- void main() {
2551
- vec3 shadowLight = normalize(vec3(0.32, 0.42, 0.84));
2552
- float shade = 0.34 + 0.48 * max(dot(normalize(vViewNormal), shadowLight), 0.0);
2553
- vec3 smoothShadow = vec3(0.26, 0.30, 0.35) * shade;
2554
- float colorMix = smoothstep(0.04, 0.35, vRoughnessScore);
2555
- vec3 finalColor = mix(smoothShadow, vColor, colorMix);
2556
- float alpha = mix(smoothOpacity, harshOpacity, smoothstep(0.02, 0.55, vRoughnessScore));
2557
- gl_FragColor = vec4(finalColor, alpha);
2558
- }
2559
- `
2560
- });
2561
- material.toneMapped = false;
2562
- return material;
2563
- }
2564
2812
  function renderCurrentRoughness(session, rawOptions) {
2565
- const r = getRenderer(session.size, session.pixelRatio);
2813
+ const state = getSessionRoughnessInspection(session, rawOptions);
2814
+ return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
2815
+ }
2816
+ function getSessionRoughnessInspection(session, rawOptions) {
2817
+ var _a;
2566
2818
  const options = resolveRoughnessInspectionOptions(rawOptions);
2819
+ const optionsKey = inspectionOptionsKey(options);
2820
+ if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
2567
2821
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
2568
2822
  const warnings = [];
2569
2823
  const objects = [];
2570
- const replacements = session.renderables.map((renderable, index) => {
2571
- const previousMaterial = renderable.solid.material;
2572
- const previousGeometry = renderable.solid.geometry;
2824
+ const cloudObjects = [];
2825
+ const points = [];
2826
+ const overlays = [];
2827
+ session.renderables.forEach((renderable, index) => {
2573
2828
  if (renderable.sdfRaymarch) {
2574
- const material2 = new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false });
2575
- material2.toneMapped = false;
2576
- renderable.solid.material = material2;
2577
2829
  warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh roughness inspection.`);
2578
- return { renderable, previousMaterial, previousGeometry, material: material2, geometry: null };
2830
+ return;
2579
2831
  }
2580
- const analysis = analyzeRoughnessGeometry(previousGeometry, options);
2581
- const bbox = analysis.geometry.boundingBox;
2832
+ const analysis = analyzeRoughnessGeometry(renderable.solid.geometry, options);
2833
+ const bbox = bboxFromGeometry(analysis.geometry);
2582
2834
  const sourceObject = byId.get(renderable.id);
2583
2835
  if (analysis.warnings.length > 0) {
2584
2836
  analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
2585
2837
  }
2838
+ const objectIndex = index + 1;
2586
2839
  objects.push({
2587
- index: index + 1,
2840
+ index: objectIndex,
2588
2841
  id: renderable.id,
2589
2842
  name: renderable.name,
2590
2843
  groupName: renderable.groupName,
2591
2844
  treePath: sourceObject == null ? void 0 : sourceObject.treePath,
2592
2845
  mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
2593
- bbox: {
2594
- min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
2595
- max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
2596
- },
2846
+ bbox,
2597
2847
  ...analysis.summary
2598
2848
  });
2599
- const material = createRoughnessMaterial(renderable.solidMaterial.clippingPlanes);
2600
- renderable.solid.geometry = analysis.geometry;
2601
- renderable.solid.material = material;
2602
- return { renderable, previousMaterial, previousGeometry, material, geometry: analysis.geometry };
2849
+ cloudObjects.push({
2850
+ index: objectIndex,
2851
+ id: renderable.id,
2852
+ name: renderable.name,
2853
+ groupName: renderable.groupName,
2854
+ treePath: sourceObject == null ? void 0 : sourceObject.treePath,
2855
+ mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
2856
+ sampleCount: analysis.pointSamples.length,
2857
+ bbox
2858
+ });
2859
+ analysis.pointSamples.forEach((sample) => {
2860
+ points.push({
2861
+ objectIndex,
2862
+ objectId: renderable.id,
2863
+ objectName: renderable.name,
2864
+ ...sample
2865
+ });
2866
+ });
2867
+ overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples) });
2868
+ analysis.geometry.dispose();
2603
2869
  });
2604
- try {
2605
- const png = withSolidOnlyVisibility(
2606
- session,
2607
- () => withTemporarySceneBackground(session, new Color(0), () => {
2608
- updateSdfRaymarchUniforms(session);
2609
- r.render(session.scene, session.camera);
2610
- return captureRenderedPng(session.size);
2611
- })
2612
- );
2613
- return {
2614
- png,
2615
- report: {
2616
- method: "mesh-dihedral-angle",
2617
- options,
2618
- objectCount: objects.length,
2619
- objects,
2620
- warnings,
2621
- style: {
2622
- smoothColor: ROUGHNESS_COLORS.smooth,
2623
- moderateColor: ROUGHNESS_COLORS.moderate,
2624
- sharpColor: ROUGHNESS_COLORS.sharp,
2625
- harshColor: ROUGHNESS_COLORS.harsh,
2626
- smoothOpacity: ROUGHNESS_SMOOTH_OPACITY,
2627
- harshOpacity: ROUGHNESS_HARSH_OPACITY
2628
- }
2870
+ const state = {
2871
+ optionsKey,
2872
+ overlays,
2873
+ pointCloud: {
2874
+ schemaVersion: 1,
2875
+ property: "roughness",
2876
+ coordinateSpace: "object-local",
2877
+ units: "degrees",
2878
+ sampleCount: points.length,
2879
+ objects: cloudObjects,
2880
+ points
2881
+ },
2882
+ report: {
2883
+ method: "mesh-dihedral-angle",
2884
+ options,
2885
+ objectCount: objects.length,
2886
+ objects,
2887
+ warnings,
2888
+ style: {
2889
+ smoothColor: ROUGHNESS_COLORS.smooth,
2890
+ moderateColor: ROUGHNESS_COLORS.moderate,
2891
+ sharpColor: ROUGHNESS_COLORS.sharp,
2892
+ harshColor: ROUGHNESS_COLORS.harsh,
2893
+ smoothOpacity: ROUGHNESS_SMOOTH_OPACITY,
2894
+ harshOpacity: ROUGHNESS_HARSH_OPACITY
2629
2895
  }
2630
- };
2631
- } finally {
2632
- replacements.forEach(({ renderable, previousMaterial, previousGeometry, material, geometry }) => {
2633
- renderable.solid.geometry = previousGeometry;
2634
- renderable.solid.material = previousMaterial;
2635
- geometry == null ? void 0 : geometry.dispose();
2636
- material.dispose();
2637
- });
2638
- }
2896
+ }
2897
+ };
2898
+ session.roughnessInspection = state;
2899
+ return state;
2639
2900
  }
2640
2901
  function emptySectionSvg() {
2641
2902
  return {
@@ -3524,19 +3785,8 @@ function createSession(code, opts) {
3524
3785
  name: entry.source.name,
3525
3786
  shape: entry.shape
3526
3787
  }));
3527
- const connectivityEntries = (opts == null ? void 0 : opts.includeConnectivity) ? shapeVisibleObjs.map((entry) => {
3528
- const bb2 = entry.shape.boundingBox();
3529
- return {
3530
- id: entry.source.id,
3531
- name: entry.source.name,
3532
- shape: entry.shape,
3533
- min: [bb2.min[0], bb2.min[1], bb2.min[2]],
3534
- max: [bb2.max[0], bb2.max[1], bb2.max[2]],
3535
- groupName: entry.source.groupName,
3536
- treePath: entry.source.treePath,
3537
- mock: entry.source.mock
3538
- };
3539
- }) : [];
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) ?? [];
3540
3790
  const collisionEntries = (opts == null ? void 0 : opts.includeCollisions) ? shapeVisibleObjs.map((entry) => {
3541
3791
  const bb2 = entry.shape.boundingBox();
3542
3792
  return {
@@ -3610,11 +3860,7 @@ function createSession(code, opts) {
3610
3860
  requestedSceneState = mergeViewportRenderSceneStates(requestedSceneState, {
3611
3861
  camera: {
3612
3862
  projectionMode: "perspective",
3613
- position: [
3614
- center.x + dir[0] * tokenDistance,
3615
- center.y + dir[1] * tokenDistance,
3616
- center.z + dir[2] * tokenDistance
3617
- ],
3863
+ position: [center.x + dir[0] * tokenDistance, center.y + dir[1] * tokenDistance, center.z + dir[2] * tokenDistance],
3618
3864
  target: [center.x, center.y, center.z],
3619
3865
  up: [0, 0, 1],
3620
3866
  fov: cameraFov
@@ -3843,10 +4089,13 @@ function createSession(code, opts) {
3843
4089
  renderables,
3844
4090
  sectionShapes,
3845
4091
  connectivityEntries,
4092
+ connectivityBodyInput,
3846
4093
  physicalConnectivity: null,
3847
4094
  distanceReport: null,
3848
4095
  collisionEntries,
3849
4096
  collisionReport: null,
4097
+ thicknessInspection: null,
4098
+ roughnessInspection: null,
3850
4099
  joints,
3851
4100
  jointCouplings,
3852
4101
  animationClips,
@@ -3951,6 +4200,8 @@ window.__forgeRender = async (code, opts) => {
3951
4200
  let collisionReport = null;
3952
4201
  let thicknessReport = null;
3953
4202
  let roughnessReport = null;
4203
+ let thicknessPointCloud = null;
4204
+ let roughnessPointCloud = null;
3954
4205
  let sectionAtlas = null;
3955
4206
  const channelViewCounts = /* @__PURE__ */ new Map();
3956
4207
  const markChannelViewStart = async (channel, view) => {
@@ -3996,6 +4247,7 @@ window.__forgeRender = async (code, opts) => {
3996
4247
  const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
3997
4248
  roughnessRenders[label] = roughness.png;
3998
4249
  roughnessReport = roughness.report;
4250
+ roughnessPointCloud = roughness.pointCloud;
3999
4251
  await markChannelViewDone("roughness", label);
4000
4252
  }
4001
4253
  if (requestedChannels.has("mask")) {
@@ -4034,6 +4286,7 @@ window.__forgeRender = async (code, opts) => {
4034
4286
  const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness);
4035
4287
  thicknessRenders[label] = thickness.png;
4036
4288
  thicknessReport = thickness.report;
4289
+ thicknessPointCloud = thickness.pointCloud;
4037
4290
  await markChannelViewDone("thickness", label);
4038
4291
  }
4039
4292
  } catch (e) {
@@ -4060,6 +4313,7 @@ window.__forgeRender = async (code, opts) => {
4060
4313
  normals: normalRenders,
4061
4314
  roughness: roughnessReport ? {
4062
4315
  ...roughnessReport,
4316
+ pointCloud: roughnessPointCloud,
4063
4317
  views: roughnessRenders
4064
4318
  } : {
4065
4319
  views: roughnessRenders
@@ -4088,6 +4342,7 @@ window.__forgeRender = async (code, opts) => {
4088
4342
  },
4089
4343
  thickness: thicknessReport ? {
4090
4344
  ...thicknessReport,
4345
+ pointCloud: thicknessPointCloud,
4091
4346
  views: thicknessRenders
4092
4347
  } : {
4093
4348
  views: thicknessRenders