forgecad 0.9.6 → 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 (76) hide show
  1. package/dist/assets/{AdminPage-Da6hhpJx.js → AdminPage-DX0mpSZT.js} +1 -1
  2. package/dist/assets/{BlogPage-Bl_sKeWb.js → BlogPage-CI_P0_Pf.js} +1 -1
  3. package/dist/assets/{DocsPage-Blz3Tp4j.js → DocsPage-DLhIIZyJ.js} +3 -3
  4. package/dist/assets/{EditorApp-CuiPbtn5.js → EditorApp-BujZvuwX.js} +140 -20
  5. package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
  6. package/dist/assets/{EmbedViewer-BFG6-Ufm.js → EmbedViewer-0S0qXKog.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-DB9fQd5P.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
  8. package/dist/assets/{PricingPage-BMxYT_F0.js → PricingPage-DGkX3Ahr.js} +1 -1
  9. package/dist/assets/{SettingsPage-VVQNrCAg.js → SettingsPage-DBsqTB_y.js} +82 -22
  10. package/dist/assets/{app-Dl9ymBWC.js → app-BE2nD6Yz.js} +1056 -258
  11. package/dist/assets/cli/{render-CFtwKCCY.js → render-iP9qh475.js} +1533 -207
  12. package/dist/assets/{evalWorker-CRvbzTXm.js → evalWorker-Ds5U4xtN.js} +2178 -30
  13. package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
  14. package/dist/assets/{manifold-DpBXFS2K.js → manifold-Bk26ViCr.js} +1 -1
  15. package/dist/assets/{manifold-DzZ4VRPs.js → manifold-DjYsd7A_.js} +2 -2
  16. package/dist/assets/{manifold-B9QSr-qP.js → manifold-sJ-axdXM.js} +1 -1
  17. package/dist/assets/{renderSceneState-BuAXF2jh.js → renderSceneState-Bngp5MrQ.js} +1 -1
  18. package/dist/assets/{reportWorker-BNWEnRg1.js → reportWorker-CU8RZ4O0.js} +2161 -30
  19. package/dist/assets/{distance-BEC2RjJi.js → sectionPlaneMath-BdTjyVfs.js} +2539 -1187
  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 +5 -0
  25. package/dist/docs-raw/coding.md +1 -1
  26. package/dist/docs-raw/generated/concepts.md +59 -2
  27. package/dist/docs-raw/generated/core.md +206 -1
  28. package/dist/docs-raw/generated/lib.md +17 -1
  29. package/dist/docs-raw/generated/viewport.md +1 -1
  30. package/dist/docs-raw/guides/inspection-bundles.md +36 -13
  31. package/dist/docs-raw/platform/auth.md +2 -0
  32. package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
  33. package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
  34. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
  35. package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
  36. package/dist/docs-raw/skills/index.md +2 -2
  37. package/dist/index.html +1 -1
  38. package/dist/sitemap.xml +6 -6
  39. package/dist-cli/forgecad.js +7975 -4528
  40. package/dist-cli/forgecad.js.map +1 -1
  41. package/dist-skill/CONTEXT.md +260 -16
  42. package/dist-skill/docs/CLI.md +82 -53
  43. package/dist-skill/docs/generated/core.md +206 -1
  44. package/dist-skill/docs/generated/lib.md +17 -1
  45. package/dist-skill/docs/generated/viewport.md +1 -1
  46. package/dist-skill/docs/guides/inspection-bundles.md +36 -13
  47. package/dist-skill/docs-dev/CLI.md +82 -53
  48. package/dist-skill/docs-dev/coding.md +1 -1
  49. package/dist-skill/docs-dev/generated/core.md +206 -1
  50. package/dist-skill/docs-dev/generated/lib.md +17 -1
  51. package/dist-skill/docs-dev/generated/viewport.md +1 -1
  52. package/dist-skill/docs-dev/guides/inspection-bundles.md +36 -13
  53. package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
  54. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
  55. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
  56. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
  57. package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
  58. package/examples/api/bolted-service-cover.forge.js +17 -0
  59. package/examples/api/cable-gland-anchor.forge.js +14 -0
  60. package/examples/api/captured-cartridge-guide.forge.js +14 -0
  61. package/examples/api/captured-linear-slide.forge.js +13 -0
  62. package/examples/api/clevis-pin-joint.forge.js +13 -0
  63. package/examples/api/datum-enclosure.forge.js +16 -0
  64. package/examples/api/hose-barb-port.forge.js +14 -0
  65. package/examples/api/intentional-overlap-overmold.forge.js +16 -0
  66. package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
  67. package/examples/api/living-hinge-cover.forge.js +14 -0
  68. package/examples/api/pcb-terminal-block.forge.js +22 -0
  69. package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
  70. package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
  71. package/examples/api/routed-tube-clip.forge.js +15 -0
  72. package/examples/api/seated-bearing-stack.forge.js +30 -0
  73. package/examples/api/snap-latch-cover.forge.js +14 -0
  74. package/examples/api/static-assembly-connectors.forge.js +14 -16
  75. package/examples/api/thumb-screw-clamp.forge.js +15 -0
  76. package/package.json +1 -1
@@ -1,5 +1,8 @@
1
- import { D as DoubleSide, bM as initSolverWasm, bL as initKernel, S as Scene, bN as BoxGeometry, bg as MeshStandardMaterial, a4 as BackSide, b0 as PointLight, M as Mesh, aa as MeshBasicMaterial, bO as localAabbPlaneRelation, h as Vector2, bP as ShapeUtils, g as Vector3, e as Color, aC as resolveForgeRenderStyle, bc as getRenderStylePreset, ax as setParamOverrides, b7 as runScript, a0 as MathUtils, G as Box3, bQ as Group, b3 as shapeToGeometry, b8 as MeshPhysicalMaterial, bd as AdditiveBlending, aH as LineBasicMaterial, b9 as LineSegments, aG as BufferGeometry, P as PerspectiveCamera, k as ShaderMaterial, bR as resolveRoughnessInspectionOptions, bb as analyzeRoughnessGeometry, bS as ROUGHNESS_COLORS, bJ as analyzePhysicalConnectivity, bT as resolveThicknessInspectionOptions, ba as analyzeThicknessGeometry, bU as summarizeThicknessSamples, bV as THICKNESS_COLORS, bW as intersectWithPlane, W as WebGLRenderer, A as ACESFilmicToneMapping, c as SRGBColorSpace, bX as parseCameraCliSpec, bY as PMREMGenerator, aV as CanvasTexture, aW as Object3D, aX as FogExp2, aY as Fog, aZ as AmbientLight, b1 as DirectionalLight, a_ as HemisphereLight, bC as findJointAnimationClip, p as Plane, Y as Vector4, $ as Matrix4, bj as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bi as buildSdfRaymarchFragmentShader, O as OrthographicCamera, bD as resolveJointAnimation, bE as resolveJointViewValues, bK as analyzeDistanceInspection, b2 as analyzeCollisionIntersections, bZ as serializeCollisionFinding, b_ as worldAuthorPlaneToLocal, a$ as SpotLight, aU as BufferAttribute } from "../distance-BEC2RjJi.js";
2
- import { m as mergeViewportRenderSceneStates, p as parseRenderSceneCliSpec } from "../renderSceneState-BuAXF2jh.js";
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
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";
3
6
  const CAD_MATERIAL_PROPS = {
4
7
  color: 6003669,
5
8
  metalness: 0.05,
@@ -266,17 +269,17 @@ function stitchLoops(points, edges) {
266
269
  const warnings = [];
267
270
  const adjacency = /* @__PURE__ */ new Map();
268
271
  const unusedEdges = /* @__PURE__ */ new Set();
269
- const edgeKey = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
272
+ const edgeKey2 = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
270
273
  for (const [a, b] of edges) {
271
274
  if (!adjacency.has(a)) adjacency.set(a, []);
272
275
  if (!adjacency.has(b)) adjacency.set(b, []);
273
276
  (_a = adjacency.get(a)) == null ? void 0 : _a.push(b);
274
277
  (_b = adjacency.get(b)) == null ? void 0 : _b.push(a);
275
- unusedEdges.add(edgeKey(a, b));
278
+ unusedEdges.add(edgeKey2(a, b));
276
279
  }
277
280
  const loops = [];
278
281
  for (const [edgeA, edgeB] of edges) {
279
- const firstKey = edgeKey(edgeA, edgeB);
282
+ const firstKey = edgeKey2(edgeA, edgeB);
280
283
  if (!unusedEdges.has(firstKey)) continue;
281
284
  const loop = [edgeA, edgeB];
282
285
  unusedEdges.delete(firstKey);
@@ -285,12 +288,12 @@ function stitchLoops(points, edges) {
285
288
  let closed = false;
286
289
  for (let guard = 0; guard < points.length + edges.length + 8; guard += 1) {
287
290
  const neighbors = adjacency.get(current) ?? [];
288
- const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey(current, candidate)));
291
+ const next = neighbors.find((candidate) => candidate !== previous && unusedEdges.has(edgeKey2(current, candidate)));
289
292
  if (next === void 0) {
290
293
  if (current === edgeA) closed = true;
291
294
  break;
292
295
  }
293
- unusedEdges.delete(edgeKey(current, next));
296
+ unusedEdges.delete(edgeKey2(current, next));
294
297
  if (next === edgeA) {
295
298
  closed = true;
296
299
  break;
@@ -454,6 +457,425 @@ function computeMeshSectionCap(mesh, planeInput) {
454
457
  warnings: stitched.warnings.length > 0 ? stitched.warnings : void 0
455
458
  };
456
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
+ const EPSILON = 1e-9;
689
+ function intervalGap(aMin, aMax, bMin, bMax) {
690
+ if (aMax < bMin) return bMin - aMax;
691
+ if (bMax < aMin) return aMin - bMax;
692
+ return 0;
693
+ }
694
+ function bboxGap(a, b) {
695
+ const axisGaps = [
696
+ intervalGap(a.bbox.min[0], a.bbox.max[0], b.bbox.min[0], b.bbox.max[0]),
697
+ intervalGap(a.bbox.min[1], a.bbox.max[1], b.bbox.min[1], b.bbox.max[1]),
698
+ intervalGap(a.bbox.min[2], a.bbox.max[2], b.bbox.min[2], b.bbox.max[2])
699
+ ];
700
+ const gap = Math.sqrt(axisGaps[0] ** 2 + axisGaps[1] ** 2 + axisGaps[2] ** 2);
701
+ return { gap, axisGaps };
702
+ }
703
+ function bboxVolume(component) {
704
+ const dx = Math.max(0, component.bbox.max[0] - component.bbox.min[0]);
705
+ const dy = Math.max(0, component.bbox.max[1] - component.bbox.min[1]);
706
+ const dz = Math.max(0, component.bbox.max[2] - component.bbox.min[2]);
707
+ return dx * dy * dz;
708
+ }
709
+ function compareDefaultRoot(a, b) {
710
+ if (a.bodyCount !== b.bodyCount) return a.bodyCount - b.bodyCount;
711
+ if (a.objectCount !== b.objectCount) return a.objectCount - b.objectCount;
712
+ const volumeDelta = bboxVolume(a) - bboxVolume(b);
713
+ if (Math.abs(volumeDelta) > EPSILON) return volumeDelta;
714
+ return b.index - a.index;
715
+ }
716
+ function defaultRootComponentIndex(components) {
717
+ if (components.length === 0) return null;
718
+ return components.reduce((best, component) => compareDefaultRoot(component, best) > 0 ? component : best, components[0]).index;
719
+ }
720
+ function componentPositionByIndex(components) {
721
+ return new Map(components.map((component, position) => [component.index, position]));
722
+ }
723
+ function computeNearestComponents(components) {
724
+ const nearest = components.map(() => ({ nearestGap: null, nearestComponentIndex: null }));
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
+ }
741
+ }
742
+ }
743
+ return nearest;
744
+ }
745
+ function computeRootDistances(components, rootComponentIndex) {
746
+ if (rootComponentIndex == null) return [];
747
+ const positions = componentPositionByIndex(components);
748
+ const rootPosition = positions.get(rootComponentIndex);
749
+ if (rootPosition == null) {
750
+ throw new Error(`rootComponentIndex ${rootComponentIndex} does not match any physical component`);
751
+ }
752
+ const visited = components.map(() => false);
753
+ const distances = components.map(() => Infinity);
754
+ const parents = components.map(() => null);
755
+ const parentGaps = components.map(() => null);
756
+ distances[rootPosition] = 0;
757
+ for (; ; ) {
758
+ let current = -1;
759
+ for (let i = 0; i < components.length; i += 1) {
760
+ if (visited[i]) continue;
761
+ if (current === -1 || distances[i] < distances[current] - EPSILON || Math.abs(distances[i] - distances[current]) <= EPSILON && components[i].index < components[current].index) {
762
+ current = i;
763
+ }
764
+ }
765
+ if (current === -1 || !Number.isFinite(distances[current])) break;
766
+ visited[current] = true;
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;
771
+ const nextDistance = distances[current] + 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;
776
+ }
777
+ }
778
+ }
779
+ return components.map((_, position) => ({
780
+ rootDistance: distances[position],
781
+ parentComponentIndex: parents[position],
782
+ parentGap: parentGaps[position]
783
+ }));
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
+ }
817
+ function analyzeDistanceInspection(entries, rawOptions = {}) {
818
+ const connectivity = analyzePhysicalConnectivity(entries, rawOptions);
819
+ const rootComponentIndex = rawOptions.rootComponentIndex ?? defaultRootComponentIndex(connectivity.components);
820
+ const nearest = computeNearestComponents(connectivity.components);
821
+ const rooted = computeRootDistances(connectivity.components, rootComponentIndex);
822
+ const gapEdges = compactGapEdges(connectivity.components, nearest, rooted);
823
+ const componentByIndex = /* @__PURE__ */ new Map();
824
+ const components = connectivity.components.map((component, position) => {
825
+ var _a, _b;
826
+ const rootData = rooted[position] ?? {
827
+ rootDistance: rootComponentIndex === component.index ? 0 : Infinity,
828
+ parentComponentIndex: null,
829
+ parentGap: null
830
+ };
831
+ const decorated = {
832
+ ...component,
833
+ isRoot: component.index === rootComponentIndex,
834
+ rootDistance: rootData.rootDistance,
835
+ nearestGap: ((_a = nearest[position]) == null ? void 0 : _a.nearestGap) ?? null,
836
+ nearestComponentIndex: ((_b = nearest[position]) == null ? void 0 : _b.nearestComponentIndex) ?? null,
837
+ parentComponentIndex: rootData.parentComponentIndex,
838
+ parentGap: rootData.parentGap
839
+ };
840
+ componentByIndex.set(component.index, decorated);
841
+ return decorated;
842
+ });
843
+ const objects = connectivity.objects.map((object) => {
844
+ const component = componentByIndex.get(object.componentIndex);
845
+ return {
846
+ ...object,
847
+ rootDistance: (component == null ? void 0 : component.rootDistance) ?? Infinity,
848
+ nearestGap: (component == null ? void 0 : component.nearestGap) ?? null,
849
+ nearestComponentIndex: (component == null ? void 0 : component.nearestComponentIndex) ?? null,
850
+ parentComponentIndex: (component == null ? void 0 : component.parentComponentIndex) ?? null,
851
+ parentGap: (component == null ? void 0 : component.parentGap) ?? null
852
+ };
853
+ });
854
+ const finiteDistances = components.map((component) => component.rootDistance).filter(Number.isFinite);
855
+ const maxRootDistance = finiteDistances.length > 0 ? Math.max(...finiteDistances) : 0;
856
+ return {
857
+ method: "physical-component-bbox-gap-graph",
858
+ distanceMethod: "axis-aligned-bbox-gap",
859
+ options: {
860
+ contactTolerance: connectivity.options.contactTolerance,
861
+ minOverlapVolume: connectivity.options.minOverlapVolume,
862
+ rootComponentIndex
863
+ },
864
+ objectCount: connectivity.objectCount,
865
+ componentCount: connectivity.componentCount,
866
+ rootComponentIndex,
867
+ maxRootDistance,
868
+ gapEdgeCount: connectivity.components.length * (connectivity.components.length - 1) / 2,
869
+ objects,
870
+ components,
871
+ gapEdges,
872
+ connectivity: {
873
+ method: connectivity.method,
874
+ edges: connectivity.edges
875
+ },
876
+ warnings: [...connectivity.warnings]
877
+ };
878
+ }
457
879
  const CAMERA_TOKEN_DIRECTIONS = {
458
880
  front: [0, -1, 0.2],
459
881
  back: [0, 1, 0.2],
@@ -589,6 +1011,805 @@ ${body}
589
1011
  pathCount
590
1012
  };
591
1013
  }
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
+ 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
+ }
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
+ function emptyRoughnessSummary() {
1417
+ return {
1418
+ triangleCount: 0,
1419
+ edgeCount: 0,
1420
+ boundaryEdgeCount: 0,
1421
+ nonManifoldEdgeCount: 0,
1422
+ smoothAreaPercent: 0,
1423
+ moderateAreaPercent: 0,
1424
+ sharpAreaPercent: 0,
1425
+ harshAreaPercent: 0,
1426
+ roughAreaPercent: 0,
1427
+ meanAngleDeg: null,
1428
+ p50AngleDeg: null,
1429
+ p90AngleDeg: null,
1430
+ p95AngleDeg: null,
1431
+ p99AngleDeg: null,
1432
+ maxAngleDeg: null,
1433
+ qualityScore: 0
1434
+ };
1435
+ }
1436
+ function summarizeRoughnessTriangles(triangles, edgeAngles, edgeCount, boundaryEdgeCount, nonManifoldEdgeCount, options) {
1437
+ const areaByClass = {
1438
+ smooth: 0,
1439
+ moderate: 0,
1440
+ sharp: 0,
1441
+ harsh: 0
1442
+ };
1443
+ let totalArea = 0;
1444
+ for (const tri of triangles) {
1445
+ const area = Number.isFinite(tri.area) ? tri.area : 0;
1446
+ totalArea += area;
1447
+ areaByClass[roughnessClassForAngle(tri.maxAngleDeg, options)] += area;
1448
+ }
1449
+ const sortedAngles = [...edgeAngles].sort((lhs, rhs) => lhs - rhs);
1450
+ const meanAngleDeg = sortedAngles.length > 0 ? Number((sortedAngles.reduce((sum, angle) => sum + angle, 0) / sortedAngles.length).toFixed(2)) : null;
1451
+ const qualityScore = totalArea > 0 ? Math.round(
1452
+ MathUtils.clamp(
1453
+ 100 * (areaByClass.smooth + areaByClass.moderate * 0.9) / totalArea - 50 * areaByClass.harsh / totalArea,
1454
+ 0,
1455
+ 100
1456
+ )
1457
+ ) : 0;
1458
+ const safePercent = (value) => totalArea > 0 ? Number((value / totalArea * 100).toFixed(2)) : 0;
1459
+ return {
1460
+ triangleCount: triangles.length,
1461
+ edgeCount,
1462
+ boundaryEdgeCount,
1463
+ nonManifoldEdgeCount,
1464
+ smoothAreaPercent: safePercent(areaByClass.smooth),
1465
+ moderateAreaPercent: safePercent(areaByClass.moderate),
1466
+ sharpAreaPercent: safePercent(areaByClass.sharp),
1467
+ harshAreaPercent: safePercent(areaByClass.harsh),
1468
+ roughAreaPercent: safePercent(areaByClass.sharp + areaByClass.harsh),
1469
+ meanAngleDeg,
1470
+ p50AngleDeg: percentile(sortedAngles, 0.5),
1471
+ p90AngleDeg: percentile(sortedAngles, 0.9),
1472
+ p95AngleDeg: percentile(sortedAngles, 0.95),
1473
+ p99AngleDeg: percentile(sortedAngles, 0.99),
1474
+ maxAngleDeg: sortedAngles.length > 0 ? Number(sortedAngles[sortedAngles.length - 1].toFixed(2)) : null,
1475
+ qualityScore
1476
+ };
1477
+ }
1478
+ function percentile(sorted, q) {
1479
+ if (sorted.length === 0) return null;
1480
+ const index = MathUtils.clamp(Math.floor(sorted.length * q), 0, sorted.length - 1);
1481
+ return Number(sorted[index].toFixed(2));
1482
+ }
1483
+ const DEG_PER_RAD = 180 / Math.PI;
1484
+ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1485
+ const options = resolveRoughnessInspectionOptions(rawOptions);
1486
+ const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
1487
+ const position = geometry.getAttribute("position");
1488
+ const warnings = [];
1489
+ if (!position || position.count < 3) {
1490
+ return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
1491
+ }
1492
+ const triangleCount = Math.floor(position.count / 3);
1493
+ const normals = new Array(triangleCount);
1494
+ const triangles = new Array(triangleCount);
1495
+ const triangleEdgeAngles = Array.from({ length: triangleCount }, () => [0, 0, 0]);
1496
+ const edges = /* @__PURE__ */ new Map();
1497
+ const colors = new Float32Array(position.count * 3);
1498
+ const scores = new Float32Array(position.count);
1499
+ const a = new Vector3();
1500
+ const b = new Vector3();
1501
+ const c = new Vector3();
1502
+ const ac = new Vector3();
1503
+ const normal = new Vector3();
1504
+ const bbox = new Box3().setFromBufferAttribute(position);
1505
+ const snap = Math.max(1e-6, bbox.getSize(new Vector3()).length() * 1e-8);
1506
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1507
+ const offset = tri * 3;
1508
+ readVertex(position, offset, a);
1509
+ readVertex(position, offset + 1, b);
1510
+ readVertex(position, offset + 2, c);
1511
+ normal.subVectors(b, a).cross(ac.subVectors(c, a));
1512
+ const areaTwice = normal.length();
1513
+ triangles[tri] = { area: areaTwice * 0.5, maxAngleDeg: 0 };
1514
+ normals[tri] = areaTwice > 1e-12 ? normal.multiplyScalar(1 / areaTwice).clone() : new Vector3(0, 0, 1);
1515
+ const keys = [vertexKey$1(a, snap), vertexKey$1(b, snap), vertexKey$1(c, snap)];
1516
+ for (let edge = 0; edge < 3; edge += 1) {
1517
+ const key = edgeKey(keys[edge], keys[(edge + 1) % 3]);
1518
+ let record = edges.get(key);
1519
+ if (!record) {
1520
+ record = { triangles: [], triangleEdges: [] };
1521
+ edges.set(key, record);
1522
+ }
1523
+ record.triangles.push(tri);
1524
+ record.triangleEdges.push({ triangle: tri, edge });
1525
+ }
1526
+ }
1527
+ const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles);
1528
+ if (boundaryEdgeCount > 0) warnings.push(`${boundaryEdgeCount} boundary edge(s) were treated as harsh roughness.`);
1529
+ if (nonManifoldEdgeCount > 0) warnings.push(`${nonManifoldEdgeCount} non-manifold edge(s) were treated as harsh roughness.`);
1530
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1531
+ const { maxAngleDeg } = triangles[tri];
1532
+ const color = roughnessColorForAngle(maxAngleDeg, options);
1533
+ const score = roughnessScoreForAngle(maxAngleDeg, options);
1534
+ const offset = tri * 3;
1535
+ for (let vertex = 0; vertex < 3; vertex += 1) {
1536
+ const colorOffset = (offset + vertex) * 3;
1537
+ colors[colorOffset] = color[0] / 255;
1538
+ colors[colorOffset + 1] = color[1] / 255;
1539
+ colors[colorOffset + 2] = color[2] / 255;
1540
+ scores[offset + vertex] = score;
1541
+ }
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
+ });
1563
+ geometry.setAttribute("color", new BufferAttribute(colors, 3));
1564
+ geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
1565
+ geometry.computeBoundingBox();
1566
+ return {
1567
+ geometry,
1568
+ pointSamples,
1569
+ summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
1570
+ warnings
1571
+ };
1572
+ }
1573
+ function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
1574
+ const edgeAngles = [];
1575
+ let boundaryEdgeCount = 0;
1576
+ let nonManifoldEdgeCount = 0;
1577
+ for (const edge of edges.values()) {
1578
+ if (edge.triangles.length === 1) {
1579
+ boundaryEdgeCount += 1;
1580
+ markEdge(edge, triangles, triangleEdgeAngles, 180);
1581
+ edgeAngles.push(180);
1582
+ continue;
1583
+ }
1584
+ if (edge.triangles.length > 2) {
1585
+ nonManifoldEdgeCount += 1;
1586
+ markEdge(edge, triangles, triangleEdgeAngles, 180);
1587
+ edgeAngles.push(180);
1588
+ continue;
1589
+ }
1590
+ const [first, second] = edge.triangles;
1591
+ const dot = MathUtils.clamp(normals[first].dot(normals[second]), -1, 1);
1592
+ const angleDeg = Math.acos(dot) * DEG_PER_RAD;
1593
+ markEdge(edge, triangles, triangleEdgeAngles, angleDeg);
1594
+ edgeAngles.push(angleDeg);
1595
+ }
1596
+ return { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles };
1597
+ }
1598
+ function readVertex(position, index, target) {
1599
+ target.set(position.getX(index), position.getY(index), position.getZ(index));
1600
+ }
1601
+ function vertexKey$1(point, snap) {
1602
+ return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
1603
+ }
1604
+ function edgeKey(a, b) {
1605
+ return a < b ? `${a}|${b}` : `${b}|${a}`;
1606
+ }
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);
1611
+ }
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
+ }
592
1813
  const canvas = document.getElementById("canvas");
593
1814
  const exportCanvas = document.createElement("canvas");
594
1815
  const exportCtx = exportCanvas.getContext("2d");
@@ -622,6 +1843,14 @@ function createCaptureDebugLogger(enabled) {
622
1843
  console.info(`[forge-capture:debug] +${sinceStart}ms Δ${delta}ms ${phase}${detailText}`);
623
1844
  };
624
1845
  }
1846
+ class EmptyInspectionShape {
1847
+ intersect() {
1848
+ return {
1849
+ isEmpty: () => true,
1850
+ volume: () => 0
1851
+ };
1852
+ }
1853
+ }
625
1854
  const COLLISION_SOURCE_OPACITY = 0.22;
626
1855
  const COLLISION_SOURCE_COLOR = [180, 200, 220];
627
1856
  const COLLISION_HIGHLIGHT_COLOR = [255, 68, 16];
@@ -665,6 +1894,31 @@ const MASK_PALETTE = [
665
1894
  [0, 0, 128],
666
1895
  [128, 128, 128]
667
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
+ }
668
1922
  function summarizeSceneGeometry(entries) {
669
1923
  const min = [Infinity, Infinity, Infinity];
670
1924
  const max = [-Infinity, -Infinity, -Infinity];
@@ -1038,6 +2292,17 @@ function distanceColorForRootDistance(distance, maxDistance) {
1038
2292
  if (t <= 0.5) return lerpColor(DISTANCE_NEAR_COLOR, DISTANCE_MID_COLOR, t * 2);
1039
2293
  return lerpColor(DISTANCE_MID_COLOR, DISTANCE_FAR_COLOR, (t - 0.5) * 2);
1040
2294
  }
2295
+ function rgbFloats(color) {
2296
+ return [color[0] / 255, color[1] / 255, color[2] / 255];
2297
+ }
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();
2303
+ geometry.setAttribute("color", new BufferAttribute(colors, 3));
2304
+ return geometry;
2305
+ }
1041
2306
  function buildMaskEntries(session) {
1042
2307
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
1043
2308
  return session.renderables.map((renderable, idx) => {
@@ -1140,30 +2405,26 @@ function renderCurrentConnectivity(session, rawReport) {
1140
2405
  const r = getRenderer(session.size, session.pixelRatio);
1141
2406
  const report = rawReport ?? analyzeSessionConnectivity(session);
1142
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();
1143
2412
  const replacements = session.renderables.map((renderable) => {
1144
2413
  const object = byId.get(renderable.id);
1145
2414
  const color = object ? maskColorForIndex(object.componentIndex) : [128, 128, 128];
1146
- const material = new ShaderMaterial({
1147
- uniforms: {
1148
- connectivityColor: { value: new Vector3(color[0] / 255, color[1] / 255, color[2] / 255) }
1149
- },
1150
- vertexShader: `
1151
- void main() {
1152
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1153
- }
1154
- `,
1155
- fragmentShader: `
1156
- precision highp float;
1157
- uniform vec3 connectivityColor;
1158
- void main() {
1159
- gl_FragColor = vec4(connectivityColor, 1.0);
1160
- }
1161
- `
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
1162
2423
  });
1163
2424
  material.toneMapped = false;
1164
- const previousMaterial = renderable.solid.material;
2425
+ if (vertexGeometry) renderable.solid.geometry = vertexGeometry;
1165
2426
  renderable.solid.material = material;
1166
- return { renderable, previousMaterial, material };
2427
+ return { renderable, previousMaterial, previousGeometry, vertexGeometry, material };
1167
2428
  });
1168
2429
  try {
1169
2430
  const png = withSolidOnlyVisibility(
@@ -1176,8 +2437,10 @@ function renderCurrentConnectivity(session, rawReport) {
1176
2437
  );
1177
2438
  return { png, report: decorateConnectivityReport(report) };
1178
2439
  } finally {
1179
- replacements.forEach(({ renderable, previousMaterial, material }) => {
2440
+ replacements.forEach(({ renderable, previousMaterial, previousGeometry, vertexGeometry, material }) => {
1180
2441
  renderable.solid.material = previousMaterial;
2442
+ renderable.solid.geometry = previousGeometry;
2443
+ vertexGeometry == null ? void 0 : vertexGeometry.dispose();
1181
2444
  material.dispose();
1182
2445
  });
1183
2446
  }
@@ -1236,30 +2499,26 @@ function renderCurrentDistance(session) {
1236
2499
  const r = getRenderer(session.size, session.pixelRatio);
1237
2500
  const report = decorateDistanceReport(analyzeSessionDistance(session));
1238
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();
1239
2506
  const replacements = session.renderables.map((renderable) => {
1240
2507
  const object = byId.get(renderable.id);
1241
2508
  const color = (object == null ? void 0 : object.color) ?? DISTANCE_UNKNOWN_COLOR;
1242
- const material = new ShaderMaterial({
1243
- uniforms: {
1244
- distanceColor: { value: new Vector3(color[0] / 255, color[1] / 255, color[2] / 255) }
1245
- },
1246
- vertexShader: `
1247
- void main() {
1248
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1249
- }
1250
- `,
1251
- fragmentShader: `
1252
- precision highp float;
1253
- uniform vec3 distanceColor;
1254
- void main() {
1255
- gl_FragColor = vec4(distanceColor, 1.0);
1256
- }
1257
- `
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
1258
2517
  });
1259
2518
  material.toneMapped = false;
1260
- const previousMaterial = renderable.solid.material;
2519
+ if (vertexGeometry) renderable.solid.geometry = vertexGeometry;
1261
2520
  renderable.solid.material = material;
1262
- return { renderable, previousMaterial, material };
2521
+ return { renderable, previousMaterial, previousGeometry, vertexGeometry, material };
1263
2522
  });
1264
2523
  try {
1265
2524
  const png = withSolidOnlyVisibility(
@@ -1272,8 +2531,10 @@ function renderCurrentDistance(session) {
1272
2531
  );
1273
2532
  return { png, report };
1274
2533
  } finally {
1275
- replacements.forEach(({ renderable, previousMaterial, material }) => {
2534
+ replacements.forEach(({ renderable, previousMaterial, previousGeometry, vertexGeometry, material }) => {
1276
2535
  renderable.solid.material = previousMaterial;
2536
+ renderable.solid.geometry = previousGeometry;
2537
+ vertexGeometry == null ? void 0 : vertexGeometry.dispose();
1277
2538
  material.dispose();
1278
2539
  });
1279
2540
  }
@@ -1373,26 +2634,113 @@ function renderCurrentCollisions(session) {
1373
2634
  collisionMaterials.forEach((material) => material.dispose());
1374
2635
  }
1375
2636
  }
1376
- 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) {
1377
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;
1378
2720
  const options = resolveThicknessInspectionOptions(rawOptions);
1379
- const material = new MeshBasicMaterial({ vertexColors: true });
1380
- material.toneMapped = false;
2721
+ const optionsKey = inspectionOptionsKey(options);
2722
+ if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
1381
2723
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
1382
2724
  const warnings = [];
1383
2725
  const objects = [];
1384
- const replacements = session.renderables.map((renderable, index) => {
1385
- const previousMaterial = renderable.solid.material;
1386
- const previousGeometry = renderable.solid.geometry;
1387
- const analysis = analyzeThicknessGeometry(previousGeometry, options);
1388
- const bbox = analysis.geometry.boundingBox;
1389
- const summary = summarizeThicknessSamples(analysis.samples, options);
2726
+ const cloudObjects = [];
2727
+ const points = [];
2728
+ const overlays = [];
2729
+ session.renderables.forEach((renderable, index) => {
1390
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);
1391
2738
  if (analysis.warnings.length > 0) {
1392
2739
  analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
1393
2740
  }
2741
+ const objectIndex = index + 1;
1394
2742
  objects.push({
1395
- index: index + 1,
2743
+ index: objectIndex,
1396
2744
  id: renderable.id,
1397
2745
  name: renderable.name,
1398
2746
  groupName: renderable.groupName,
@@ -1401,174 +2749,154 @@ function renderCurrentThickness(session, rawOptions) {
1401
2749
  triangleCount: analysis.triangleCount,
1402
2750
  sampledTriangleCount: analysis.sampledTriangleCount,
1403
2751
  sampleStride: analysis.sampleStride,
1404
- bbox: {
1405
- min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
1406
- max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
1407
- },
2752
+ bbox,
1408
2753
  ...summary
1409
2754
  });
1410
- renderable.solid.geometry = analysis.geometry;
1411
- renderable.solid.material = material;
1412
- 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();
1413
2775
  });
1414
- try {
1415
- const png = withSolidOnlyVisibility(
1416
- session,
1417
- () => withTemporarySceneBackground(session, new Color(0), () => {
1418
- updateSdfRaymarchUniforms(session);
1419
- r.render(session.scene, session.camera);
1420
- return captureRenderedPng(session.size);
1421
- })
1422
- );
1423
- return {
1424
- png,
1425
- report: {
1426
- method: "mesh-normal-raycast",
1427
- options,
1428
- objectCount: objects.length,
1429
- objects,
1430
- warnings,
1431
- style: {
1432
- criticalColor: THICKNESS_COLORS.critical,
1433
- warningColor: THICKNESS_COLORS.warning,
1434
- okColor: THICKNESS_COLORS.ok,
1435
- thickColor: THICKNESS_COLORS.thick,
1436
- unknownColor: THICKNESS_COLORS.unknown
1437
- }
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
1438
2800
  }
1439
- };
1440
- } finally {
1441
- replacements.forEach(({ renderable, previousMaterial, previousGeometry, geometry }) => {
1442
- renderable.solid.geometry = previousGeometry;
1443
- renderable.solid.material = previousMaterial;
1444
- geometry.dispose();
1445
- });
1446
- material.dispose();
1447
- }
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 };
1448
2809
  }
1449
2810
  const ROUGHNESS_SMOOTH_OPACITY = 0.16;
1450
2811
  const ROUGHNESS_HARSH_OPACITY = 1;
1451
- function createRoughnessMaterial(clippingPlanes) {
1452
- const material = new ShaderMaterial({
1453
- transparent: true,
1454
- depthTest: true,
1455
- depthWrite: true,
1456
- clippingPlanes: clippingPlanes ?? void 0,
1457
- uniforms: {
1458
- smoothOpacity: { value: ROUGHNESS_SMOOTH_OPACITY },
1459
- harshOpacity: { value: ROUGHNESS_HARSH_OPACITY }
1460
- },
1461
- vertexShader: `
1462
- attribute vec3 color;
1463
- attribute float roughnessScore;
1464
- varying vec3 vColor;
1465
- varying float vRoughnessScore;
1466
- varying vec3 vViewNormal;
1467
-
1468
- void main() {
1469
- vColor = color;
1470
- vRoughnessScore = roughnessScore;
1471
- vViewNormal = normalize(normalMatrix * normal);
1472
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1473
- }
1474
- `,
1475
- fragmentShader: `
1476
- precision highp float;
1477
- uniform float smoothOpacity;
1478
- uniform float harshOpacity;
1479
- varying vec3 vColor;
1480
- varying float vRoughnessScore;
1481
- varying vec3 vViewNormal;
1482
-
1483
- void main() {
1484
- vec3 shadowLight = normalize(vec3(0.32, 0.42, 0.84));
1485
- float shade = 0.34 + 0.48 * max(dot(normalize(vViewNormal), shadowLight), 0.0);
1486
- vec3 smoothShadow = vec3(0.26, 0.30, 0.35) * shade;
1487
- float colorMix = smoothstep(0.04, 0.35, vRoughnessScore);
1488
- vec3 finalColor = mix(smoothShadow, vColor, colorMix);
1489
- float alpha = mix(smoothOpacity, harshOpacity, smoothstep(0.02, 0.55, vRoughnessScore));
1490
- gl_FragColor = vec4(finalColor, alpha);
1491
- }
1492
- `
1493
- });
1494
- material.toneMapped = false;
1495
- return material;
1496
- }
1497
2812
  function renderCurrentRoughness(session, rawOptions) {
1498
- 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;
1499
2818
  const options = resolveRoughnessInspectionOptions(rawOptions);
2819
+ const optionsKey = inspectionOptionsKey(options);
2820
+ if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
1500
2821
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
1501
2822
  const warnings = [];
1502
2823
  const objects = [];
1503
- const replacements = session.renderables.map((renderable, index) => {
1504
- const previousMaterial = renderable.solid.material;
1505
- const previousGeometry = renderable.solid.geometry;
2824
+ const cloudObjects = [];
2825
+ const points = [];
2826
+ const overlays = [];
2827
+ session.renderables.forEach((renderable, index) => {
1506
2828
  if (renderable.sdfRaymarch) {
1507
- const material2 = new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false });
1508
- material2.toneMapped = false;
1509
- renderable.solid.material = material2;
1510
2829
  warnings.push(`${renderable.name}: SDF raymarch objects are not included in mesh roughness inspection.`);
1511
- return { renderable, previousMaterial, previousGeometry, material: material2, geometry: null };
2830
+ return;
1512
2831
  }
1513
- const analysis = analyzeRoughnessGeometry(previousGeometry, options);
1514
- const bbox = analysis.geometry.boundingBox;
2832
+ const analysis = analyzeRoughnessGeometry(renderable.solid.geometry, options);
2833
+ const bbox = bboxFromGeometry(analysis.geometry);
1515
2834
  const sourceObject = byId.get(renderable.id);
1516
2835
  if (analysis.warnings.length > 0) {
1517
2836
  analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
1518
2837
  }
2838
+ const objectIndex = index + 1;
1519
2839
  objects.push({
1520
- index: index + 1,
2840
+ index: objectIndex,
1521
2841
  id: renderable.id,
1522
2842
  name: renderable.name,
1523
2843
  groupName: renderable.groupName,
1524
2844
  treePath: sourceObject == null ? void 0 : sourceObject.treePath,
1525
2845
  mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
1526
- bbox: {
1527
- min: bbox ? [bbox.min.x, bbox.min.y, bbox.min.z] : [0, 0, 0],
1528
- max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
1529
- },
2846
+ bbox,
1530
2847
  ...analysis.summary
1531
2848
  });
1532
- const material = createRoughnessMaterial(renderable.solidMaterial.clippingPlanes);
1533
- renderable.solid.geometry = analysis.geometry;
1534
- renderable.solid.material = material;
1535
- 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();
1536
2869
  });
1537
- try {
1538
- const png = withSolidOnlyVisibility(
1539
- session,
1540
- () => withTemporarySceneBackground(session, new Color(0), () => {
1541
- updateSdfRaymarchUniforms(session);
1542
- r.render(session.scene, session.camera);
1543
- return captureRenderedPng(session.size);
1544
- })
1545
- );
1546
- return {
1547
- png,
1548
- report: {
1549
- method: "mesh-dihedral-angle",
1550
- options,
1551
- objectCount: objects.length,
1552
- objects,
1553
- warnings,
1554
- style: {
1555
- smoothColor: ROUGHNESS_COLORS.smooth,
1556
- moderateColor: ROUGHNESS_COLORS.moderate,
1557
- sharpColor: ROUGHNESS_COLORS.sharp,
1558
- harshColor: ROUGHNESS_COLORS.harsh,
1559
- smoothOpacity: ROUGHNESS_SMOOTH_OPACITY,
1560
- harshOpacity: ROUGHNESS_HARSH_OPACITY
1561
- }
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
1562
2895
  }
1563
- };
1564
- } finally {
1565
- replacements.forEach(({ renderable, previousMaterial, previousGeometry, material, geometry }) => {
1566
- renderable.solid.geometry = previousGeometry;
1567
- renderable.solid.material = previousMaterial;
1568
- geometry == null ? void 0 : geometry.dispose();
1569
- material.dispose();
1570
- });
1571
- }
2896
+ }
2897
+ };
2898
+ session.roughnessInspection = state;
2899
+ return state;
1572
2900
  }
1573
2901
  function emptySectionSvg() {
1574
2902
  return {
@@ -2457,19 +3785,8 @@ function createSession(code, opts) {
2457
3785
  name: entry.source.name,
2458
3786
  shape: entry.shape
2459
3787
  }));
2460
- const connectivityEntries = (opts == null ? void 0 : opts.includeConnectivity) ? shapeVisibleObjs.map((entry) => {
2461
- const bb2 = entry.shape.boundingBox();
2462
- return {
2463
- id: entry.source.id,
2464
- name: entry.source.name,
2465
- shape: entry.shape,
2466
- min: [bb2.min[0], bb2.min[1], bb2.min[2]],
2467
- max: [bb2.max[0], bb2.max[1], bb2.max[2]],
2468
- groupName: entry.source.groupName,
2469
- treePath: entry.source.treePath,
2470
- mock: entry.source.mock
2471
- };
2472
- }) : [];
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) ?? [];
2473
3790
  const collisionEntries = (opts == null ? void 0 : opts.includeCollisions) ? shapeVisibleObjs.map((entry) => {
2474
3791
  const bb2 = entry.shape.boundingBox();
2475
3792
  return {
@@ -2772,10 +4089,13 @@ function createSession(code, opts) {
2772
4089
  renderables,
2773
4090
  sectionShapes,
2774
4091
  connectivityEntries,
4092
+ connectivityBodyInput,
2775
4093
  physicalConnectivity: null,
2776
4094
  distanceReport: null,
2777
4095
  collisionEntries,
2778
4096
  collisionReport: null,
4097
+ thicknessInspection: null,
4098
+ roughnessInspection: null,
2779
4099
  joints,
2780
4100
  jointCouplings,
2781
4101
  animationClips,
@@ -2880,6 +4200,8 @@ window.__forgeRender = async (code, opts) => {
2880
4200
  let collisionReport = null;
2881
4201
  let thicknessReport = null;
2882
4202
  let roughnessReport = null;
4203
+ let thicknessPointCloud = null;
4204
+ let roughnessPointCloud = null;
2883
4205
  let sectionAtlas = null;
2884
4206
  const channelViewCounts = /* @__PURE__ */ new Map();
2885
4207
  const markChannelViewStart = async (channel, view) => {
@@ -2925,6 +4247,7 @@ window.__forgeRender = async (code, opts) => {
2925
4247
  const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
2926
4248
  roughnessRenders[label] = roughness.png;
2927
4249
  roughnessReport = roughness.report;
4250
+ roughnessPointCloud = roughness.pointCloud;
2928
4251
  await markChannelViewDone("roughness", label);
2929
4252
  }
2930
4253
  if (requestedChannels.has("mask")) {
@@ -2963,6 +4286,7 @@ window.__forgeRender = async (code, opts) => {
2963
4286
  const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness);
2964
4287
  thicknessRenders[label] = thickness.png;
2965
4288
  thicknessReport = thickness.report;
4289
+ thicknessPointCloud = thickness.pointCloud;
2966
4290
  await markChannelViewDone("thickness", label);
2967
4291
  }
2968
4292
  } catch (e) {
@@ -2989,6 +4313,7 @@ window.__forgeRender = async (code, opts) => {
2989
4313
  normals: normalRenders,
2990
4314
  roughness: roughnessReport ? {
2991
4315
  ...roughnessReport,
4316
+ pointCloud: roughnessPointCloud,
2992
4317
  views: roughnessRenders
2993
4318
  } : {
2994
4319
  views: roughnessRenders
@@ -3017,6 +4342,7 @@ window.__forgeRender = async (code, opts) => {
3017
4342
  },
3018
4343
  thickness: thicknessReport ? {
3019
4344
  ...thicknessReport,
4345
+ pointCloud: thicknessPointCloud,
3020
4346
  views: thicknessRenders
3021
4347
  } : {
3022
4348
  views: thicknessRenders