forgecad 0.10.3 → 0.10.5

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 (123) hide show
  1. package/dist/assets/{AdminPage-CK7ObBz3.js → AdminPage-raksfnNA.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-Ds7Z2doN.js → BenchmarkPage-DP3RxhPs.js} +2 -2
  3. package/dist/assets/{BlogPage-DlPbpt6A.js → BlogPage-D7Dos-vl.js} +1 -1
  4. package/dist/assets/{DocsPage-vZb3b3Y0.js → DocsPage-DO1kvBns.js} +34 -43
  5. package/dist/assets/{EditorApp-HLoKfe15.js → EditorApp-DQJmcmRT.js} +51 -17
  6. package/dist/assets/{EmbedViewer--KnqBKrJ.js → EmbedViewer-DFDUhOma.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-C_LssmnA.js → LandingPageProofDriven-DbE_tp8-.js} +54 -36
  8. package/dist/assets/{LegalPage-DGsyo4n1.js → LegalPage-CominSso.js} +2 -2
  9. package/dist/assets/{PricingPage-BOE27B-R.js → PricingPage-CcVIN9yj.js} +2 -2
  10. package/dist/assets/{SettingsPage-f47cnk39.js → SettingsPage-DLWcP289.js} +1 -1
  11. package/dist/assets/{app-D6ccu2Xx.js → app-xW3hOdq9.js} +1343 -4004
  12. package/dist/assets/{backendInit-DbTkQN9J.js → backendInit-mDHk97u7.js} +12346 -3803
  13. package/dist/assets/cli/{render-BsngirjC.js → render--SIU27W_.js} +1909 -146
  14. package/dist/assets/{constructionHistoryWorker-PCwXrTDB.js → constructionHistoryWorker-uEe_Q7Kg.js} +2362 -835
  15. package/dist/assets/{evalWorker-CS63PfZu.js → evalWorker-BqyDHDcI.js} +7755 -3127
  16. package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
  17. package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
  18. package/dist/assets/{inspectWorker-Y4cOzNyA.js → inspectWorker-UXMxlcR8.js} +6550 -2943
  19. package/dist/assets/{jointPose-AMvCywzS.js → jointPose-bYMlwU3v.js} +1 -1
  20. package/dist/assets/{landing-proof-driven-ORyigZ6p.css → landing-proof-driven-_u4v_xQb.css} +71 -11
  21. package/dist/assets/{manifold-Crd_F2qx.js → manifold-BR7UYI4P.js} +1 -1
  22. package/dist/assets/{manifold-CBry38ly.js → manifold-CyOV5B9S.js} +2 -2
  23. package/dist/assets/{manifold-k2kRcc85.js → manifold-D4d5NQst.js} +1 -1
  24. package/dist/assets/{reportWorker-CWvn0CEv.js → reportWorker-DsaICZsn.js} +7320 -2827
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/AI/usage.md +17 -15
  28. package/dist/docs-raw/CLI.md +4 -2
  29. package/dist/docs-raw/component-model.md +2 -2
  30. package/dist/docs-raw/generated/assembly.md +76 -3
  31. package/dist/docs-raw/generated/concepts.md +36 -5
  32. package/dist/docs-raw/generated/core.md +185 -21
  33. package/dist/docs-raw/generated/curves.md +344 -6
  34. package/dist/docs-raw/generated/runtime-names.md +12 -12
  35. package/dist/docs-raw/generated/sketch.md +16 -3
  36. package/dist/docs-raw/guides/inspection-bundles.md +5 -3
  37. package/dist/docs-raw/guides/structural-fea.md +224 -0
  38. package/dist/docs-raw/simulation-workflow.md +1 -1
  39. package/dist/docs-raw/skills/{forgecad-make-a-model.md → forgecad-build-model.md} +18 -8
  40. package/dist/docs-raw/skills/{forgecad-spec-by-walking-through-it.md → forgecad-design-spec.md} +6 -6
  41. package/dist/docs-raw/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  42. package/{dist-skill/website/skills/forgecad-visual-spec.md → dist/docs-raw/skills/forgecad-image-prompt.md} +7 -7
  43. package/dist/docs-raw/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  44. package/{dist-skill/website/skills/forgecad-project.md → dist/docs-raw/skills/forgecad-project-sync.md} +5 -5
  45. package/dist/docs-raw/skills/{forgecad-3d-reconstruction.md → forgecad-reconstruct-cad-file.md} +7 -7
  46. package/dist/docs-raw/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  47. package/dist/docs-raw/skills/{forgecad-mujoco-verify.md → forgecad-verify-mujoco.md} +6 -6
  48. package/dist/docs-raw/skills/forgecad.md +1 -0
  49. package/dist/docs-raw/skills/index.md +9 -12
  50. package/dist/index.html +9 -9
  51. package/dist/llms.txt +7 -7
  52. package/dist/sitemap.xml +16 -16
  53. package/dist-cli/{check-compiler-HPF2T2FS.js → check-compiler-7YAHVXYM.js} +1 -1
  54. package/dist-cli/{check-query-propagation-HYSLTXAB.js → check-query-propagation-ZRR6IOJW.js} +1 -1
  55. package/dist-cli/{chunk-WLUKAW3H.js → chunk-VNM67DIV.js} +29671 -24865
  56. package/dist-cli/forgecad.js +5906 -714
  57. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  58. package/dist-skill/CONTEXT.md +853 -45
  59. package/dist-skill/SKILL.md +1 -0
  60. package/dist-skill/docs/CLI.md +4 -2
  61. package/dist-skill/docs/generated/assembly.md +73 -3
  62. package/dist-skill/docs/generated/core.md +185 -21
  63. package/dist-skill/docs/generated/curves.md +343 -6
  64. package/dist-skill/docs/generated/runtime-names.md +12 -12
  65. package/dist-skill/docs/generated/sketch.md +16 -3
  66. package/dist-skill/docs/guides/inspection-bundles.md +5 -3
  67. package/dist-skill/docs/guides/structural-fea.md +224 -0
  68. package/dist-skill/library/README.md +9 -12
  69. package/dist-skill/library/{forgecad-make-a-model → forgecad-build-model}/SKILL.md +16 -6
  70. package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/SKILL.md +4 -4
  71. package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/references/master-prompt.md +1 -1
  72. package/dist-skill/library/{forgecad-model-grader → forgecad-grade-model}/SKILL.md +6 -4
  73. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +4 -0
  74. package/dist-skill/library/{forgecad-visual-spec → forgecad-image-prompt}/SKILL.md +5 -5
  75. package/dist-skill/library/forgecad-image-prompt/agents/openai.yaml +4 -0
  76. package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/SKILL.md +4 -4
  77. package/dist-skill/library/{forgecad-project → forgecad-project-sync}/SKILL.md +3 -3
  78. package/dist-skill/library/{forgecad-3d-reconstruction → forgecad-reconstruct-cad-file}/SKILL.md +5 -5
  79. package/dist-skill/library/forgecad-reconstruct-cad-file/agents/openai.yaml +4 -0
  80. package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/SKILL.md +10 -10
  81. package/dist-skill/library/forgecad-reconstruct-from-images/agents/openai.yaml +4 -0
  82. package/dist-skill/library/{forgecad-mujoco-verify → forgecad-verify-mujoco}/SKILL.md +4 -4
  83. package/dist-skill/website/skills/{forgecad-make-a-model.md → forgecad-build-model.md} +18 -8
  84. package/dist-skill/website/skills/{forgecad-spec-by-walking-through-it.md → forgecad-design-spec.md} +6 -6
  85. package/dist-skill/website/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  86. package/{dist/docs-raw/skills/forgecad-visual-spec.md → dist-skill/website/skills/forgecad-image-prompt.md} +7 -7
  87. package/dist-skill/website/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  88. package/{dist/docs-raw/skills/forgecad-project.md → dist-skill/website/skills/forgecad-project-sync.md} +5 -5
  89. package/dist-skill/website/skills/{forgecad-3d-reconstruction.md → forgecad-reconstruct-cad-file.md} +7 -7
  90. package/dist-skill/website/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  91. package/dist-skill/website/skills/{forgecad-mujoco-verify.md → forgecad-verify-mujoco.md} +6 -6
  92. package/dist-skill/website/skills/forgecad.md +1 -0
  93. package/dist-skill/website/skills/index.md +9 -12
  94. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  95. package/examples/api/blend-full-round.forge.js +37 -0
  96. package/examples/api/blend-variable-radius.forge.js +51 -0
  97. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  98. package/examples/api/extrude-up-to-face.forge.js +47 -0
  99. package/examples/api/spoon-full-tang-handle.forge.js +148 -0
  100. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  101. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  102. package/examples/api/texture-projection.forge.js +75 -0
  103. package/examples/assets/uv-grid.png +0 -0
  104. package/package.json +4 -1
  105. package/dist/docs-raw/skills/forgecad-blockout-model.md +0 -49
  106. package/dist/docs-raw/skills/forgecad-component-model.md +0 -53
  107. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +0 -60
  108. package/dist-skill/library/forgecad-3d-reconstruction/agents/openai.yaml +0 -4
  109. package/dist-skill/library/forgecad-blockout-model/SKILL.md +0 -42
  110. package/dist-skill/library/forgecad-component-model/SKILL.md +0 -46
  111. package/dist-skill/library/forgecad-image-replicator/agents/openai.yaml +0 -4
  112. package/dist-skill/library/forgecad-model-grader/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +0 -48
  114. package/dist-skill/library/forgecad-reconstruction-benchmark/agents/openai.yaml +0 -4
  115. package/dist-skill/library/forgecad-visual-spec/agents/openai.yaml +0 -4
  116. package/dist-skill/website/skills/forgecad-blockout-model.md +0 -49
  117. package/dist-skill/website/skills/forgecad-component-model.md +0 -53
  118. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +0 -60
  119. /package/dist/assets/{landing-proof-driven-DiGqdtWa.js → landing-proof-driven-DNPRKL_p.js} +0 -0
  120. /package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/references/default-profiles.md +0 -0
  121. /package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/summarize_manifest.py +0 -0
  122. /package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/scripts/compare_images.py +0 -0
  123. /package/dist-skill/library/{forgecad-mujoco-verify → forgecad-verify-mujoco}/scripts/mujoco_verify.py +0 -0
@@ -1,8 +1,8 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { D as DoubleSide, a as Scene, bw as BoxGeometry, c1 as MeshStandardMaterial, a5 as BackSide, b2 as PointLight, M as Mesh, af as MeshBasicMaterial, cF as localAabbPlaneRelation, i as Vector2, cG as ShapeUtils, cH as analyzePhysicalConnectivity, h as Vector3, a0 as Matrix4, cI as Frustum, J as Box3, a1 as MathUtils, cJ as meshContactDataFor, cK as AabbSpatialIndex, cL as detectPhysicalContact, cM as resolveThicknessInspectionOptions, R as Raycaster, cN as thicknessColor, cO as thicknessClass, a6 as BufferAttribute, cE as initBackendForEvaluation, f as Color, b5 as COMPARISON_COLORS, aE as resolveForgeRenderStyle, bG as getRenderStylePreset, aD as setParamOverrides, bk as runScript, cp as scanProxyGridForBounds, cP as Group, b9 as shapeToGeometry, bl as MeshPhysicalMaterial, bD as AdditiveBlending, bJ as scanMaterialShellColor, cQ as createScanProxyGeometry, aU as LineBasicMaterial, bI as NormalBlending, bm as LineSegments, P as PerspectiveCamera, cm as DEFAULT_VIEW_CONFIG, bs as worldAuthorPlaneToLocal, cR as resolveSectionHatchMetrics, cv as buildGeometryComparisonPointCloud, ct as triangleSoupFromMeshes, O as OrthographicCamera, l as ShaderMaterial, bP as ZEBRA_STRIPE_FRAGMENT_SHADER, bQ as ZEBRA_STRIPE_VERTEX_SHADER, bK as ZEBRA_STRIPE_SOFTNESS, bL as ZEBRA_STRIPE_SCALE, bM as ZEBRA_LIGHT_COLOR, bN as ZEBRA_DARK_COLOR, bO as ZEBRA_ACCENT_COLOR, bE as geometryWithVisibleVertexColors, cS as intersectWithPlane, cT as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, d as SRGBColorSpace, cU as parseCameraCliSpec, cV as PMREMGenerator, aX as CanvasTexture, aY as Object3D, aZ as FogExp2, a_ as Fog, a$ as AmbientLight, b3 as DirectionalLight, b0 as HemisphereLight, aO as findJointAnimationClip, q as Plane, bT as SURFACE_FIELD_FRAGMENT_SHADER, bU as SURFACE_FIELD_VERTEX_SHADER, bS as scanMaterialLayerStyles, bR as SCAN_PROXY_LAYER_STYLES, aP as resolveJointAnimation, aQ as resolveJointViewValues, aT as BufferGeometry, cW as DEFAULT_ROUGHNESS_COLOR_SCALE, cX as PointsMaterial, cY as Points, b4 as heatPointsForSide, cZ as analyzeCollisionIntersections, c_ as serializeCollisionFinding, c$ as summarizeThicknessSamples, d0 as THICKNESS_GRADIENT_COLORS, d1 as THICKNESS_COLORS, d2 as DEFAULT_THICKNESS_COLOR_SCALE, b1 as SpotLight, c5 as CylinderGeometry, d3 as TorusGeometry, bW as CatmullRomCurve3, bX as TubeGeometry, cz as resolveScalarSceneSampleBudget, d4 as sampleColorScale, ba as buildComparisonHeatPatchGeometry, bb as EdgesGeometry, d5 as SphereGeometry, d6 as ConeGeometry, b6 as comparisonHeatDepthTest, b7 as comparisonHeatEdgeOpacity, b8 as comparisonHeatPatchOpacity, cC as comparisonCandidateContextOpacity, d7 as DEFAULT_COMPARISON_CANDIDATE_OPACITY } from "../backendInit-DbTkQN9J.js";
5
- import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-AMvCywzS.js";
4
+ import { D as DoubleSide, a as Scene, bJ as BoxGeometry, cp as MeshStandardMaterial, a5 as BackSide, bd as PointLight, M as Mesh, ab as MeshBasicMaterial, d2 as localAabbPlaneRelation, i as Vector2, d3 as ShapeUtils, d4 as analyzePhysicalConnectivity, h as Vector3, a0 as Matrix4, d5 as Frustum, J as Box3, a1 as MathUtils, d6 as meshContactDataFor, d7 as AabbSpatialIndex, d8 as detectPhysicalContact, d9 as resolveThicknessInspectionOptions, da as thicknessColor, db as thicknessClass, bf as BufferAttribute, R as Raycaster, bS as MeshBVH, c1 as acceleratedRaycast, dc as requireFiniteNumber, dd as requireIntegerAtLeast, de as requirePositiveFiniteNumber, d1 as initBackendForEvaluation, f as Color, bh as COMPARISON_COLORS, aB as resolveForgeRenderStyle, b_ as getRenderStylePreset, az as setParamOverrides, bw as runScript, cN as scanProxyGridForBounds, df as Group, bl as shapeToGeometry, bx as MeshPhysicalMaterial, bQ as AdditiveBlending, c4 as scanMaterialShellColor, bY as descriptorToThreeTexture, bZ as applyProjectedTexture, dg as createScanProxyGeometry, b3 as LineBasicMaterial, c0 as NormalBlending, by as LineSegments, P as PerspectiveCamera, cK as DEFAULT_VIEW_CONFIG, bF as worldAuthorPlaneToLocal, dh as resolveSectionHatchMetrics, cT as buildGeometryComparisonPointCloud, cR as triangleSoupFromMeshes, O as OrthographicCamera, l as ShaderMaterial, ca as ZEBRA_STRIPE_FRAGMENT_SHADER, cb as ZEBRA_STRIPE_VERTEX_SHADER, c5 as ZEBRA_STRIPE_SOFTNESS, c6 as ZEBRA_STRIPE_SCALE, c7 as ZEBRA_LIGHT_COLOR, c8 as ZEBRA_DARK_COLOR, c9 as ZEBRA_ACCENT_COLOR, bR as geometryWithVisibleVertexColors, di as intersectWithPlane, dj as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, d as SRGBColorSpace, dk as parseCameraCliSpec, dl as PMREMGenerator, b6 as CanvasTexture, b7 as Object3D, b8 as FogExp2, b9 as Fog, ba as AmbientLight, be as DirectionalLight, bb as HemisphereLight, aW as findJointAnimationClip, q as Plane, ce as SURFACE_FIELD_FRAGMENT_SHADER, cf as SURFACE_FIELD_VERTEX_SHADER, cd as scanMaterialLayerStyles, cc as SCAN_PROXY_LAYER_STYLES, aX as resolveJointAnimation, aY as resolveJointViewValues, b2 as BufferGeometry, dm as DEFAULT_ROUGHNESS_COLOR_SCALE, bT as makeColorScaleTexture, bU as colorScaleLUT, bV as makeScalarValueTexture, bW as makeInspectScalarUniforms, c2 as INSPECT_SCALAR_FRAGMENT_SHADER, c3 as INSPECT_SCALAR_VERTEX_SHADER, bg as heatPointsForSide, dn as analyzeCollisionIntersections, dp as serializeCollisionFinding, dq as summarizeThicknessSamples, dr as THICKNESS_COLORS, ds as DEFAULT_THICKNESS_COLOR_SCALE, dt as DEFAULT_INSPECT_ISOLINE_SPACING, bc as SpotLight, ct as CylinderGeometry, du as TorusGeometry, ch as CatmullRomCurve3, ci as TubeGeometry, cX as resolveScalarSceneSampleBudget, bm as buildComparisonHeatPatchGeometry, bn as EdgesGeometry, ck as DEFAULT_COLORMAP, dv as SphereGeometry, dw as ConeGeometry, bi as comparisonHeatDepthTest, bj as comparisonHeatEdgeOpacity, bk as comparisonHeatPatchOpacity, c$ as comparisonCandidateContextOpacity, dx as DEFAULT_COMPARISON_CANDIDATE_OPACITY } from "../backendInit-mDHk97u7.js";
5
+ import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-bYMlwU3v.js";
6
6
  const CAD_MATERIAL_PROPS = {
7
7
  color: 6003669,
8
8
  metalness: 0.05,
@@ -1187,12 +1187,12 @@ function planeKey(triangle, snap) {
1187
1187
  }
1188
1188
  function triangleEdgeKeys(triangle, snap) {
1189
1189
  const vertices = [vertexKey$2(triangle.a, snap), vertexKey$2(triangle.b, snap), vertexKey$2(triangle.c, snap)];
1190
- return [edgeKey$1(vertices[0], vertices[1]), edgeKey$1(vertices[1], vertices[2]), edgeKey$1(vertices[2], vertices[0])];
1190
+ return [edgeKey$2(vertices[0], vertices[1]), edgeKey$2(vertices[1], vertices[2]), edgeKey$2(vertices[2], vertices[0])];
1191
1191
  }
1192
1192
  function vertexKey$2(point, snap) {
1193
1193
  return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
1194
1194
  }
1195
- function edgeKey$1(a, b) {
1195
+ function edgeKey$2(a, b) {
1196
1196
  return a < b ? `${a}|${b}` : `${b}|${a}`;
1197
1197
  }
1198
1198
  const MIN_TRIANGLE_AREA = 1e-12;
@@ -1321,10 +1321,17 @@ function fract(value) {
1321
1321
  function clampUnit(value) {
1322
1322
  return Math.min(1 - 1e-9, Math.max(1e-9, value));
1323
1323
  }
1324
- function cloneGeometryForFaceColors(geometry) {
1324
+ function cloneGeometryForFaceColors$1(geometry) {
1325
1325
  return geometry.index ? geometry.toNonIndexed() : geometry.clone();
1326
1326
  }
1327
- function geometryMaxDimension(geometry) {
1327
+ function makeThicknessRaycastTarget(sourceGeometry, rayMaterial) {
1328
+ const geometry = sourceGeometry.clone();
1329
+ geometry.boundsTree = new MeshBVH(geometry);
1330
+ const mesh = new Mesh(geometry, rayMaterial);
1331
+ mesh.raycast = acceleratedRaycast;
1332
+ return { mesh, geometry };
1333
+ }
1334
+ function geometryMaxDimension$1(geometry) {
1328
1335
  geometry.computeBoundingBox();
1329
1336
  const box = geometry.boundingBox;
1330
1337
  if (!box) return 1;
@@ -1332,34 +1339,47 @@ function geometryMaxDimension(geometry) {
1332
1339
  box.getSize(size);
1333
1340
  return Math.max(1, size.x, size.y, size.z);
1334
1341
  }
1335
- function firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, jumpableMeshes, point, direction, epsilon, far, contactTolerance) {
1342
+ function createThicknessRaycastContext(sourceGeometry, rawOptions = {}, context = {}) {
1343
+ const options = resolveThicknessInspectionOptions(rawOptions);
1344
+ const connectedGeometries = context.connectedGeometries ?? [];
1345
+ const maxDim = Math.max(geometryMaxDimension$1(sourceGeometry), ...connectedGeometries.map(geometryMaxDimension$1));
1346
+ const epsilon = Math.max(1e-4, maxDim * 1e-6);
1347
+ const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1348
+ const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1349
+ const rayTargets = [
1350
+ makeThicknessRaycastTarget(sourceGeometry, rayMaterial),
1351
+ ...connectedGeometries.map((connectedGeometry) => makeThicknessRaycastTarget(connectedGeometry, rayMaterial))
1352
+ ];
1353
+ return {
1354
+ raycaster: new Raycaster(),
1355
+ rayTargetMeshes: rayTargets.map((target) => target.mesh),
1356
+ epsilon,
1357
+ far,
1358
+ contactTolerance: options.contactTolerance,
1359
+ dispose: () => {
1360
+ rayTargets.forEach((target) => target.geometry.dispose());
1361
+ rayMaterial.dispose();
1362
+ }
1363
+ };
1364
+ }
1365
+ function firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, point, direction, epsilon, far, contactTolerance) {
1336
1366
  const origin = point.clone().addScaledVector(direction, epsilon);
1337
1367
  raycaster.set(origin, direction);
1338
1368
  raycaster.near = epsilon;
1339
1369
  raycaster.far = far;
1340
1370
  const hits = raycaster.intersectObjects(rayTargetMeshes, false);
1371
+ const minDistance = Math.max(epsilon, contactTolerance);
1341
1372
  for (const hit of hits) {
1342
- if (hit.distance <= epsilon) continue;
1343
- if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
1373
+ if (hit.distance <= minDistance) continue;
1344
1374
  return hit.distance + epsilon;
1345
1375
  }
1346
1376
  return null;
1347
1377
  }
1348
- function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid, normal, epsilon, far, contactTolerance) {
1349
- const forward = firstOppositeSurfaceDistance(
1350
- raycaster,
1351
- rayTargetMeshes,
1352
- jumpableMeshes,
1353
- centroid,
1354
- normal,
1355
- epsilon,
1356
- far,
1357
- contactTolerance
1358
- );
1378
+ function triangleThickness(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance) {
1379
+ const forward = firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance);
1359
1380
  const backward = firstOppositeSurfaceDistance(
1360
1381
  raycaster,
1361
1382
  rayTargetMeshes,
1362
- jumpableMeshes,
1363
1383
  centroid,
1364
1384
  normal.clone().negate(),
1365
1385
  epsilon,
@@ -1370,9 +1390,20 @@ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid,
1370
1390
  if (backward == null) return forward;
1371
1391
  return Math.min(forward, backward);
1372
1392
  }
1393
+ function measureThicknessAtPoint(context, point, normal) {
1394
+ return triangleThickness(
1395
+ context.raycaster,
1396
+ context.rayTargetMeshes,
1397
+ point,
1398
+ normal,
1399
+ context.epsilon,
1400
+ context.far,
1401
+ context.contactTolerance
1402
+ );
1403
+ }
1373
1404
  function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
1374
1405
  const options = resolveThicknessInspectionOptions(rawOptions);
1375
- const geometry = cloneGeometryForFaceColors(sourceGeometry);
1406
+ const geometry = cloneGeometryForFaceColors$1(sourceGeometry);
1376
1407
  const position = geometry.getAttribute("position");
1377
1408
  if (!position || position.count < 3) {
1378
1409
  return {
@@ -1388,82 +1419,1238 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
1388
1419
  const triangleCount = Math.floor(position.count / 3);
1389
1420
  const surfaceTriangles = readSurfaceTriangles(position);
1390
1421
  const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
1391
- const connectedGeometries = context.connectedGeometries ?? [];
1392
- const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
1393
- const epsilon = Math.max(1e-4, maxDim * 1e-6);
1394
- const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1395
1422
  const colors = new Float32Array(position.count * 3);
1396
1423
  const triangleThicknessValues = new Array(triangleCount).fill(void 0);
1397
1424
  const samples = [];
1398
1425
  const pointSamples = [];
1399
1426
  const warnings = [];
1427
+ const rayContext = createThicknessRaycastContext(geometry, options, context);
1428
+ try {
1429
+ if (surfaceTriangles.length === 0) {
1430
+ warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
1431
+ } else if (surfaceSamples.length < surfaceTriangles.length) {
1432
+ warnings.push(
1433
+ `Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
1434
+ );
1435
+ }
1436
+ const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1437
+ for (const sample of surfaceSamples) {
1438
+ sampledTriangleIndexes.add(sample.triangle.index);
1439
+ const thickness = measureThicknessAtPoint(rayContext, sample.position, sample.normal);
1440
+ samples.push({ thickness, area: sample.area });
1441
+ const previous = triangleThicknessValues[sample.triangle.index];
1442
+ if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
1443
+ triangleThicknessValues[sample.triangle.index] = thickness;
1444
+ }
1445
+ pointSamples.push({
1446
+ position: [sample.position.x, sample.position.y, sample.position.z],
1447
+ normal: [sample.normal.x, sample.normal.y, sample.normal.z],
1448
+ value: thickness,
1449
+ className: thicknessClass(thickness, options),
1450
+ color: thicknessColor(thickness, options),
1451
+ area: sample.area
1452
+ });
1453
+ }
1454
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1455
+ const color = thicknessColor(triangleThicknessValues[tri], options);
1456
+ const offset = tri * 3;
1457
+ for (let vertex = 0; vertex < 3; vertex += 1) {
1458
+ const colorOffset = (offset + vertex) * 3;
1459
+ colors[colorOffset] = color[0] / 255;
1460
+ colors[colorOffset + 1] = color[1] / 255;
1461
+ colors[colorOffset + 2] = color[2] / 255;
1462
+ }
1463
+ }
1464
+ geometry.setAttribute("color", new BufferAttribute(colors, 3));
1465
+ return {
1466
+ geometry,
1467
+ samples,
1468
+ pointSamples,
1469
+ triangleCount,
1470
+ sampledTriangleCount: sampledTriangleIndexes.size,
1471
+ sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
1472
+ warnings
1473
+ };
1474
+ } finally {
1475
+ rayContext.dispose();
1476
+ }
1477
+ }
1478
+ const DEFAULT_TEXTURE_SIZE_CAP = 4096;
1479
+ const DEFAULT_TARGET_TEXEL_SPACING_FACTOR = 0.5;
1480
+ const DEFAULT_PLANAR_TARGET_TEXEL_SPACING_FACTOR = 0.12;
1481
+ const PATCH_GUTTER = 2;
1482
+ const MIN_PATCH_SIDE = 4;
1483
+ const MAX_PACK_ATTEMPTS = 8;
1484
+ const CREASE_NORMAL_DOT = Math.SQRT1_2;
1485
+ const PLANAR_COMPONENT_NORMAL_DOT = Math.cos(Math.PI / 180);
1486
+ function nextPowerOfTwo(value) {
1487
+ return 2 ** Math.ceil(Math.log2(Math.max(1, value)));
1488
+ }
1489
+ function clamp(value, min, max) {
1490
+ return Math.min(max, Math.max(min, value));
1491
+ }
1492
+ function vertexAt(position, index, out) {
1493
+ return out.fromBufferAttribute(position, index);
1494
+ }
1495
+ function triangleMetrics(position, tri, a, b, c) {
1496
+ vertexAt(position, tri * 3, a);
1497
+ vertexAt(position, tri * 3 + 1, b);
1498
+ vertexAt(position, tri * 3 + 2, c);
1499
+ const ab = b.distanceTo(a);
1500
+ const bc = c.distanceTo(b);
1501
+ const ca = a.distanceTo(c);
1502
+ const area = b.clone().sub(a).cross(c.clone().sub(a)).length() * 0.5;
1503
+ return { area, maxEdge: Math.max(ab, bc, ca) };
1504
+ }
1505
+ function buildTriangleInfos(position) {
1506
+ const triangleCount = Math.floor(position.count / 3);
1507
+ const infos = [];
1508
+ const box = new Box3();
1509
+ const a = new Vector3();
1510
+ const b = new Vector3();
1511
+ const c = new Vector3();
1512
+ const ab = new Vector3();
1513
+ const ac = new Vector3();
1514
+ for (let i = 0; i < position.count; i += 1) {
1515
+ box.expandByPoint(vertexAt(position, i, a));
1516
+ }
1517
+ const size = new Vector3();
1518
+ box.getSize(size);
1519
+ const tolerance = Math.max(size.length() * 1e-5, Number.EPSILON);
1520
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1521
+ const va = vertexAt(position, tri * 3, a).clone();
1522
+ const vb = vertexAt(position, tri * 3 + 1, b).clone();
1523
+ const vc = vertexAt(position, tri * 3 + 2, c).clone();
1524
+ const abLen = va.distanceTo(vb);
1525
+ const bcLen = vb.distanceTo(vc);
1526
+ const caLen = vc.distanceTo(va);
1527
+ ab.subVectors(vb, va);
1528
+ ac.subVectors(vc, va);
1529
+ const normal = ab.clone().cross(ac);
1530
+ const area = normal.length() * 0.5;
1531
+ normal.normalize();
1532
+ infos.push({
1533
+ tri,
1534
+ a: va,
1535
+ b: vb,
1536
+ c: vc,
1537
+ normal,
1538
+ area,
1539
+ maxEdge: Math.max(abLen, bcLen, caLen)
1540
+ });
1541
+ }
1542
+ return { infos, tolerance };
1543
+ }
1544
+ function quantizedPointKey(point, tolerance) {
1545
+ const inv = 1 / tolerance;
1546
+ return `${Math.round(point.x * inv)},${Math.round(point.y * inv)},${Math.round(point.z * inv)}`;
1547
+ }
1548
+ function edgeKey$1(left, right) {
1549
+ return left < right ? `${left}|${right}` : `${right}|${left}`;
1550
+ }
1551
+ function triangleVertexKeys(info, tolerance) {
1552
+ return [quantizedPointKey(info.a, tolerance), quantizedPointKey(info.b, tolerance), quantizedPointKey(info.c, tolerance)];
1553
+ }
1554
+ function buildTriangleAdjacency(infos, tolerance) {
1555
+ const edgeToTris = /* @__PURE__ */ new Map();
1556
+ for (const info of infos) {
1557
+ const keys = triangleVertexKeys(info, tolerance);
1558
+ for (const [left, right] of [
1559
+ [keys[0], keys[1]],
1560
+ [keys[1], keys[2]],
1561
+ [keys[2], keys[0]]
1562
+ ]) {
1563
+ const key = edgeKey$1(left, right);
1564
+ const tris = edgeToTris.get(key) ?? [];
1565
+ tris.push(info.tri);
1566
+ edgeToTris.set(key, tris);
1567
+ }
1568
+ }
1569
+ const adjacency = Array.from({ length: infos.length }, () => /* @__PURE__ */ new Set());
1570
+ for (const tris of edgeToTris.values()) {
1571
+ for (let i = 0; i < tris.length; i += 1) {
1572
+ for (let j = i + 1; j < tris.length; j += 1) {
1573
+ adjacency[tris[i]].add(tris[j]);
1574
+ adjacency[tris[j]].add(tris[i]);
1575
+ }
1576
+ }
1577
+ }
1578
+ return adjacency.map((neighbors) => [...neighbors]);
1579
+ }
1580
+ function triangleOnPlane(info, origin, normal, tolerance) {
1581
+ return Math.abs(info.a.clone().sub(origin).dot(normal)) <= tolerance && Math.abs(info.b.clone().sub(origin).dot(normal)) <= tolerance && Math.abs(info.c.clone().sub(origin).dot(normal)) <= tolerance;
1582
+ }
1583
+ function detectPlanarComponents(infos, tolerance) {
1584
+ const adjacency = buildTriangleAdjacency(infos, tolerance);
1585
+ const assigned = new Array(infos.length).fill(false);
1586
+ const components = [];
1587
+ for (const seed of infos) {
1588
+ if (assigned[seed.tri] || seed.area <= 0) continue;
1589
+ const queue = [seed.tri];
1590
+ const component = [];
1591
+ assigned[seed.tri] = true;
1592
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
1593
+ const tri = queue[cursor];
1594
+ component.push(tri);
1595
+ for (const neighborTri of adjacency[tri]) {
1596
+ if (assigned[neighborTri]) continue;
1597
+ const neighbor = infos[neighborTri];
1598
+ if (neighbor.area <= 0) continue;
1599
+ if (neighbor.normal.dot(seed.normal) < PLANAR_COMPONENT_NORMAL_DOT) continue;
1600
+ if (!triangleOnPlane(neighbor, seed.a, seed.normal, tolerance)) continue;
1601
+ assigned[neighborTri] = true;
1602
+ queue.push(neighborTri);
1603
+ }
1604
+ }
1605
+ if (component.length > 1) components.push(component);
1606
+ }
1607
+ return components;
1608
+ }
1609
+ function planarBasis(component, infos) {
1610
+ const origin = infos[component[0]].a.clone();
1611
+ const normal = new Vector3();
1612
+ let bestEdge = new Vector3(1, 0, 0);
1613
+ let bestEdgeLength = 0;
1614
+ for (const tri of component) {
1615
+ const info = infos[tri];
1616
+ normal.addScaledVector(info.normal, info.area);
1617
+ for (const [left, right] of [
1618
+ [info.a, info.b],
1619
+ [info.b, info.c],
1620
+ [info.c, info.a]
1621
+ ]) {
1622
+ const edge = right.clone().sub(left);
1623
+ const length = edge.length();
1624
+ if (length > bestEdgeLength) {
1625
+ bestEdgeLength = length;
1626
+ bestEdge = edge;
1627
+ }
1628
+ }
1629
+ }
1630
+ normal.normalize();
1631
+ const uAxis = bestEdge.sub(normal.clone().multiplyScalar(bestEdge.dot(normal)));
1632
+ if (uAxis.lengthSq() < 1e-12) {
1633
+ uAxis.copy(Math.abs(normal.z) < 0.9 ? new Vector3(0, 0, 1) : new Vector3(1, 0, 0));
1634
+ uAxis.sub(normal.clone().multiplyScalar(uAxis.dot(normal)));
1635
+ }
1636
+ uAxis.normalize();
1637
+ const vAxis = normal.clone().cross(uAxis).normalize();
1638
+ return { origin, normal, uAxis, vAxis };
1639
+ }
1640
+ function planarCoordinate(point, patch) {
1641
+ const relative = point.clone().sub(patch.origin);
1642
+ return [relative.dot(patch.uAxis), relative.dot(patch.vAxis)];
1643
+ }
1644
+ function buildPlanarPatch(component, infos, targetTexel) {
1645
+ const basis = planarBasis(component, infos);
1646
+ let minU = Infinity;
1647
+ let maxU = -Infinity;
1648
+ let minV = Infinity;
1649
+ let maxV = -Infinity;
1650
+ for (const tri of component) {
1651
+ const info = infos[tri];
1652
+ for (const point of [info.a, info.b, info.c]) {
1653
+ const [u, v] = planarCoordinate(point, basis);
1654
+ minU = Math.min(minU, u);
1655
+ maxU = Math.max(maxU, u);
1656
+ minV = Math.min(minV, v);
1657
+ maxV = Math.max(maxV, v);
1658
+ }
1659
+ }
1660
+ const spanU = Math.max(maxU - minU, targetTexel);
1661
+ const spanV = Math.max(maxV - minV, targetTexel);
1662
+ const innerU = Math.max(1, Math.ceil(spanU / targetTexel));
1663
+ const innerV = Math.max(1, Math.ceil(spanV / targetTexel));
1664
+ return {
1665
+ kind: "planar",
1666
+ tris: component,
1667
+ width: innerU + PATCH_GUTTER * 2 + 1,
1668
+ height: innerV + PATCH_GUTTER * 2 + 1,
1669
+ innerU,
1670
+ innerV,
1671
+ x: 0,
1672
+ y: 0,
1673
+ ...basis,
1674
+ minU,
1675
+ maxU,
1676
+ minV,
1677
+ maxV
1678
+ };
1679
+ }
1680
+ function buildScalarPatches(infos, planarComponents, targetTexel, planarTargetTexel) {
1681
+ const planarTriSet = new Set(planarComponents.flat());
1682
+ const patches = planarComponents.map((component) => buildPlanarPatch(component, infos, planarTargetTexel));
1683
+ for (const info of infos) {
1684
+ if (planarTriSet.has(info.tri)) continue;
1685
+ const inner = Math.max(1, Math.ceil(info.maxEdge / targetTexel));
1686
+ const side = Math.max(MIN_PATCH_SIDE, inner + PATCH_GUTTER * 2 + 1);
1687
+ patches.push({
1688
+ kind: "triangle",
1689
+ tri: info.tri,
1690
+ width: side,
1691
+ height: side,
1692
+ side,
1693
+ inner: side - PATCH_GUTTER * 2 - 1,
1694
+ x: 0,
1695
+ y: 0
1696
+ });
1697
+ }
1698
+ return patches;
1699
+ }
1700
+ function packPatches(position, sampleSpacing, options) {
1701
+ const textureSizeCap = Math.max(64, Math.floor(options.textureSizeCap ?? DEFAULT_TEXTURE_SIZE_CAP));
1702
+ const targetFactor = options.targetTexelSpacingFactor ?? DEFAULT_TARGET_TEXEL_SPACING_FACTOR;
1703
+ const planarTargetFactor = options.planarTargetTexelSpacingFactor ?? DEFAULT_PLANAR_TARGET_TEXEL_SPACING_FACTOR;
1704
+ const triangleCount = Math.floor(position.count / 3);
1705
+ const { infos, tolerance } = buildTriangleInfos(position);
1706
+ const planarComponents = detectPlanarComponents(infos, tolerance);
1707
+ let targetTexel = Math.max(1e-6, sampleSpacing * targetFactor);
1708
+ let planarTargetTexel = Math.max(1e-6, sampleSpacing * planarTargetFactor);
1709
+ let capped = false;
1710
+ for (let attempt = 0; attempt < MAX_PACK_ATTEMPTS; attempt += 1) {
1711
+ const patches = buildScalarPatches(infos, planarComponents, targetTexel, planarTargetTexel);
1712
+ const patchArea = patches.reduce((sum, patch) => sum + patch.width * patch.height, 0);
1713
+ let width = nextPowerOfTwo(Math.ceil(Math.sqrt(patchArea)));
1714
+ width = clamp(width, 256, textureSizeCap);
1715
+ const maxPatchWidth = patches.reduce((max, patch) => Math.max(max, patch.width), 0);
1716
+ const maxPatchHeight = patches.reduce((max, patch) => Math.max(max, patch.height), 0);
1717
+ const maxPatchDimension = Math.max(maxPatchWidth, maxPatchHeight);
1718
+ width = Math.max(width, Math.min(textureSizeCap, nextPowerOfTwo(maxPatchWidth)));
1719
+ let x = 0;
1720
+ let y = 0;
1721
+ let rowH = 0;
1722
+ for (const patch of [...patches].sort((left, right) => right.height - left.height || right.width - left.width)) {
1723
+ if (x + patch.width > width) {
1724
+ x = 0;
1725
+ y += rowH;
1726
+ rowH = 0;
1727
+ }
1728
+ patch.x = x;
1729
+ patch.y = y;
1730
+ x += patch.width;
1731
+ rowH = Math.max(rowH, patch.height);
1732
+ }
1733
+ const height = nextPowerOfTwo(y + rowH);
1734
+ if (maxPatchDimension <= textureSizeCap && height <= textureSizeCap) {
1735
+ const byTri = new Array(triangleCount);
1736
+ patches.forEach((patch) => {
1737
+ if (patch.kind === "planar") {
1738
+ for (const tri of patch.tris) byTri[tri] = patch;
1739
+ } else {
1740
+ byTri[patch.tri] = patch;
1741
+ }
1742
+ });
1743
+ return { patches, patchByTri: byTri, width, height, capped };
1744
+ }
1745
+ capped = true;
1746
+ const grow = Math.sqrt(Math.max(height / textureSizeCap, maxPatchDimension / textureSizeCap)) * 1.05;
1747
+ targetTexel *= grow;
1748
+ planarTargetTexel *= grow;
1749
+ }
1750
+ throw new Error(`buildThicknessSurfaceScalarField: could not pack scalar texture atlas within ${textureSizeCap}px`);
1751
+ }
1752
+ function projectToRightTriangle(u, v) {
1753
+ let uu = clamp(u, 0, 1);
1754
+ let vv = clamp(v, 0, 1);
1755
+ const sum = uu + vv;
1756
+ if (sum > 1) {
1757
+ uu /= sum;
1758
+ vv /= sum;
1759
+ }
1760
+ return [uu, vv];
1761
+ }
1762
+ function triangleParamForTexel(px, py, inner) {
1763
+ return projectToRightTriangle((px - PATCH_GUTTER) / inner, (py - PATCH_GUTTER) / inner);
1764
+ }
1765
+ function barycentric2d(px, py, ax, ay, bx, by, cx, cy) {
1766
+ const v0x = bx - ax;
1767
+ const v0y = by - ay;
1768
+ const v1x = cx - ax;
1769
+ const v1y = cy - ay;
1770
+ const v2x = px - ax;
1771
+ const v2y = py - ay;
1772
+ const d00 = v0x * v0x + v0y * v0y;
1773
+ const d01 = v0x * v1x + v0y * v1y;
1774
+ const d11 = v1x * v1x + v1y * v1y;
1775
+ const d20 = v2x * v0x + v2y * v0y;
1776
+ const d21 = v2x * v1x + v2y * v1y;
1777
+ const denom = d00 * d11 - d01 * d01;
1778
+ if (Math.abs(denom) < 1e-12) return null;
1779
+ const v = (d11 * d20 - d01 * d21) / denom;
1780
+ const w = (d00 * d21 - d01 * d20) / denom;
1781
+ return [1 - v - w, v, w];
1782
+ }
1783
+ function setPlanarVertexUv(uvs, vertexIndex, point, patch, textureWidth, textureHeight) {
1784
+ const [u, v] = planarCoordinate(point, patch);
1785
+ const spanU = Math.max(patch.maxU - patch.minU, 1e-9);
1786
+ const spanV = Math.max(patch.maxV - patch.minV, 1e-9);
1787
+ const gridU = PATCH_GUTTER + (u - patch.minU) / spanU * patch.innerU;
1788
+ const gridV = PATCH_GUTTER + (v - patch.minV) / spanV * patch.innerV;
1789
+ uvs[vertexIndex * 2] = (patch.x + gridU + 0.5) / textureWidth;
1790
+ uvs[vertexIndex * 2 + 1] = (patch.y + gridV + 0.5) / textureHeight;
1791
+ }
1792
+ function rasterizePlanarPatch(values, textureWidth, textureHeight, patch, position, measurement, measurementContext) {
1793
+ const spanU = Math.max(patch.maxU - patch.minU, 1e-9);
1794
+ const spanV = Math.max(patch.maxV - patch.minV, 1e-9);
1795
+ const a = new Vector3();
1796
+ const b = new Vector3();
1797
+ const c = new Vector3();
1798
+ const point = new Vector3();
1799
+ for (const tri of patch.tris) {
1800
+ vertexAt(position, tri * 3, a);
1801
+ vertexAt(position, tri * 3 + 1, b);
1802
+ vertexAt(position, tri * 3 + 2, c);
1803
+ const [au, av] = planarCoordinate(a, patch);
1804
+ const [bu, bv] = planarCoordinate(b, patch);
1805
+ const [cu, cv] = planarCoordinate(c, patch);
1806
+ const ax = PATCH_GUTTER + (au - patch.minU) / spanU * patch.innerU;
1807
+ const ay = PATCH_GUTTER + (av - patch.minV) / spanV * patch.innerV;
1808
+ const bx = PATCH_GUTTER + (bu - patch.minU) / spanU * patch.innerU;
1809
+ const by = PATCH_GUTTER + (bv - patch.minV) / spanV * patch.innerV;
1810
+ const cx = PATCH_GUTTER + (cu - patch.minU) / spanU * patch.innerU;
1811
+ const cy = PATCH_GUTTER + (cv - patch.minV) / spanV * patch.innerV;
1812
+ const minX = clamp(Math.floor(Math.min(ax, bx, cx)) - 1, 0, patch.width - 1);
1813
+ const maxX = clamp(Math.ceil(Math.max(ax, bx, cx)) + 1, 0, patch.width - 1);
1814
+ const minY = clamp(Math.floor(Math.min(ay, by, cy)) - 1, 0, patch.height - 1);
1815
+ const maxY = clamp(Math.ceil(Math.max(ay, by, cy)) + 1, 0, patch.height - 1);
1816
+ for (let py = minY; py <= maxY; py += 1) {
1817
+ for (let px = minX; px <= maxX; px += 1) {
1818
+ const bary = barycentric2d(px, py, ax, ay, bx, by, cx, cy);
1819
+ if (!bary) continue;
1820
+ const [wa, wb, wc] = bary;
1821
+ if (wa < -1e-6 || wb < -1e-6 || wc < -1e-6) continue;
1822
+ point.copy(a).multiplyScalar(wa).addScaledVector(b, wb).addScaledVector(c, wc);
1823
+ const value = measurement.measureAtPoint(measurementContext, point, patch.normal);
1824
+ if (value == null) continue;
1825
+ const texelX = patch.x + px;
1826
+ const texelY = patch.y + py;
1827
+ if (texelX < 0 || texelY < 0 || texelX >= textureWidth || texelY >= textureHeight) continue;
1828
+ values[texelY * textureWidth + texelX] = value;
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ function fillPatchHoles(values, width, patch, fill) {
1834
+ const minX = patch.x;
1835
+ const minY = patch.y;
1836
+ const maxX = patch.x + patch.width;
1837
+ const maxY = patch.y + patch.height;
1838
+ for (let pass = 0; pass < Math.max(8, patch.width, patch.height); pass += 1) {
1839
+ let changed = false;
1840
+ for (let y = minY; y < maxY; y += 1) {
1841
+ for (let x = minX; x < maxX; x += 1) {
1842
+ const idx = y * width + x;
1843
+ if (Number.isFinite(values[idx])) continue;
1844
+ let sum = 0;
1845
+ let count = 0;
1846
+ for (let dy = -1; dy <= 1; dy += 1) {
1847
+ for (let dx = -1; dx <= 1; dx += 1) {
1848
+ if (dx === 0 && dy === 0) continue;
1849
+ const xx = x + dx;
1850
+ const yy = y + dy;
1851
+ if (xx < minX || yy < minY || xx >= maxX || yy >= maxY) continue;
1852
+ const value = values[yy * width + xx];
1853
+ if (!Number.isFinite(value)) continue;
1854
+ sum += value;
1855
+ count += 1;
1856
+ }
1857
+ }
1858
+ if (count > 0) {
1859
+ values[idx] = sum / count;
1860
+ changed = true;
1861
+ }
1862
+ }
1863
+ }
1864
+ if (!changed) break;
1865
+ }
1866
+ for (let y = minY; y < maxY; y += 1) {
1867
+ for (let x = minX; x < maxX; x += 1) {
1868
+ const idx = y * width + x;
1869
+ if (!Number.isFinite(values[idx])) values[idx] = fill;
1870
+ }
1871
+ }
1872
+ }
1873
+ function fillUnusedTextureCells(values, fill) {
1874
+ for (let i = 0; i < values.length; i += 1) {
1875
+ if (!Number.isFinite(values[i])) values[i] = fill;
1876
+ }
1877
+ }
1878
+ function valueRange(values) {
1879
+ let min = Infinity;
1880
+ let max = -Infinity;
1881
+ for (const value of values) {
1882
+ if (!Number.isFinite(value)) continue;
1883
+ if (value < min) min = value;
1884
+ if (value > max) max = value;
1885
+ }
1886
+ return Number.isFinite(min) && Number.isFinite(max) ? { min, max } : { min: 0, max: 0 };
1887
+ }
1888
+ function sampleNearest(values, width, height, u, v) {
1889
+ const x = clamp(Math.round(u * width - 0.5), 0, width - 1);
1890
+ const y = clamp(Math.round(v * height - 0.5), 0, height - 1);
1891
+ return values[y * width + x];
1892
+ }
1893
+ function buildCreaseAwareRenderNormals(position) {
1894
+ const normals = new Float32Array(position.count * 3);
1895
+ const a = new Vector3();
1896
+ const b = new Vector3();
1897
+ const c = new Vector3();
1898
+ const face = new Vector3();
1899
+ const box = new Box3();
1900
+ for (let i = 0; i < position.count; i += 1) {
1901
+ box.expandByPoint(vertexAt(position, i, a));
1902
+ }
1903
+ const size = new Vector3();
1904
+ box.getSize(size);
1905
+ const tolerance = Math.max(size.length() * 1e-5, Number.EPSILON);
1906
+ const inv = 1 / tolerance;
1907
+ const buckets = /* @__PURE__ */ new Map();
1908
+ for (let tri = 0; tri < Math.floor(position.count / 3); tri += 1) {
1909
+ vertexAt(position, tri * 3, a);
1910
+ vertexAt(position, tri * 3 + 1, b);
1911
+ vertexAt(position, tri * 3 + 2, c);
1912
+ face.subVectors(b, a).cross(c.clone().sub(a)).normalize();
1913
+ for (let corner = 0; corner < 3; corner += 1) {
1914
+ const vi = tri * 3 + corner;
1915
+ vertexAt(position, vi, a);
1916
+ const key = `${Math.round(a.x * inv)},${Math.round(a.y * inv)},${Math.round(a.z * inv)}`;
1917
+ let groups = buckets.get(key);
1918
+ if (!groups) {
1919
+ groups = [];
1920
+ buckets.set(key, groups);
1921
+ }
1922
+ let group = groups.find((candidate) => candidate.normal.dot(face) >= CREASE_NORMAL_DOT);
1923
+ if (!group) {
1924
+ group = { normal: face.clone(), vertices: [] };
1925
+ groups.push(group);
1926
+ } else {
1927
+ group.normal.add(face).normalize();
1928
+ }
1929
+ group.vertices.push(vi);
1930
+ }
1931
+ }
1932
+ for (const groups of buckets.values()) {
1933
+ for (const group of groups) {
1934
+ for (const vi of group.vertices) {
1935
+ normals[vi * 3] = group.normal.x;
1936
+ normals[vi * 3 + 1] = group.normal.y;
1937
+ normals[vi * 3 + 2] = group.normal.z;
1938
+ }
1939
+ }
1940
+ }
1941
+ return normals;
1942
+ }
1943
+ function buildMeasuredSurfaceScalarField(sourceGeometry, rawOptions, fieldOptions = {}, context = {}, measurement) {
1944
+ const options = measurement.resolveOptions(rawOptions);
1945
+ const fieldGeometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry;
1946
+ try {
1947
+ const position = fieldGeometry.getAttribute("position");
1948
+ if (!position || position.count < 3) {
1949
+ throw new Error(`${measurement.errorPrefix}: sourceGeometry must contain triangle positions`);
1950
+ }
1951
+ const triangleCount = Math.floor(position.count / 3);
1952
+ const a = new Vector3();
1953
+ const b = new Vector3();
1954
+ const c = new Vector3();
1955
+ let surfaceArea = 0;
1956
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1957
+ surfaceArea += triangleMetrics(position, tri, a, b, c).area;
1958
+ }
1959
+ const sampleSpacing = surfaceArea > 0 ? Math.sqrt(surfaceArea / Math.max(1, options.maxSamplesPerObject)) : 1;
1960
+ const { patches, patchByTri, width, height, capped } = packPatches(position, sampleSpacing, fieldOptions);
1961
+ const values = new Float32Array(width * height);
1962
+ values.fill(NaN);
1963
+ const positions = new Float32Array(position.count * 3);
1964
+ const normals = buildCreaseAwareRenderNormals(position);
1965
+ const uvs = new Float32Array(position.count * 2);
1966
+ const index = new Uint32Array(position.count);
1967
+ const aValue = new Float32Array(position.count);
1968
+ const measurementContext = measurement.createContext(fieldGeometry, rawOptions, context);
1969
+ const point = new Vector3();
1970
+ const normal = new Vector3();
1971
+ const ab = new Vector3();
1972
+ const ac = new Vector3();
1973
+ try {
1974
+ for (let i = 0; i < position.count; i += 1) {
1975
+ vertexAt(position, i, point);
1976
+ positions[i * 3] = point.x;
1977
+ positions[i * 3 + 1] = point.y;
1978
+ positions[i * 3 + 2] = point.z;
1979
+ index[i] = i;
1980
+ }
1981
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1982
+ const patch = patchByTri[tri];
1983
+ vertexAt(position, tri * 3, a);
1984
+ vertexAt(position, tri * 3 + 1, b);
1985
+ vertexAt(position, tri * 3 + 2, c);
1986
+ if (patch.kind === "planar") {
1987
+ setPlanarVertexUv(uvs, tri * 3, a, patch, width, height);
1988
+ setPlanarVertexUv(uvs, tri * 3 + 1, b, patch, width, height);
1989
+ setPlanarVertexUv(uvs, tri * 3 + 2, c, patch, width, height);
1990
+ continue;
1991
+ }
1992
+ ab.subVectors(b, a);
1993
+ ac.subVectors(c, a);
1994
+ normal.copy(ab).cross(ac).normalize();
1995
+ uvs[tri * 6] = (patch.x + PATCH_GUTTER + 0.5) / width;
1996
+ uvs[tri * 6 + 1] = (patch.y + PATCH_GUTTER + 0.5) / height;
1997
+ uvs[tri * 6 + 2] = (patch.x + PATCH_GUTTER + patch.inner + 0.5) / width;
1998
+ uvs[tri * 6 + 3] = (patch.y + PATCH_GUTTER + 0.5) / height;
1999
+ uvs[tri * 6 + 4] = (patch.x + PATCH_GUTTER + 0.5) / width;
2000
+ uvs[tri * 6 + 5] = (patch.y + PATCH_GUTTER + patch.inner + 0.5) / height;
2001
+ for (let py = 0; py < patch.side; py += 1) {
2002
+ for (let px = 0; px < patch.side; px += 1) {
2003
+ const [u, v] = triangleParamForTexel(px, py, patch.inner);
2004
+ point.copy(a).addScaledVector(ab, u).addScaledVector(ac, v);
2005
+ const value = measurement.measureAtPoint(measurementContext, point, normal);
2006
+ const texel = (patch.y + py) * width + patch.x + px;
2007
+ if (value != null) values[texel] = value;
2008
+ }
2009
+ }
2010
+ }
2011
+ for (const patch of patches) {
2012
+ if (patch.kind === "planar") rasterizePlanarPatch(values, width, height, patch, position, measurement, measurementContext);
2013
+ }
2014
+ } finally {
2015
+ measurementContext.dispose();
2016
+ }
2017
+ const range = valueRange(values);
2018
+ for (const patch of patches) fillPatchHoles(values, width, patch, range.min);
2019
+ const finalRange = valueRange(values);
2020
+ fillUnusedTextureCells(values, finalRange.min);
2021
+ for (let i = 0; i < position.count; i += 1) {
2022
+ aValue[i] = sampleNearest(values, width, height, uvs[i * 2], uvs[i * 2 + 1]);
2023
+ }
2024
+ return {
2025
+ positions,
2026
+ normals,
2027
+ index,
2028
+ values: aValue,
2029
+ valueMin: finalRange.min,
2030
+ valueMax: finalRange.max,
2031
+ capped,
2032
+ degenerate: false,
2033
+ holeCount: 0,
2034
+ vertexCount: position.count,
2035
+ uvs,
2036
+ textureValues: values,
2037
+ textureWidth: width,
2038
+ textureHeight: height
2039
+ };
2040
+ } finally {
2041
+ if (fieldGeometry !== sourceGeometry) fieldGeometry.dispose();
2042
+ }
2043
+ }
2044
+ function buildThicknessSurfaceScalarField(sourceGeometry, rawOptions = {}, fieldOptions = {}, context = {}) {
2045
+ return buildMeasuredSurfaceScalarField(sourceGeometry, rawOptions, fieldOptions, context, {
2046
+ errorPrefix: "buildThicknessSurfaceScalarField",
2047
+ resolveOptions: resolveThicknessInspectionOptions,
2048
+ createContext: createThicknessRaycastContext,
2049
+ measureAtPoint: measureThicknessAtPoint
2050
+ });
2051
+ }
2052
+ function makeThroughThicknessTarget(sourceGeometry, rayMaterial) {
2053
+ const geometry = sourceGeometry.clone();
2054
+ const bvh = new MeshBVH(geometry);
2055
+ geometry.boundsTree = bvh;
2056
+ const mesh = new Mesh(geometry, rayMaterial);
2057
+ mesh.raycast = acceleratedRaycast;
2058
+ return { mesh, geometry, bvh };
2059
+ }
2060
+ function geometryMaxDimension(geometry) {
2061
+ geometry.computeBoundingBox();
2062
+ const box = geometry.boundingBox;
2063
+ if (!box) return 1;
2064
+ const size = new Vector3();
2065
+ box.getSize(size);
2066
+ return Math.max(1, size.x, size.y, size.z);
2067
+ }
2068
+ function resolveThroughThicknessInspectionOptions(raw = {}) {
2069
+ return resolveThicknessInspectionOptions(raw);
2070
+ }
2071
+ function createThroughThicknessContext(sourceGeometry, rawOptions = {}, context = {}) {
2072
+ const options = resolveThroughThicknessInspectionOptions(rawOptions);
2073
+ const connectedGeometries = context.connectedGeometries ?? [];
2074
+ const maxDim = Math.max(geometryMaxDimension(sourceGeometry), ...connectedGeometries.map(geometryMaxDimension));
2075
+ const epsilon = Math.max(1e-4, maxDim * 1e-6);
2076
+ const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1400
2077
  const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1401
- const rayTargets = [
1402
- { mesh: new Mesh(geometry, rayMaterial), jumpable: false },
1403
- ...connectedGeometries.map((connectedGeometry) => ({
1404
- mesh: new Mesh(connectedGeometry, rayMaterial),
1405
- jumpable: true
1406
- }))
2078
+ const targets = [
2079
+ makeThroughThicknessTarget(sourceGeometry, rayMaterial),
2080
+ ...connectedGeometries.map((geometry) => makeThroughThicknessTarget(geometry, rayMaterial))
1407
2081
  ];
1408
- const rayTargetMeshes = rayTargets.map((target) => target.mesh);
1409
- const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
1410
- const raycaster = new Raycaster();
1411
- if (surfaceTriangles.length === 0) {
1412
- warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
1413
- } else if (surfaceSamples.length < surfaceTriangles.length) {
1414
- warnings.push(
1415
- `Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
1416
- );
1417
- }
1418
- const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1419
- for (const sample of surfaceSamples) {
1420
- sampledTriangleIndexes.add(sample.triangle.index);
1421
- const thickness = triangleThickness(
1422
- raycaster,
1423
- rayTargetMeshes,
1424
- jumpableMeshes,
1425
- sample.position,
1426
- sample.normal,
1427
- epsilon,
1428
- far,
1429
- options.contactTolerance
1430
- );
1431
- samples.push({ thickness, area: sample.area });
1432
- const previous = triangleThicknessValues[sample.triangle.index];
1433
- if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
1434
- triangleThicknessValues[sample.triangle.index] = thickness;
2082
+ return {
2083
+ raycaster: new Raycaster(),
2084
+ targets,
2085
+ epsilon,
2086
+ far,
2087
+ contactTolerance: options.contactTolerance,
2088
+ minInwardDot: 1e-5,
2089
+ dispose: () => {
2090
+ targets.forEach((target) => target.geometry.dispose());
2091
+ rayMaterial.dispose();
1435
2092
  }
1436
- pointSamples.push({
1437
- position: [sample.position.x, sample.position.y, sample.position.z],
1438
- normal: [sample.normal.x, sample.normal.y, sample.normal.z],
1439
- value: thickness,
1440
- className: thicknessClass(thickness, options),
1441
- color: thicknessColor(thickness, options),
1442
- area: sample.area
2093
+ };
2094
+ }
2095
+ function firstRayHitDistance(context, point, direction) {
2096
+ const origin = point.clone().addScaledVector(direction, context.epsilon);
2097
+ context.raycaster.set(origin, direction);
2098
+ context.raycaster.near = context.epsilon;
2099
+ context.raycaster.far = context.far;
2100
+ const hits = context.raycaster.intersectObjects(
2101
+ context.targets.map((target) => target.mesh),
2102
+ false
2103
+ );
2104
+ const minDistance = Math.max(context.epsilon, context.contactTolerance);
2105
+ for (const hit of hits) {
2106
+ if (hit.distance <= minDistance) continue;
2107
+ return hit.distance + context.epsilon;
2108
+ }
2109
+ return null;
2110
+ }
2111
+ function boxDistanceToPointSq(box, point) {
2112
+ const x = point.x < box.min.x ? box.min.x - point.x : point.x > box.max.x ? point.x - box.max.x : 0;
2113
+ const y = point.y < box.min.y ? box.min.y - point.y : point.y > box.max.y ? point.y - box.max.y : 0;
2114
+ const z = point.z < box.min.z ? box.min.z - point.z : point.z > box.max.z ? point.z - box.max.z : 0;
2115
+ return x * x + y * y + z * z;
2116
+ }
2117
+ function candidateTravelsThroughMaterial(context, point, normal, candidate, candidateDistance) {
2118
+ if (candidateDistance <= Math.max(context.epsilon, context.contactTolerance)) return false;
2119
+ const direction = candidate.clone().sub(point).multiplyScalar(1 / candidateDistance);
2120
+ if (direction.dot(normal) >= -1e-5) return false;
2121
+ const firstHit = firstRayHitDistance(context, point, direction);
2122
+ if (firstHit == null) return false;
2123
+ const tolerance = Math.max(context.contactTolerance, context.epsilon * 8);
2124
+ return Math.abs(firstHit - candidateDistance) <= tolerance;
2125
+ }
2126
+ function measureThroughThicknessAtPoint(context, point, normal) {
2127
+ let bestDistanceSq = Infinity;
2128
+ const closest = new Vector3();
2129
+ const candidate = new Vector3();
2130
+ for (const target of context.targets) {
2131
+ target.bvh.shapecast({
2132
+ boundsTraverseOrder: (box) => boxDistanceToPointSq(box, point),
2133
+ intersectsBounds: (box) => boxDistanceToPointSq(box, point) < bestDistanceSq,
2134
+ intersectsTriangle: (triangle) => {
2135
+ triangle.closestPointToPoint(point, candidate);
2136
+ const distanceSq = candidate.distanceToSquared(point);
2137
+ if (distanceSq >= bestDistanceSq) return;
2138
+ const distance = Math.sqrt(distanceSq);
2139
+ if (!candidateTravelsThroughMaterial(context, point, normal, candidate, distance)) return;
2140
+ bestDistanceSq = distanceSq;
2141
+ closest.copy(candidate);
2142
+ }
1443
2143
  });
1444
2144
  }
1445
- for (let tri = 0; tri < triangleCount; tri += 1) {
1446
- const color = thicknessColor(triangleThicknessValues[tri], options);
1447
- const offset = tri * 3;
1448
- for (let vertex = 0; vertex < 3; vertex += 1) {
1449
- const colorOffset = (offset + vertex) * 3;
1450
- colors[colorOffset] = color[0] / 255;
1451
- colors[colorOffset + 1] = color[1] / 255;
1452
- colors[colorOffset + 2] = color[2] / 255;
2145
+ return Number.isFinite(bestDistanceSq) ? closest.distanceTo(point) : null;
2146
+ }
2147
+ function cloneGeometryForFaceColors(geometry) {
2148
+ return geometry.index ? geometry.toNonIndexed() : geometry.clone();
2149
+ }
2150
+ function analyzeThroughThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
2151
+ const options = resolveThroughThicknessInspectionOptions(rawOptions);
2152
+ const geometry = cloneGeometryForFaceColors(sourceGeometry);
2153
+ const position = geometry.getAttribute("position");
2154
+ if (!position || position.count < 3) {
2155
+ return {
2156
+ geometry,
2157
+ samples: [],
2158
+ pointSamples: [],
2159
+ triangleCount: 0,
2160
+ sampledTriangleCount: 0,
2161
+ sampleStride: 1,
2162
+ warnings: ["No triangle geometry."]
2163
+ };
2164
+ }
2165
+ const triangleCount = Math.floor(position.count / 3);
2166
+ const surfaceTriangles = readSurfaceTriangles(position);
2167
+ const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
2168
+ const samples = [];
2169
+ const pointSamples = [];
2170
+ const warnings = [];
2171
+ const throughContext = createThroughThicknessContext(geometry, options, context);
2172
+ try {
2173
+ if (surfaceTriangles.length === 0) {
2174
+ warnings.push("No non-degenerate triangle surface was available for through-thickness sampling.");
2175
+ } else if (surfaceSamples.length < surfaceTriangles.length) {
2176
+ warnings.push(
2177
+ `Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
2178
+ );
2179
+ }
2180
+ const sampledTriangleIndexes = /* @__PURE__ */ new Set();
2181
+ for (const sample of surfaceSamples) {
2182
+ sampledTriangleIndexes.add(sample.triangle.index);
2183
+ const throughThickness = measureThroughThicknessAtPoint(throughContext, sample.position, sample.normal);
2184
+ samples.push({ thickness: throughThickness, area: sample.area });
2185
+ pointSamples.push({
2186
+ position: [sample.position.x, sample.position.y, sample.position.z],
2187
+ normal: [sample.normal.x, sample.normal.y, sample.normal.z],
2188
+ value: throughThickness,
2189
+ className: thicknessClass(throughThickness, options),
2190
+ color: thicknessColor(throughThickness, options),
2191
+ area: sample.area
2192
+ });
2193
+ }
2194
+ return {
2195
+ geometry,
2196
+ samples,
2197
+ pointSamples,
2198
+ triangleCount,
2199
+ sampledTriangleCount: sampledTriangleIndexes.size,
2200
+ sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
2201
+ warnings
2202
+ };
2203
+ } finally {
2204
+ throughContext.dispose();
2205
+ }
2206
+ }
2207
+ const DEFAULT_VERTEX_CAP = 2e6;
2208
+ const DEFAULT_TARGET_EDGE_SPACING_FACTOR = 2;
2209
+ const DEFAULT_K = 8;
2210
+ const DEFAULT_GATE_DOT = 0.3;
2211
+ const CREASE_WELD_DOT = Math.SQRT1_2;
2212
+ const SCATTER_CELL_SPACING_FACTOR = 1.5;
2213
+ const SCATTER_RADIUS_SPACING_FACTOR = 3;
2214
+ const IDW_DISTANCE_FLOOR = 1e-9;
2215
+ const MAX_SUBDIVISION_PASSES = 24;
2216
+ function subVec(a, b) {
2217
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2218
+ }
2219
+ function crossVec(a, b) {
2220
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
2221
+ }
2222
+ function dotVec(a, b) {
2223
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2224
+ }
2225
+ function lenVec(a) {
2226
+ return Math.hypot(a[0], a[1], a[2]);
2227
+ }
2228
+ function getVert(positions, i) {
2229
+ const o = i * 3;
2230
+ return [positions[o], positions[o + 1], positions[o + 2]];
2231
+ }
2232
+ function triArea(positions, a, b, c) {
2233
+ const va = getVert(positions, a);
2234
+ return 0.5 * lenVec(crossVec(subVec(getVert(positions, b), va), subVec(getVert(positions, c), va)));
2235
+ }
2236
+ function triNormal(positions, a, b, c) {
2237
+ const va = getVert(positions, a);
2238
+ const n = crossVec(subVec(getVert(positions, b), va), subVec(getVert(positions, c), va));
2239
+ const l = lenVec(n) || 1;
2240
+ return [n[0] / l, n[1] / l, n[2] / l];
2241
+ }
2242
+ function edgeLen(positions, a, b) {
2243
+ return lenVec(subVec(getVert(positions, a), getVert(positions, b)));
2244
+ }
2245
+ function maxEdge(positions, a, b, c) {
2246
+ return Math.max(edgeLen(positions, a, b), edgeLen(positions, b, c), edgeLen(positions, c, a));
2247
+ }
2248
+ function weld(positions) {
2249
+ if (positions.length % 9 !== 0) {
2250
+ throw new Error(`weld: positions length must be a multiple of 9 (9 floats per triangle), got ${positions.length}`);
2251
+ }
2252
+ positions.length / 3;
2253
+ let minX = Infinity;
2254
+ let minY = Infinity;
2255
+ let minZ = Infinity;
2256
+ let maxX = -Infinity;
2257
+ let maxY = -Infinity;
2258
+ let maxZ = -Infinity;
2259
+ for (let i = 0; i < positions.length; i += 3) {
2260
+ const x = positions[i];
2261
+ const y = positions[i + 1];
2262
+ const z = positions[i + 2];
2263
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
2264
+ throw new Error(`weld: non-finite vertex position at float index ${i}`);
2265
+ }
2266
+ if (x < minX) minX = x;
2267
+ if (y < minY) minY = y;
2268
+ if (z < minZ) minZ = z;
2269
+ if (x > maxX) maxX = x;
2270
+ if (y > maxY) maxY = y;
2271
+ if (z > maxZ) maxZ = z;
2272
+ }
2273
+ const diagonal = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ);
2274
+ const tolerance = Math.max(diagonal * 1e-5, Number.EPSILON);
2275
+ const remap = /* @__PURE__ */ new Map();
2276
+ const out = [];
2277
+ const tris = [];
2278
+ const weldNormals = [];
2279
+ const inv = 1 / tolerance;
2280
+ const quantKey = (i) => {
2281
+ const qx = Math.round(positions[i] * inv);
2282
+ const qy = Math.round(positions[i + 1] * inv);
2283
+ const qz = Math.round(positions[i + 2] * inv);
2284
+ return `${qx},${qy},${qz}`;
2285
+ };
2286
+ for (let tri = 0; tri < positions.length; tri += 9) {
2287
+ const faceNormal = rawTriangleNormal(positions, tri);
2288
+ const idx = [];
2289
+ for (let corner = 0; corner < 3; corner += 1) {
2290
+ const o = tri + corner * 3;
2291
+ const key = quantKey(o);
2292
+ const bucket = remap.get(key);
2293
+ let vi;
2294
+ if (bucket) {
2295
+ for (const candidate of bucket) {
2296
+ if (dotVec(weldNormals[candidate], faceNormal) >= CREASE_WELD_DOT) {
2297
+ vi = candidate;
2298
+ weldNormals[candidate] = normalizedSum(weldNormals[candidate], faceNormal);
2299
+ break;
2300
+ }
2301
+ }
2302
+ }
2303
+ if (vi === void 0) {
2304
+ vi = out.length / 3;
2305
+ out.push(positions[o], positions[o + 1], positions[o + 2]);
2306
+ weldNormals.push(faceNormal);
2307
+ if (bucket) {
2308
+ bucket.push(vi);
2309
+ } else {
2310
+ remap.set(key, [vi]);
2311
+ }
2312
+ }
2313
+ idx.push(vi);
2314
+ }
2315
+ if (idx[0] !== idx[1] && idx[1] !== idx[2] && idx[2] !== idx[0]) {
2316
+ tris.push(idx[0], idx[1], idx[2]);
1453
2317
  }
1454
2318
  }
1455
- geometry.setAttribute("color", new BufferAttribute(colors, 3));
1456
- rayMaterial.dispose();
2319
+ const distinctVertexCount = out.length / 3;
2320
+ const degenerate = distinctVertexCount < 3 || tris.length === 0;
2321
+ return { positions: out, tris, degenerate };
2322
+ }
2323
+ function rawTriangleNormal(positions, triOffset) {
2324
+ const ax = positions[triOffset];
2325
+ const ay = positions[triOffset + 1];
2326
+ const az = positions[triOffset + 2];
2327
+ const bx = positions[triOffset + 3];
2328
+ const by = positions[triOffset + 4];
2329
+ const bz = positions[triOffset + 5];
2330
+ const cx = positions[triOffset + 6];
2331
+ const cy = positions[triOffset + 7];
2332
+ const cz = positions[triOffset + 8];
2333
+ const ab = [bx - ax, by - ay, bz - az];
2334
+ const ac = [cx - ax, cy - ay, cz - az];
2335
+ const n = crossVec(ab, ac);
2336
+ const length = lenVec(n);
2337
+ if (!(length > 0)) return [0, 0, 0];
2338
+ return [n[0] / length, n[1] / length, n[2] / length];
2339
+ }
2340
+ function normalizedSum(a, b) {
2341
+ const sum = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
2342
+ const length = lenVec(sum);
2343
+ if (!(length > 0)) return a;
2344
+ return [sum[0] / length, sum[1] / length, sum[2] / length];
2345
+ }
2346
+ function adaptiveSubdivide(mesh, targetEdge, vertexCap) {
2347
+ requirePositiveFiniteNumber(targetEdge, "adaptiveSubdivide targetEdge");
2348
+ requireIntegerAtLeast(vertexCap, "adaptiveSubdivide vertexCap", 3);
2349
+ const positions = mesh.positions.slice();
2350
+ let tris = mesh.tris.slice();
2351
+ let capped = false;
2352
+ const keyOf = (a, b) => a < b ? `${a}_${b}` : `${b}_${a}`;
2353
+ for (let pass = 0; pass < MAX_SUBDIVISION_PASSES; pass += 1) {
2354
+ const next = [];
2355
+ const midCache = /* @__PURE__ */ new Map();
2356
+ let changed = false;
2357
+ const getMid = (a, b) => {
2358
+ const key = keyOf(a, b);
2359
+ let mi = midCache.get(key);
2360
+ if (mi === void 0) {
2361
+ mi = positions.length / 3;
2362
+ const ao = a * 3;
2363
+ const bo = b * 3;
2364
+ positions.push(
2365
+ (positions[ao] + positions[bo]) / 2,
2366
+ (positions[ao + 1] + positions[bo + 1]) / 2,
2367
+ (positions[ao + 2] + positions[bo + 2]) / 2
2368
+ );
2369
+ midCache.set(key, mi);
2370
+ }
2371
+ return mi;
2372
+ };
2373
+ for (let t = 0; t < tris.length; t += 3) {
2374
+ const a = tris[t];
2375
+ const b = tris[t + 1];
2376
+ const c = tris[t + 2];
2377
+ const vertexCount = positions.length / 3;
2378
+ if (maxEdge(positions, a, b, c) > targetEdge && vertexCount < vertexCap) {
2379
+ const ab = getMid(a, b);
2380
+ const bc = getMid(b, c);
2381
+ const ca = getMid(c, a);
2382
+ next.push(a, ab, ca, ab, b, bc, ca, bc, c, ab, bc, ca);
2383
+ changed = true;
2384
+ } else {
2385
+ if (positions.length / 3 >= vertexCap) capped = true;
2386
+ next.push(a, b, c);
2387
+ }
2388
+ }
2389
+ tris = next;
2390
+ if (!changed) break;
2391
+ }
2392
+ return { positions, tris, capped };
2393
+ }
2394
+ function vertexNormals(mesh) {
2395
+ const vertexCount = mesh.positions.length / 3;
2396
+ const acc = new Float32Array(vertexCount * 3);
2397
+ for (let t = 0; t < mesh.tris.length; t += 3) {
2398
+ const a = mesh.tris[t];
2399
+ const b = mesh.tris[t + 1];
2400
+ const c = mesh.tris[t + 2];
2401
+ const n = triNormal(mesh.positions, a, b, c);
2402
+ const area = triArea(mesh.positions, a, b, c);
2403
+ const nx = n[0] * area;
2404
+ const ny = n[1] * area;
2405
+ const nz = n[2] * area;
2406
+ for (const vi of [a, b, c]) {
2407
+ acc[vi * 3] += nx;
2408
+ acc[vi * 3 + 1] += ny;
2409
+ acc[vi * 3 + 2] += nz;
2410
+ }
2411
+ }
2412
+ for (let i = 0; i < vertexCount; i += 1) {
2413
+ const o = i * 3;
2414
+ const l = Math.hypot(acc[o], acc[o + 1], acc[o + 2]) || 1;
2415
+ acc[o] /= l;
2416
+ acc[o + 1] /= l;
2417
+ acc[o + 2] /= l;
2418
+ }
2419
+ return acc;
2420
+ }
2421
+ function buildSampleGrid(positions, cell) {
2422
+ const grid = /* @__PURE__ */ new Map();
2423
+ const sampleCount = positions.length / 3;
2424
+ for (let i = 0; i < sampleCount; i += 1) {
2425
+ const o = i * 3;
2426
+ const key = `${Math.floor(positions[o] / cell)},${Math.floor(positions[o + 1] / cell)},${Math.floor(positions[o + 2] / cell)}`;
2427
+ let arr = grid.get(key);
2428
+ if (!arr) {
2429
+ arr = [];
2430
+ grid.set(key, arr);
2431
+ }
2432
+ arr.push(i);
2433
+ }
2434
+ return { grid, cell };
2435
+ }
2436
+ function gatherSamples(sampleGrid, p, rings, out) {
2437
+ out.length = 0;
2438
+ const { cell, grid } = sampleGrid;
2439
+ const bx = Math.floor(p[0] / cell);
2440
+ const by = Math.floor(p[1] / cell);
2441
+ const bz = Math.floor(p[2] / cell);
2442
+ for (let dx = -rings; dx <= rings; dx += 1) {
2443
+ for (let dy = -rings; dy <= rings; dy += 1) {
2444
+ for (let dz = -rings; dz <= rings; dz += 1) {
2445
+ const arr = grid.get(`${bx + dx},${by + dy},${bz + dz}`);
2446
+ if (arr) for (const i of arr) out.push(i);
2447
+ }
2448
+ }
2449
+ }
2450
+ }
2451
+ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot, normalGateMode = "signed") {
2452
+ const cell = spacing * SCATTER_CELL_SPACING_FACTOR;
2453
+ const sampleGrid = buildSampleGrid(samples.positions, cell);
2454
+ const radius = spacing * SCATTER_RADIUS_SPACING_FACTOR;
2455
+ const radius2 = radius * radius;
2456
+ const vertexCount = positions.length / 3;
2457
+ const values = new Float32Array(vertexCount);
2458
+ let holeCount = 0;
2459
+ const candidates = [];
2460
+ const scratchP = [0, 0, 0];
2461
+ const bestD2 = new Float64Array(k);
2462
+ const bestVal = new Float64Array(k);
2463
+ for (let vi = 0; vi < vertexCount; vi += 1) {
2464
+ const pi = vi * 3;
2465
+ const px = positions[pi];
2466
+ const py = positions[pi + 1];
2467
+ const pz = positions[pi + 2];
2468
+ const nx = vnormals[pi];
2469
+ const ny = vnormals[pi + 1];
2470
+ const nz = vnormals[pi + 2];
2471
+ scratchP[0] = px;
2472
+ scratchP[1] = py;
2473
+ scratchP[2] = pz;
2474
+ gatherSamples(sampleGrid, scratchP, 3, candidates);
2475
+ let count = 0;
2476
+ let worst = 0;
2477
+ let worstD2 = -Infinity;
2478
+ for (let c = 0; c < candidates.length; c += 1) {
2479
+ const si = candidates[c];
2480
+ const so = si * 3;
2481
+ const dx = samples.positions[so] - px;
2482
+ const dy = samples.positions[so + 1] - py;
2483
+ const dz = samples.positions[so + 2] - pz;
2484
+ const d2 = dx * dx + dy * dy + dz * dz;
2485
+ if (d2 > radius2) continue;
2486
+ const normalDot = samples.normals[so] * nx + samples.normals[so + 1] * ny + samples.normals[so + 2] * nz;
2487
+ if ((normalGateMode === "absolute" ? Math.abs(normalDot) : normalDot) < gateDot) continue;
2488
+ const val = samples.values[si];
2489
+ if (count < k) {
2490
+ bestD2[count] = d2;
2491
+ bestVal[count] = val;
2492
+ if (d2 > worstD2) {
2493
+ worstD2 = d2;
2494
+ worst = count;
2495
+ }
2496
+ count += 1;
2497
+ } else if (d2 < worstD2) {
2498
+ bestD2[worst] = d2;
2499
+ bestVal[worst] = val;
2500
+ worstD2 = -Infinity;
2501
+ for (let t = 0; t < k; t += 1) {
2502
+ if (bestD2[t] > worstD2) {
2503
+ worstD2 = bestD2[t];
2504
+ worst = t;
2505
+ }
2506
+ }
2507
+ }
2508
+ }
2509
+ if (count === 0) {
2510
+ holeCount += 1;
2511
+ values[vi] = NaN;
2512
+ continue;
2513
+ }
2514
+ let w = 0;
2515
+ let accum = 0;
2516
+ for (let t = 0; t < count; t += 1) {
2517
+ const ww = 1 / Math.max(bestD2[t], IDW_DISTANCE_FLOOR);
2518
+ w += ww;
2519
+ accum += ww * bestVal[t];
2520
+ }
2521
+ values[vi] = accum / w;
2522
+ }
2523
+ return { values, holeCount };
2524
+ }
2525
+ function backfillScalarFieldHoles(values, mesh) {
2526
+ const vertexCount = values.length;
2527
+ const adjacency = Array.from({ length: vertexCount }, () => []);
2528
+ for (let t = 0; t < mesh.tris.length; t += 3) {
2529
+ const a = mesh.tris[t];
2530
+ const b = mesh.tris[t + 1];
2531
+ const c = mesh.tris[t + 2];
2532
+ adjacency[a].push(b, c);
2533
+ adjacency[b].push(a, c);
2534
+ adjacency[c].push(a, b);
2535
+ }
2536
+ let frontier = [];
2537
+ for (let i = 0; i < vertexCount; i += 1) {
2538
+ if (Number.isFinite(values[i])) frontier.push(i);
2539
+ }
2540
+ if (frontier.length === 0) {
2541
+ values.fill(0);
2542
+ return;
2543
+ }
2544
+ while (frontier.length > 0) {
2545
+ const nextFrontier = [];
2546
+ for (const v of frontier) {
2547
+ const val = values[v];
2548
+ for (const n of adjacency[v]) {
2549
+ if (!Number.isFinite(values[n])) {
2550
+ values[n] = val;
2551
+ nextFrontier.push(n);
2552
+ }
2553
+ }
2554
+ }
2555
+ frontier = nextFrontier;
2556
+ }
2557
+ }
2558
+ function reconstructSurfaceScalarField(trianglePositions, samples, options = {}) {
2559
+ if (!(trianglePositions instanceof Float32Array)) {
2560
+ throw new Error("reconstructSurfaceScalarField: trianglePositions must be a Float32Array");
2561
+ }
2562
+ if (trianglePositions.length === 0 || trianglePositions.length % 9 !== 0) {
2563
+ throw new Error(
2564
+ `reconstructSurfaceScalarField: trianglePositions length must be a positive multiple of 9, got ${trianglePositions.length}`
2565
+ );
2566
+ }
2567
+ if (!(samples.positions instanceof Float32Array) || !(samples.values instanceof Float32Array) || !(samples.normals instanceof Float32Array)) {
2568
+ throw new Error("reconstructSurfaceScalarField: samples positions/values/normals must be Float32Arrays");
2569
+ }
2570
+ const sampleCount = samples.values.length;
2571
+ if (sampleCount === 0) {
2572
+ throw new Error("reconstructSurfaceScalarField: samples must be non-empty");
2573
+ }
2574
+ if (samples.positions.length !== sampleCount * 3) {
2575
+ throw new Error(
2576
+ `reconstructSurfaceScalarField: samples.positions length ${samples.positions.length} must equal values*3 (${sampleCount * 3})`
2577
+ );
2578
+ }
2579
+ if (samples.normals.length !== sampleCount * 3) {
2580
+ throw new Error(
2581
+ `reconstructSurfaceScalarField: samples.normals length ${samples.normals.length} must equal values*3 (${sampleCount * 3})`
2582
+ );
2583
+ }
2584
+ for (let i = 0; i < sampleCount; i += 1) {
2585
+ requireFiniteNumber(samples.values[i], `samples.values[${i}]`);
2586
+ }
2587
+ const vertexCap = options.vertexCap === void 0 ? DEFAULT_VERTEX_CAP : requireIntegerAtLeast(options.vertexCap, "vertexCap", 3);
2588
+ const targetEdgeSpacingFactor = options.targetEdgeSpacingFactor === void 0 ? DEFAULT_TARGET_EDGE_SPACING_FACTOR : requirePositiveFiniteNumber(options.targetEdgeSpacingFactor, "targetEdgeSpacingFactor");
2589
+ const k = options.k === void 0 ? DEFAULT_K : requireIntegerAtLeast(options.k, "k", 1);
2590
+ const gateDot = options.gateDot === void 0 ? DEFAULT_GATE_DOT : requireFiniteNumber(options.gateDot, "gateDot");
2591
+ const normalGateMode = options.normalGateMode ?? "signed";
2592
+ if (normalGateMode !== "signed" && normalGateMode !== "absolute") {
2593
+ throw new Error(`reconstructSurfaceScalarField: normalGateMode must be 'signed' or 'absolute', got '${normalGateMode}'`);
2594
+ }
2595
+ const welded = weld(trianglePositions);
2596
+ let surfaceArea = 0;
2597
+ for (let t = 0; t < welded.tris.length; t += 3) {
2598
+ surfaceArea += triArea(welded.positions, welded.tris[t], welded.tris[t + 1], welded.tris[t + 2]);
2599
+ }
2600
+ const sampleSpacing = surfaceArea > 0 ? Math.sqrt(surfaceArea / sampleCount) : 0;
2601
+ let subdivided;
2602
+ if (welded.degenerate || !(sampleSpacing > 0)) {
2603
+ subdivided = { positions: welded.positions, tris: welded.tris, capped: false };
2604
+ } else {
2605
+ const targetEdge = targetEdgeSpacingFactor * sampleSpacing;
2606
+ subdivided = adaptiveSubdivide(welded, targetEdge, vertexCap);
2607
+ }
2608
+ const normals = vertexNormals(subdivided);
2609
+ const effectiveSpacing = sampleSpacing > 0 ? sampleSpacing : 1;
2610
+ const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot, normalGateMode);
2611
+ let valueMin = Infinity;
2612
+ let valueMax = -Infinity;
2613
+ for (let i = 0; i < scatter.values.length; i += 1) {
2614
+ const v = scatter.values[i];
2615
+ if (!Number.isFinite(v)) continue;
2616
+ if (v < valueMin) valueMin = v;
2617
+ if (v > valueMax) valueMax = v;
2618
+ }
2619
+ if (!Number.isFinite(valueMin) || !Number.isFinite(valueMax)) {
2620
+ valueMin = 0;
2621
+ valueMax = 0;
2622
+ }
2623
+ backfillScalarFieldHoles(scatter.values, subdivided);
2624
+ const vertexCount = subdivided.positions.length / 3;
1457
2625
  return {
1458
- geometry,
1459
- samples,
1460
- pointSamples,
1461
- triangleCount,
1462
- sampledTriangleCount: sampledTriangleIndexes.size,
1463
- sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
1464
- warnings
2626
+ positions: Float32Array.from(subdivided.positions),
2627
+ normals,
2628
+ index: Uint32Array.from(subdivided.tris),
2629
+ values: scatter.values,
2630
+ valueMin,
2631
+ valueMax,
2632
+ capped: subdivided.capped,
2633
+ degenerate: welded.degenerate,
2634
+ holeCount: scatter.holeCount,
2635
+ vertexCount
1465
2636
  };
1466
2637
  }
2638
+ function stressSurfaceToScalarSamples(surface) {
2639
+ const positions = new Float32Array(surface.samples.length * 3);
2640
+ const normals = new Float32Array(surface.samples.length * 3);
2641
+ const values = new Float32Array(surface.samples.length);
2642
+ surface.samples.forEach((sample, index) => {
2643
+ const base = index * 3;
2644
+ positions[base] = sample.position[0];
2645
+ positions[base + 1] = sample.position[1];
2646
+ positions[base + 2] = sample.position[2];
2647
+ normals[base] = sample.normal[0];
2648
+ normals[base + 1] = sample.normal[1];
2649
+ normals[base + 2] = sample.normal[2];
2650
+ values[index] = sample.value;
2651
+ });
2652
+ return { positions, normals, values };
2653
+ }
1467
2654
  const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1468
2655
  smoothAngleDeg: 5,
1469
2656
  sharpAngleDeg: 30,
@@ -1605,13 +2792,15 @@ function percentile(sorted, q) {
1605
2792
  return Number(sorted[index].toFixed(2));
1606
2793
  }
1607
2794
  const DEG_PER_RAD = 180 / Math.PI;
2795
+ const ROUGHNESS_HOTSPOT_LIMIT = 16;
2796
+ const ROUGHNESS_HOTSPOT_RADIUS_FRACTION = 0.03;
1608
2797
  function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1609
2798
  const options = resolveRoughnessInspectionOptions(rawOptions);
1610
2799
  const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
1611
2800
  const position = geometry.getAttribute("position");
1612
2801
  const warnings = [];
1613
2802
  if (!position || position.count < 3) {
1614
- return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
2803
+ return { geometry, pointSamples: [], hotSpots: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
1615
2804
  }
1616
2805
  const triangleCount = Math.floor(position.count / 3);
1617
2806
  const normals = new Array(triangleCount);
@@ -1684,16 +2873,66 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
1684
2873
  area: sample.area
1685
2874
  };
1686
2875
  });
2876
+ const hotSpots = selectRoughnessHotSpots(pointSamples, bbox, options.sharpAngleDeg);
1687
2877
  geometry.setAttribute("color", new BufferAttribute(colors, 3));
1688
2878
  geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
1689
2879
  geometry.computeBoundingBox();
1690
2880
  return {
1691
2881
  geometry,
1692
2882
  pointSamples,
2883
+ hotSpots,
1693
2884
  summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
1694
2885
  warnings
1695
2886
  };
1696
2887
  }
2888
+ function selectRoughnessHotSpots(samples, bbox, minAngleDeg) {
2889
+ const size = bbox.getSize(new Vector3());
2890
+ const clusterRadius = Math.max(size.length() * ROUGHNESS_HOTSPOT_RADIUS_FRACTION, 1e-6);
2891
+ const clusterRadiusSq = clusterRadius * clusterRadius;
2892
+ const candidates = samples.filter((sample) => sample.value !== null && Number.isFinite(sample.value) && sample.value >= minAngleDeg).sort((lhs, rhs) => {
2893
+ const valueDelta = (rhs.value ?? -Infinity) - (lhs.value ?? -Infinity);
2894
+ if (Math.abs(valueDelta) > 1e-9) return valueDelta;
2895
+ return (rhs.area ?? 0) - (lhs.area ?? 0);
2896
+ });
2897
+ const clusters = [];
2898
+ for (const candidate of candidates) {
2899
+ const cluster = clusters.find((entry) => squaredDistance(candidate.position, entry.representative.position) <= clusterRadiusSq);
2900
+ if (cluster) {
2901
+ cluster.count += 1;
2902
+ cluster.area += candidate.area ?? 0;
2903
+ const value = candidate.value ?? -Infinity;
2904
+ if (value > cluster.peakValue) {
2905
+ cluster.peakValue = value;
2906
+ cluster.representative = candidate;
2907
+ }
2908
+ continue;
2909
+ }
2910
+ clusters.push({
2911
+ representative: candidate,
2912
+ peakValue: candidate.value ?? 0,
2913
+ count: 1,
2914
+ area: candidate.area ?? 0
2915
+ });
2916
+ }
2917
+ return clusters.sort((lhs, rhs) => {
2918
+ const valueDelta = rhs.peakValue - lhs.peakValue;
2919
+ if (Math.abs(valueDelta) > 1e-9) return valueDelta;
2920
+ if (rhs.count !== lhs.count) return rhs.count - lhs.count;
2921
+ return rhs.area - lhs.area;
2922
+ }).slice(0, ROUGHNESS_HOTSPOT_LIMIT).map((cluster, index) => ({
2923
+ ...cluster.representative,
2924
+ rank: index + 1,
2925
+ clusterRadius,
2926
+ nearbySampleCount: cluster.count,
2927
+ nearbyArea: Number(cluster.area.toFixed(6))
2928
+ }));
2929
+ }
2930
+ function squaredDistance(a, b) {
2931
+ const dx = a[0] - b[0];
2932
+ const dy = a[1] - b[1];
2933
+ const dz = a[2] - b[2];
2934
+ return dx * dx + dy * dy + dz * dz;
2935
+ }
1697
2936
  function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
1698
2937
  const edgeAngles = [];
1699
2938
  let boundaryEdgeCount = 0;
@@ -2001,6 +3240,16 @@ function createCaptureDebugLogger(enabled) {
2001
3240
  console.info(`[forge-capture:debug] +${sinceStart}ms Δ${delta}ms ${phase}${detailText}`);
2002
3241
  };
2003
3242
  }
3243
+ function setInspectProfileEnabled(enabled) {
3244
+ window.__forgeInspectProfile = enabled === true;
3245
+ }
3246
+ function inspectProfileEnabled() {
3247
+ return window.__forgeInspectProfile === true;
3248
+ }
3249
+ function inspectProfileLog(phase, detail) {
3250
+ if (!inspectProfileEnabled()) return;
3251
+ console.info(`[forge-inspect-profile] ${phase} ${JSON.stringify(detail)}`);
3252
+ }
2004
3253
  class EmptyInspectionShape {
2005
3254
  intersect() {
2006
3255
  return {
@@ -3675,28 +4924,120 @@ function bboxFromGeometry(geometry) {
3675
4924
  max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
3676
4925
  };
3677
4926
  }
3678
- function pointBuffersFromSamples(samples, colorScale, offset = 0.025) {
3679
- const positions = new Float32Array(samples.length * 3);
3680
- const colors = new Float32Array(samples.length * 3);
3681
- samples.forEach((sample, index) => {
4927
+ const SCALAR_SURFACE_VERTEX_CAP = 2e6;
4928
+ const SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR = 8;
4929
+ function scalarSamplesFromPointSamples(samples) {
4930
+ const finite = samples.filter((sample) => sample.value != null && Number.isFinite(sample.value));
4931
+ const positions = new Float32Array(finite.length * 3);
4932
+ const values = new Float32Array(finite.length);
4933
+ const normals = new Float32Array(finite.length * 3);
4934
+ finite.forEach((sample, index) => {
3682
4935
  const base = index * 3;
3683
- positions[base] = sample.position[0] + sample.normal[0] * offset;
3684
- positions[base + 1] = sample.position[1] + sample.normal[1] * offset;
3685
- positions[base + 2] = sample.position[2] + sample.normal[2] * offset;
3686
- if (colorScale && sample.value != null && Number.isFinite(sample.value)) {
3687
- const [r, g, b] = sampleColorScale(colorScale, sample.value);
3688
- colors[base] = r / 255;
3689
- colors[base + 1] = g / 255;
3690
- colors[base + 2] = b / 255;
3691
- } else {
3692
- colors[base] = sample.color[0] / 255;
3693
- colors[base + 1] = sample.color[1] / 255;
3694
- colors[base + 2] = sample.color[2] / 255;
4936
+ positions[base] = sample.position[0];
4937
+ positions[base + 1] = sample.position[1];
4938
+ positions[base + 2] = sample.position[2];
4939
+ normals[base] = sample.normal[0];
4940
+ normals[base + 1] = sample.normal[1];
4941
+ normals[base + 2] = sample.normal[2];
4942
+ values[index] = sample.value;
4943
+ });
4944
+ return { positions, values, normals };
4945
+ }
4946
+ const DEFAULT_SCALAR_FIELD_PARAMS = {
4947
+ quantizeBands: 0,
4948
+ isoEnabled: false,
4949
+ isoSpacing: 0,
4950
+ isoLineWidthPx: 1.5,
4951
+ criticalEnabled: false,
4952
+ criticalThreshold: 0,
4953
+ shadingEnabled: true
4954
+ };
4955
+ function mergeScalarFieldParams(override) {
4956
+ if (!override) return DEFAULT_SCALAR_FIELD_PARAMS;
4957
+ const merged = { ...DEFAULT_SCALAR_FIELD_PARAMS };
4958
+ if (override.quantizeBands !== void 0) {
4959
+ const bands = override.quantizeBands;
4960
+ if (!Number.isInteger(bands) || bands < 0) {
4961
+ throw new RangeError(`Heatmap band count must be a non-negative integer, got ${bands}.`);
4962
+ }
4963
+ merged.quantizeBands = bands;
4964
+ }
4965
+ if (override.isoEnabled !== void 0) {
4966
+ merged.isoEnabled = override.isoEnabled;
4967
+ }
4968
+ if (merged.isoEnabled && merged.isoSpacing <= 0) {
4969
+ merged.isoSpacing = DEFAULT_INSPECT_ISOLINE_SPACING;
4970
+ }
4971
+ if (override.isoLineWidthPx !== void 0) {
4972
+ const width = override.isoLineWidthPx;
4973
+ if (!Number.isFinite(width) || width <= 0) {
4974
+ throw new RangeError(`Heatmap isoline width must be a finite positive number of pixels, got ${width}.`);
3695
4975
  }
4976
+ merged.isoLineWidthPx = width;
4977
+ }
4978
+ return merged;
4979
+ }
4980
+ function scalarFieldColorOverride(override) {
4981
+ return {
4982
+ colormap: override == null ? void 0 : override.colormap,
4983
+ reversed: (override == null ? void 0 : override.reversed) === true
4984
+ };
4985
+ }
4986
+ function scalarSurfaceOverlayFromField(renderable, field, domain, fieldParams, colorOverride = {}) {
4987
+ const colormap = colorOverride.colormap ?? DEFAULT_COLORMAP;
4988
+ const reversed = colorOverride.reversed === true ? { reversed: true } : {};
4989
+ const colorScale = domain ? { colormap, domainMin: domain.min, domainMax: domain.max, ...reversed } : {
4990
+ colormap,
4991
+ domainMin: field.valueMin,
4992
+ domainMax: field.valueMax > field.valueMin ? field.valueMax : field.valueMin + 1,
4993
+ ...reversed
4994
+ };
4995
+ return {
4996
+ overlay: {
4997
+ renderable,
4998
+ positions: field.positions,
4999
+ normals: field.normals,
5000
+ index: field.index,
5001
+ aValue: field.values,
5002
+ uvs: "uvs" in field ? field.uvs : void 0,
5003
+ textureValues: "textureValues" in field ? field.textureValues : void 0,
5004
+ textureWidth: "textureWidth" in field ? field.textureWidth : void 0,
5005
+ textureHeight: "textureHeight" in field ? field.textureHeight : void 0,
5006
+ colorScale,
5007
+ fieldParams
5008
+ },
5009
+ capped: field.capped,
5010
+ holeCount: field.holeCount
5011
+ };
5012
+ }
5013
+ function buildScalarSurfaceOverlayFromSamples(renderable, trianglePositions, scalarSamples, domain, fieldParams, fieldOptions = {}, colorOverride = {}) {
5014
+ const started = performance.now();
5015
+ if (scalarSamples.values.length === 0) return null;
5016
+ const reconstructStarted = performance.now();
5017
+ const field = reconstructSurfaceScalarField(trianglePositions, scalarSamples, {
5018
+ vertexCap: SCALAR_SURFACE_VERTEX_CAP,
5019
+ targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR,
5020
+ ...fieldOptions
3696
5021
  });
3697
- return { positions, colors };
5022
+ const reconstructMs = performance.now() - reconstructStarted;
5023
+ inspectProfileLog("scalar-overlay", {
5024
+ object: renderable.name,
5025
+ samples: scalarSamples.values.length,
5026
+ inputTriangles: trianglePositions.length / 9,
5027
+ vertices: field.vertexCount,
5028
+ capped: field.capped,
5029
+ holes: field.holeCount,
5030
+ reconstructMs: Number(reconstructMs.toFixed(1)),
5031
+ totalMs: Number((performance.now() - started).toFixed(1))
5032
+ });
5033
+ return scalarSurfaceOverlayFromField(renderable, field, domain, fieldParams, colorOverride);
5034
+ }
5035
+ function buildScalarSurfaceOverlay(renderable, trianglePositions, samples, domain, fieldParams) {
5036
+ const scalarSamples = scalarSamplesFromPointSamples(samples);
5037
+ return buildScalarSurfaceOverlayFromSamples(renderable, trianglePositions, scalarSamples, domain, fieldParams);
3698
5038
  }
3699
- function renderScalarPointOverlays(session, overlays) {
5039
+ function renderScalarSurfaceOverlays(session, overlays) {
5040
+ const started = performance.now();
3700
5041
  const r = getRenderer(session.size, session.pixelRatio);
3701
5042
  const overlayById = new Map(overlays.map((overlay) => [overlay.renderable.id, overlay]));
3702
5043
  const replacements = session.renderables.map((renderable) => {
@@ -3713,49 +5054,98 @@ function renderScalarPointOverlays(session, overlays) {
3713
5054
  ghostMaterial.toneMapped = false;
3714
5055
  renderable.solid.material = ghostMaterial;
3715
5056
  const overlay = overlayById.get(renderable.id);
3716
- if (!overlay || overlay.positions.length === 0)
3717
- return { renderable, previousMaterial, ghostMaterial, points: null, geometry: null, material: null };
5057
+ const previousVisible = renderable.solid.visible;
5058
+ if (overlay) renderable.solid.visible = false;
5059
+ if (!overlay || overlay.positions.length === 0) {
5060
+ return {
5061
+ renderable,
5062
+ previousMaterial,
5063
+ previousVisible,
5064
+ ghostMaterial,
5065
+ mesh: null,
5066
+ geometry: null,
5067
+ material: null,
5068
+ lut: null,
5069
+ scalarTexture: null
5070
+ };
5071
+ }
3718
5072
  const geometry = new BufferGeometry();
3719
5073
  geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
3720
- geometry.setAttribute("color", new BufferAttribute(overlay.colors, 3));
3721
- const material = new PointsMaterial({
3722
- size: 3,
3723
- sizeAttenuation: false,
3724
- vertexColors: true,
3725
- depthTest: true,
3726
- depthWrite: false,
3727
- clippingPlanes: renderable.solidMaterial.clippingPlanes ?? void 0
5074
+ geometry.setAttribute("normal", new BufferAttribute(overlay.normals, 3));
5075
+ geometry.setAttribute("aValue", new BufferAttribute(overlay.aValue, 1));
5076
+ geometry.setAttribute("uv", new BufferAttribute(overlay.uvs ?? new Float32Array(overlay.positions.length / 3 * 2), 2));
5077
+ geometry.setIndex(new BufferAttribute(overlay.index, 1));
5078
+ const lut = makeColorScaleTexture(colorScaleLUT(overlay.colorScale.colormap));
5079
+ trackTextureDecode(lut);
5080
+ const scalarTexture = overlay.textureValues && overlay.textureWidth && overlay.textureHeight ? makeScalarValueTexture(overlay.textureValues, overlay.textureWidth, overlay.textureHeight) : null;
5081
+ if (scalarTexture) trackTextureDecode(scalarTexture);
5082
+ const uniforms = makeInspectScalarUniforms({
5083
+ colorScale: lut,
5084
+ scalarTexture,
5085
+ domainMin: overlay.colorScale.domainMin,
5086
+ domainMax: overlay.colorScale.domainMax,
5087
+ colorScaleReversed: overlay.colorScale.reversed === true,
5088
+ quantizeBands: overlay.fieldParams.quantizeBands,
5089
+ isoEnabled: overlay.fieldParams.isoEnabled,
5090
+ isoSpacing: overlay.fieldParams.isoSpacing,
5091
+ isoLineWidthPx: overlay.fieldParams.isoLineWidthPx,
5092
+ criticalEnabled: overlay.fieldParams.criticalEnabled,
5093
+ criticalThreshold: overlay.fieldParams.criticalThreshold,
5094
+ shadingEnabled: overlay.fieldParams.shadingEnabled
5095
+ });
5096
+ const clippingPlanes = renderable.solidMaterial.clippingPlanes ?? void 0;
5097
+ const material = new ShaderMaterial({
5098
+ vertexShader: INSPECT_SCALAR_VERTEX_SHADER,
5099
+ fragmentShader: INSPECT_SCALAR_FRAGMENT_SHADER,
5100
+ uniforms,
5101
+ side: DoubleSide,
5102
+ clippingPlanes,
5103
+ // Engage the shader's `#include <clipping_planes_*>` chunks only when section
5104
+ // planes are actually present (NUM_CLIPPING_PLANES define is gated on this),
5105
+ // so heatmap surfaces honor section cuts the same as the ghosted solid does.
5106
+ clipping: Boolean(clippingPlanes && clippingPlanes.length > 0)
3728
5107
  });
3729
5108
  material.toneMapped = false;
3730
- const points = new Points(geometry, material);
3731
- points.renderOrder = 5;
3732
- points.raycast = () => null;
3733
- renderable.root.add(points);
3734
- return { renderable, previousMaterial, ghostMaterial, points, geometry, material };
5109
+ const mesh = new Mesh(geometry, material);
5110
+ mesh.renderOrder = 5;
5111
+ mesh.raycast = () => null;
5112
+ renderable.root.add(mesh);
5113
+ return { renderable, previousMaterial, previousVisible, ghostMaterial, mesh, geometry, material, lut, scalarTexture };
3735
5114
  });
3736
5115
  try {
3737
- return withSolidOnlyVisibility(
5116
+ const png = withSolidOnlyVisibility(
3738
5117
  session,
3739
5118
  () => withTemporarySceneBackground(session, new Color(0), () => {
3740
5119
  r.render(session.scene, session.camera);
3741
5120
  return captureRenderedPng(session.size);
3742
5121
  })
3743
5122
  );
3744
- } finally {
3745
- replacements.forEach(({ renderable, previousMaterial, ghostMaterial, points, geometry, material }) => {
3746
- if (points) renderable.root.remove(points);
3747
- renderable.solid.material = previousMaterial;
3748
- ghostMaterial.dispose();
3749
- geometry == null ? void 0 : geometry.dispose();
3750
- material == null ? void 0 : material.dispose();
5123
+ inspectProfileLog("scalar-render", {
5124
+ overlays: overlays.length,
5125
+ totalMs: Number((performance.now() - started).toFixed(1))
3751
5126
  });
5127
+ return png;
5128
+ } finally {
5129
+ replacements.forEach(
5130
+ ({ renderable, previousMaterial, previousVisible, ghostMaterial, mesh, geometry, material, lut, scalarTexture }) => {
5131
+ if (mesh) renderable.root.remove(mesh);
5132
+ renderable.solid.material = previousMaterial;
5133
+ renderable.solid.visible = previousVisible;
5134
+ ghostMaterial.dispose();
5135
+ geometry == null ? void 0 : geometry.dispose();
5136
+ material == null ? void 0 : material.dispose();
5137
+ lut == null ? void 0 : lut.dispose();
5138
+ scalarTexture == null ? void 0 : scalarTexture.dispose();
5139
+ }
5140
+ );
3752
5141
  }
3753
5142
  }
3754
- function getSessionThicknessInspection(session, rawOptions) {
5143
+ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
3755
5144
  var _a;
3756
5145
  const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
3757
5146
  const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
3758
- const optionsKey = inspectionOptionsKey({ options, sampleBudget });
5147
+ const fieldParams = { ...mergeScalarFieldParams(fieldOverride), shadingEnabled: false };
5148
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
3759
5149
  if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
3760
5150
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
3761
5151
  const warnings = [];
@@ -3763,17 +5153,30 @@ function getSessionThicknessInspection(session, rawOptions) {
3763
5153
  const thicknessColorScale = {
3764
5154
  colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
3765
5155
  domainMin: options.colorMinThickness,
3766
- domainMax: options.colorMaxThickness
5156
+ domainMax: options.colorMaxThickness,
5157
+ ...{ reversed: true }
3767
5158
  };
3768
5159
  const objects = [];
3769
5160
  const cloudObjects = [];
3770
5161
  const points = [];
3771
5162
  const overlays = [];
5163
+ const connectivityStarted = performance.now();
3772
5164
  const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
5165
+ inspectProfileLog("thickness-connectivity", {
5166
+ ms: Number((performance.now() - connectivityStarted).toFixed(1)),
5167
+ neighborSets: raycastConnectivity.neighborIdsByObjectId.size
5168
+ });
3773
5169
  session.renderables.forEach((renderable, index) => {
3774
5170
  const sourceObject = byId.get(renderable.id);
3775
- const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, {
3776
- connectedGeometries: connectedThicknessGeometriesFor(raycastConnectivity, renderable)
5171
+ const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
5172
+ const analysisStarted = performance.now();
5173
+ const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, { connectedGeometries });
5174
+ inspectProfileLog("thickness-analysis", {
5175
+ object: renderable.name,
5176
+ samples: analysis.pointSamples.length,
5177
+ triangles: analysis.triangleCount,
5178
+ connectedGeometries: connectedGeometries.length,
5179
+ ms: Number((performance.now() - analysisStarted).toFixed(1))
3777
5180
  });
3778
5181
  const bbox = bboxFromGeometry(analysis.geometry);
3779
5182
  const summary = summarizeThicknessSamples(analysis.samples, options);
@@ -3812,7 +5215,34 @@ function getSessionThicknessInspection(session, rawOptions) {
3812
5215
  ...sample
3813
5216
  });
3814
5217
  });
3815
- overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples, thicknessColorScale) });
5218
+ if (analysis.pointSamples.length > 0) {
5219
+ const scalarStarted = performance.now();
5220
+ const field = buildThicknessSurfaceScalarField(analysis.geometry, options, {}, { connectedGeometries });
5221
+ inspectProfileLog("thickness-scalar-overlay", {
5222
+ object: renderable.name,
5223
+ vertices: field.vertexCount,
5224
+ capped: field.capped,
5225
+ holes: field.holeCount,
5226
+ ms: Number((performance.now() - scalarStarted).toFixed(1))
5227
+ });
5228
+ const built = scalarSurfaceOverlayFromField(
5229
+ renderable,
5230
+ field,
5231
+ { min: thicknessColorScale.domainMin, max: thicknessColorScale.domainMax },
5232
+ fieldParams
5233
+ );
5234
+ overlays.push(built.overlay);
5235
+ if (built.capped) {
5236
+ warnings.push(
5237
+ `${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
5238
+ );
5239
+ }
5240
+ if (built.holeCount > 0) {
5241
+ warnings.push(
5242
+ `${renderable.name}: ${built.holeCount} scalar texture samples had ambiguous edge thickness (filled from neighboring texels).`
5243
+ );
5244
+ }
5245
+ }
3816
5246
  analysis.geometry.dispose();
3817
5247
  });
3818
5248
  const state = {
@@ -3835,9 +5265,6 @@ function getSessionThicknessInspection(session, rawOptions) {
3835
5265
  objects,
3836
5266
  warnings,
3837
5267
  style: {
3838
- // gradientColors is the legacy 'thickness-classic' rainbow, kept for
3839
- // back-compat. colorScale is the truth the viewport/CLI now render with.
3840
- gradientColors: THICKNESS_GRADIENT_COLORS.map((color) => [...color]),
3841
5268
  colorScale: thicknessColorScale,
3842
5269
  colorMinThickness: options.colorMinThickness,
3843
5270
  colorMaxThickness: options.colorMaxThickness,
@@ -3848,26 +5275,228 @@ function getSessionThicknessInspection(session, rawOptions) {
3848
5275
  session.thicknessInspection = state;
3849
5276
  return state;
3850
5277
  }
3851
- function renderCurrentThickness(session, rawOptions) {
3852
- const state = getSessionThicknessInspection(session, rawOptions);
3853
- return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
5278
+ function renderCurrentThickness(session, rawOptions, fieldOverride) {
5279
+ const state = getSessionThicknessInspection(session, rawOptions, fieldOverride);
5280
+ return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
5281
+ }
5282
+ function getSessionThroughThicknessInspection(session, rawOptions, fieldOverride) {
5283
+ var _a;
5284
+ const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
5285
+ const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
5286
+ const fieldParams = { ...mergeScalarFieldParams(fieldOverride), shadingEnabled: false };
5287
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
5288
+ if (((_a = session.throughThicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.throughThicknessInspection;
5289
+ const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
5290
+ const warnings = [];
5291
+ maybePushSceneSampleBudgetWarning(warnings, "Minimum solid span", sampleBudget);
5292
+ const colorScale = {
5293
+ colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
5294
+ domainMin: options.colorMinThickness,
5295
+ domainMax: options.colorMaxThickness,
5296
+ ...{ reversed: true }
5297
+ };
5298
+ const objects = [];
5299
+ const cloudObjects = [];
5300
+ const points = [];
5301
+ const overlays = [];
5302
+ const connectivityStarted = performance.now();
5303
+ const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
5304
+ inspectProfileLog("through-thickness-connectivity", {
5305
+ ms: Number((performance.now() - connectivityStarted).toFixed(1)),
5306
+ neighborSets: raycastConnectivity.neighborIdsByObjectId.size
5307
+ });
5308
+ session.renderables.forEach((renderable, index) => {
5309
+ const sourceObject = byId.get(renderable.id);
5310
+ const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
5311
+ const analysisStarted = performance.now();
5312
+ const analysis = analyzeThroughThicknessGeometry(renderable.solid.geometry, options, { connectedGeometries });
5313
+ inspectProfileLog("through-thickness-analysis", {
5314
+ object: renderable.name,
5315
+ samples: analysis.pointSamples.length,
5316
+ triangles: analysis.triangleCount,
5317
+ connectedGeometries: connectedGeometries.length,
5318
+ ms: Number((performance.now() - analysisStarted).toFixed(1))
5319
+ });
5320
+ const bbox = bboxFromGeometry(analysis.geometry);
5321
+ const summary = summarizeThicknessSamples(analysis.samples, options);
5322
+ if (analysis.warnings.length > 0) {
5323
+ analysis.warnings.forEach((warning) => warnings.push(`${renderable.name}: ${warning}`));
5324
+ }
5325
+ const objectIndex = index + 1;
5326
+ objects.push({
5327
+ index: objectIndex,
5328
+ id: renderable.id,
5329
+ name: renderable.name,
5330
+ groupName: renderable.groupName,
5331
+ treePath: sourceObject == null ? void 0 : sourceObject.treePath,
5332
+ mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
5333
+ triangleCount: analysis.triangleCount,
5334
+ sampledTriangleCount: analysis.sampledTriangleCount,
5335
+ sampleStride: analysis.sampleStride,
5336
+ bbox,
5337
+ ...summary
5338
+ });
5339
+ cloudObjects.push({
5340
+ index: objectIndex,
5341
+ id: renderable.id,
5342
+ name: renderable.name,
5343
+ groupName: renderable.groupName,
5344
+ treePath: sourceObject == null ? void 0 : sourceObject.treePath,
5345
+ mock: (sourceObject == null ? void 0 : sourceObject.mock) === true,
5346
+ sampleCount: analysis.pointSamples.length,
5347
+ bbox
5348
+ });
5349
+ analysis.pointSamples.forEach((sample) => {
5350
+ points.push({
5351
+ objectIndex,
5352
+ objectId: renderable.id,
5353
+ objectName: renderable.name,
5354
+ ...sample
5355
+ });
5356
+ });
5357
+ if (analysis.pointSamples.length > 0) {
5358
+ const trianglePositions = cloneGeometryPositions(analysis.geometry);
5359
+ if (trianglePositions) {
5360
+ const built = buildScalarSurfaceOverlay(
5361
+ renderable,
5362
+ trianglePositions,
5363
+ analysis.pointSamples,
5364
+ { min: colorScale.domainMin, max: colorScale.domainMax },
5365
+ fieldParams
5366
+ );
5367
+ if (built) {
5368
+ overlays.push(built.overlay);
5369
+ if (built.capped) {
5370
+ warnings.push(
5371
+ `${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
5372
+ );
5373
+ }
5374
+ if (built.holeCount > 0) {
5375
+ warnings.push(
5376
+ `${renderable.name}: ${built.holeCount} surface vertices had no finite minimum-solid-span sample nearby (filled by nearest neighbor).`
5377
+ );
5378
+ }
5379
+ }
5380
+ }
5381
+ }
5382
+ analysis.geometry.dispose();
5383
+ });
5384
+ const state = {
5385
+ optionsKey,
5386
+ overlays,
5387
+ pointCloud: {
5388
+ schemaVersion: 1,
5389
+ property: "throughThickness",
5390
+ coordinateSpace: "object-local",
5391
+ units: "model",
5392
+ sampleCount: points.length,
5393
+ objects: cloudObjects,
5394
+ points
5395
+ },
5396
+ report: {
5397
+ method: "mesh-nearest-through-boundary-bvh",
5398
+ options,
5399
+ sampleBudget,
5400
+ objectCount: objects.length,
5401
+ objects,
5402
+ warnings,
5403
+ style: {
5404
+ colorScale,
5405
+ colorMinThickness: options.colorMinThickness,
5406
+ colorMaxThickness: options.colorMaxThickness,
5407
+ unknownColor: THICKNESS_COLORS.unknown
5408
+ }
5409
+ }
5410
+ };
5411
+ session.throughThicknessInspection = state;
5412
+ return state;
5413
+ }
5414
+ function renderCurrentThroughThickness(session, rawOptions, fieldOverride) {
5415
+ const state = getSessionThroughThicknessInspection(session, rawOptions, fieldOverride);
5416
+ return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
3854
5417
  }
3855
5418
  const ROUGHNESS_SMOOTH_OPACITY = 0.16;
3856
5419
  const ROUGHNESS_HARSH_OPACITY = 1;
3857
- function renderCurrentRoughness(session, rawOptions) {
3858
- const state = getSessionRoughnessInspection(session, rawOptions);
3859
- return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
5420
+ function renderCurrentRoughness(session, rawOptions, fieldOverride) {
5421
+ const state = getSessionRoughnessInspection(session, rawOptions, fieldOverride);
5422
+ return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
5423
+ }
5424
+ function stressSamplesForRenderable(surface, renderable, useAllSamples) {
5425
+ const samples = useAllSamples ? surface.samples : surface.samples.filter((sample) => sample.partName === renderable.name || sample.partName === renderable.groupName);
5426
+ if (samples.length === 0) return null;
5427
+ return { ...surface, samples };
5428
+ }
5429
+ function renderCurrentStressSurface(session, surface, fieldOverride) {
5430
+ var _a;
5431
+ const fieldParams = mergeScalarFieldParams(fieldOverride);
5432
+ const colorOverride = scalarFieldColorOverride(fieldOverride);
5433
+ const overlays = [];
5434
+ const warnings = [];
5435
+ const useAllSamples = session.renderables.length === 1;
5436
+ const domain = {
5437
+ min: surface.domain.min,
5438
+ max: surface.domain.max > surface.domain.min ? surface.domain.max : surface.domain.min + 1
5439
+ };
5440
+ for (const renderable of session.renderables) {
5441
+ const scopedSurface = stressSamplesForRenderable(surface, renderable, useAllSamples);
5442
+ if (!scopedSurface) continue;
5443
+ const trianglePositions = (_a = renderable.solid.geometry.getAttribute("position")) == null ? void 0 : _a.array;
5444
+ if (!(trianglePositions instanceof Float32Array)) {
5445
+ warnings.push(`${renderable.name}: rendered geometry has no Float32Array position buffer for stress heatmap.`);
5446
+ continue;
5447
+ }
5448
+ const built = buildScalarSurfaceOverlayFromSamples(
5449
+ renderable,
5450
+ trianglePositions,
5451
+ stressSurfaceToScalarSamples(scopedSurface),
5452
+ domain,
5453
+ fieldParams,
5454
+ { normalGateMode: "absolute" },
5455
+ colorOverride
5456
+ );
5457
+ if (!built) continue;
5458
+ overlays.push(built.overlay);
5459
+ if (built.capped) {
5460
+ throw new Error(
5461
+ `${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; refusing to render an incomplete stress heatmap.`
5462
+ );
5463
+ }
5464
+ if (built.holeCount > 0) {
5465
+ throw new Error(
5466
+ `${renderable.name}: ${built.holeCount} surface vertices had no in-gate stress sample; refusing to render a filled stress heatmap.`
5467
+ );
5468
+ }
5469
+ }
5470
+ if (overlays.length === 0) {
5471
+ throw new Error("Stress surface samples did not match any rendered model surface.");
5472
+ }
5473
+ return {
5474
+ png: renderScalarSurfaceOverlays(session, overlays),
5475
+ report: {
5476
+ method: "solver-stress-surface-v1",
5477
+ field: surface.field,
5478
+ unit: surface.unit,
5479
+ coordinateSpace: surface.coordinateSpace,
5480
+ sampleCount: surface.samples.length,
5481
+ valueMin: surface.valueMin,
5482
+ valueMax: surface.valueMax,
5483
+ hotSpots: surface.hotSpots,
5484
+ warnings
5485
+ }
5486
+ };
3860
5487
  }
3861
- function getSessionRoughnessInspection(session, rawOptions) {
5488
+ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
3862
5489
  var _a;
3863
5490
  const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
3864
5491
  const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
3865
- const optionsKey = inspectionOptionsKey({ options, sampleBudget });
5492
+ const fieldParams = mergeScalarFieldParams(fieldOverride);
5493
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
3866
5494
  if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
3867
5495
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
3868
5496
  const warnings = [];
3869
5497
  maybePushSceneSampleBudgetWarning(warnings, "Roughness", sampleBudget);
3870
5498
  const objects = [];
5499
+ const hotSpotCandidates = [];
3871
5500
  const cloudObjects = [];
3872
5501
  const points = [];
3873
5502
  const overlays = [];
@@ -3886,6 +5515,7 @@ function getSessionRoughnessInspection(session, rawOptions) {
3886
5515
  });
3887
5516
  const roughnessColorScale = Number.isFinite(roughnessMin) && roughnessMax > roughnessMin ? { colormap: DEFAULT_ROUGHNESS_COLOR_SCALE.colormap, domainMin: roughnessMin, domainMax: roughnessMax } : DEFAULT_ROUGHNESS_COLOR_SCALE;
3888
5517
  session.renderables.forEach((renderable, index) => {
5518
+ var _a2;
3889
5519
  const analysis = analysesByIndex[index];
3890
5520
  const bbox = bboxFromGeometry(analysis.geometry);
3891
5521
  const sourceObject = byId.get(renderable.id);
@@ -3921,9 +5551,42 @@ function getSessionRoughnessInspection(session, rawOptions) {
3921
5551
  ...sample
3922
5552
  });
3923
5553
  });
3924
- overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples, roughnessColorScale) });
5554
+ analysis.hotSpots.forEach((hotSpot) => {
5555
+ hotSpotCandidates.push({
5556
+ ...hotSpot,
5557
+ rank: 0,
5558
+ objectRank: hotSpot.rank,
5559
+ objectIndex,
5560
+ objectId: renderable.id,
5561
+ objectName: renderable.name
5562
+ });
5563
+ });
5564
+ const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
5565
+ if (trianglePositions instanceof Float32Array) {
5566
+ const built = buildScalarSurfaceOverlay(renderable, trianglePositions, analysis.pointSamples, null, fieldParams);
5567
+ if (built) {
5568
+ overlays.push(built.overlay);
5569
+ if (built.capped) {
5570
+ warnings.push(
5571
+ `${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
5572
+ );
5573
+ }
5574
+ if (built.holeCount > 0) {
5575
+ warnings.push(`${renderable.name}: ${built.holeCount} surface vertices had no in-gate sample (filled by nearest neighbor).`);
5576
+ }
5577
+ }
5578
+ }
3925
5579
  analysis.geometry.dispose();
3926
5580
  });
5581
+ const hotSpots = hotSpotCandidates.sort((lhs, rhs) => {
5582
+ const valueDelta = (rhs.value ?? -Infinity) - (lhs.value ?? -Infinity);
5583
+ if (Math.abs(valueDelta) > 1e-9) return valueDelta;
5584
+ if (rhs.nearbySampleCount !== lhs.nearbySampleCount) return rhs.nearbySampleCount - lhs.nearbySampleCount;
5585
+ const areaDelta = rhs.nearbyArea - lhs.nearbyArea;
5586
+ if (Math.abs(areaDelta) > 1e-9) return areaDelta;
5587
+ if (lhs.objectIndex !== rhs.objectIndex) return lhs.objectIndex - rhs.objectIndex;
5588
+ return lhs.objectRank - rhs.objectRank;
5589
+ }).map((hotSpot, index) => ({ ...hotSpot, rank: index + 1 }));
3927
5590
  const state = {
3928
5591
  optionsKey,
3929
5592
  overlays,
@@ -3942,6 +5605,7 @@ function getSessionRoughnessInspection(session, rawOptions) {
3942
5605
  sampleBudget,
3943
5606
  objectCount: objects.length,
3944
5607
  objects,
5608
+ hotSpots,
3945
5609
  warnings,
3946
5610
  style: {
3947
5611
  // Legacy class colors kept for back-compat; colorScale is the truth the
@@ -5073,6 +6737,60 @@ Available renderable objects: ${available}`;
5073
6737
  }
5074
6738
  return "No visible renderable objects found.";
5075
6739
  }
6740
+ const PENDING_TEXTURE_DECODES = /* @__PURE__ */ new Set();
6741
+ function trackTextureDecode(texture) {
6742
+ PENDING_TEXTURE_DECODES.add(textureDecoded(texture));
6743
+ }
6744
+ function textureDecoded(texture) {
6745
+ return new Promise((resolve) => {
6746
+ var _a;
6747
+ const finish = () => {
6748
+ texture.needsUpdate = true;
6749
+ resolve();
6750
+ };
6751
+ const forgeDecoded = (_a = texture.userData) == null ? void 0 : _a.forgeDecoded;
6752
+ if (forgeDecoded) {
6753
+ forgeDecoded.then(
6754
+ () => finish(),
6755
+ () => finish()
6756
+ );
6757
+ return;
6758
+ }
6759
+ const image = texture.image;
6760
+ if (image && typeof ImageBitmap !== "undefined" && image instanceof ImageBitmap) {
6761
+ finish();
6762
+ return;
6763
+ }
6764
+ if (image && typeof image.decode === "function") {
6765
+ image.decode().then(finish, finish);
6766
+ return;
6767
+ }
6768
+ if (image && (image.naturalWidth > 0 || image.complete && image.width > 0)) {
6769
+ finish();
6770
+ return;
6771
+ }
6772
+ if (image && "onload" in image) {
6773
+ const prevLoad = image.onload;
6774
+ const prevError = image.onerror;
6775
+ image.onload = (ev) => {
6776
+ if (typeof prevLoad === "function") prevLoad.call(image, ev);
6777
+ finish();
6778
+ };
6779
+ image.onerror = (ev) => {
6780
+ if (typeof prevError === "function") prevError.call(image, ev);
6781
+ finish();
6782
+ };
6783
+ return;
6784
+ }
6785
+ finish();
6786
+ });
6787
+ }
6788
+ async function awaitPendingTextureDecodes() {
6789
+ if (PENDING_TEXTURE_DECODES.size === 0) return;
6790
+ const pending = Array.from(PENDING_TEXTURE_DECODES);
6791
+ PENDING_TEXTURE_DECODES.clear();
6792
+ await Promise.all(pending);
6793
+ }
5076
6794
  function createSession(code, opts) {
5077
6795
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
5078
6796
  const size = (opts == null ? void 0 : opts.size) ?? 1024;
@@ -5426,6 +7144,12 @@ Fix one:
5426
7144
  depthWrite: materialOpacity >= 0.99 && materialTransmission <= 0,
5427
7145
  clippingPlanes: applicableCutPlanes
5428
7146
  });
7147
+ const textureDescriptor = mp == null ? void 0 : mp.texture;
7148
+ if (textureDescriptor && solidMaterial instanceof MeshPhysicalMaterial) {
7149
+ const projectedTexture = descriptorToThreeTexture(textureDescriptor.image, { colorSpace: "srgb" });
7150
+ applyProjectedTexture(solidMaterial, textureDescriptor.projection, projectedTexture);
7151
+ trackTextureDecode(projectedTexture);
7152
+ }
5429
7153
  solid = new Mesh(geo.solid, solidMaterial);
5430
7154
  if (isScanRenderStyle) {
5431
7155
  const scanProxyGeometry = createScanProxyGeometry(geo.solid, { grid: scanProxyGrid ?? void 0 });
@@ -5572,6 +7296,7 @@ Fix one:
5572
7296
  collisionEntries,
5573
7297
  collisionReport: null,
5574
7298
  thicknessInspection: null,
7299
+ throughThicknessInspection: null,
5575
7300
  roughnessInspection: null,
5576
7301
  joints,
5577
7302
  jointCouplings,
@@ -5618,6 +7343,7 @@ async function emitInspectProgress(opts, event) {
5618
7343
  }
5619
7344
  window.__forgeRender = async (code, opts) => {
5620
7345
  var _a, _b, _c;
7346
+ setInspectProfileEnabled(opts == null ? void 0 : opts.debug);
5621
7347
  const requestedCameraTokens = (opts == null ? void 0 : opts.cameras) ?? (opts == null ? void 0 : opts.angles);
5622
7348
  const hasDirectionalCameraTokens = Array.isArray(requestedCameraTokens) && requestedCameraTokens.length > 0;
5623
7349
  if ((opts == null ? void 0 : opts.viewName) && hasDirectionalCameraTokens) {
@@ -5635,6 +7361,7 @@ window.__forgeRender = async (code, opts) => {
5635
7361
  const built = createSession(code, {
5636
7362
  size: (opts == null ? void 0 : opts.size) || 1024,
5637
7363
  pixelRatio: (opts == null ? void 0 : opts.pixelRatio) || 1,
7364
+ debug: opts == null ? void 0 : opts.debug,
5638
7365
  quality: opts == null ? void 0 : opts.quality,
5639
7366
  allFiles: opts == null ? void 0 : opts.allFiles,
5640
7367
  binaryFiles: opts == null ? void 0 : opts.binaryFiles,
@@ -5654,7 +7381,7 @@ window.__forgeRender = async (code, opts) => {
5654
7381
  scanGranularity: opts == null ? void 0 : opts.scanGranularity,
5655
7382
  respectAuthoredSceneStyle: opts == null ? void 0 : opts.respectAuthoredSceneStyle,
5656
7383
  cutaway: (opts == null ? void 0 : opts.cutaway) ?? null,
5657
- includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("floating") || requestedChannels.has("distance") || requestedChannels.has("thickness"),
7384
+ includeConnectivity: requestedChannels.has("connectivity") || requestedChannels.has("floating") || requestedChannels.has("distance") || requestedChannels.has("throughThickness") || requestedChannels.has("thickness"),
5658
7385
  includeCollisions: requestedChannels.has("collisions"),
5659
7386
  capture: "orbit"
5660
7387
  });
@@ -5697,6 +7424,7 @@ window.__forgeRender = async (code, opts) => {
5697
7424
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
5698
7425
  }
5699
7426
  }
7427
+ await awaitPendingTextureDecodes();
5700
7428
  await emitInspectProgress(opts, { type: "session-done", objectCount: session.objects.length });
5701
7429
  const renderMode = (opts == null ? void 0 : opts.renderMode) === "wireframe" ? "wireframe" : "solid";
5702
7430
  const edgePreset = (opts == null ? void 0 : opts.edges) ?? "off";
@@ -5732,7 +7460,9 @@ window.__forgeRender = async (code, opts) => {
5732
7460
  const comparisonRenders = {};
5733
7461
  const collisionRenders = {};
5734
7462
  const thicknessRenders = {};
7463
+ const throughThicknessRenders = {};
5735
7464
  const roughnessRenders = {};
7465
+ const stressRenders = {};
5736
7466
  let maskObjects = [];
5737
7467
  let connectivityReport = null;
5738
7468
  let floatingReport = null;
@@ -5742,8 +7472,11 @@ window.__forgeRender = async (code, opts) => {
5742
7472
  let comparisonPointCloud = null;
5743
7473
  let collisionReport = null;
5744
7474
  let thicknessReport = null;
7475
+ let throughThicknessReport = null;
5745
7476
  let roughnessReport = null;
7477
+ let stressReport = null;
5746
7478
  let thicknessPointCloud = null;
7479
+ let throughThicknessPointCloud = null;
5747
7480
  let roughnessPointCloud = null;
5748
7481
  let sectionEvidence = null;
5749
7482
  const framingViews = {};
@@ -5822,7 +7555,7 @@ window.__forgeRender = async (code, opts) => {
5822
7555
  }
5823
7556
  if (requestedChannels.has("roughness")) {
5824
7557
  await markChannelViewStart("roughness", label);
5825
- const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
7558
+ const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness, opts == null ? void 0 : opts.scalarFieldParams);
5826
7559
  roughnessRenders[label] = roughness.png;
5827
7560
  roughnessReport = roughness.report;
5828
7561
  roughnessPointCloud = roughness.pointCloud;
@@ -5877,12 +7610,28 @@ window.__forgeRender = async (code, opts) => {
5877
7610
  }
5878
7611
  if (requestedChannels.has("thickness")) {
5879
7612
  await markChannelViewStart("thickness", label);
5880
- const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness);
7613
+ const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness, opts == null ? void 0 : opts.scalarFieldParams);
5881
7614
  thicknessRenders[label] = thickness.png;
5882
7615
  thicknessReport = thickness.report;
5883
7616
  thicknessPointCloud = thickness.pointCloud;
5884
7617
  await markChannelViewDone("thickness", label);
5885
7618
  }
7619
+ if (requestedChannels.has("stress")) {
7620
+ if (!(opts == null ? void 0 : opts.stressSurface)) throw new Error("stress channel requires solver-produced stressSurface evidence.");
7621
+ await markChannelViewStart("stress", label);
7622
+ const stress = renderCurrentStressSurface(session, opts.stressSurface, opts.scalarFieldParams);
7623
+ stressRenders[label] = stress.png;
7624
+ stressReport = stress.report;
7625
+ await markChannelViewDone("stress", label);
7626
+ }
7627
+ if (requestedChannels.has("throughThickness")) {
7628
+ await markChannelViewStart("throughThickness", label);
7629
+ const throughThickness = renderCurrentThroughThickness(session, opts == null ? void 0 : opts.thickness, opts == null ? void 0 : opts.scalarFieldParams);
7630
+ throughThicknessRenders[label] = throughThickness.png;
7631
+ throughThicknessReport = throughThickness.report;
7632
+ throughThicknessPointCloud = throughThickness.pointCloud;
7633
+ await markChannelViewDone("throughThickness", label);
7634
+ }
5886
7635
  } catch (e) {
5887
7636
  if (comparisonSession) disposeSession(comparisonSession);
5888
7637
  disposeSession(session);
@@ -5991,6 +7740,19 @@ window.__forgeRender = async (code, opts) => {
5991
7740
  } : {
5992
7741
  views: thicknessRenders
5993
7742
  },
7743
+ stress: stressReport ? {
7744
+ ...stressReport,
7745
+ views: stressRenders
7746
+ } : {
7747
+ views: stressRenders
7748
+ },
7749
+ throughThickness: throughThicknessReport ? {
7750
+ ...throughThicknessReport,
7751
+ pointCloud: throughThicknessPointCloud,
7752
+ views: throughThicknessRenders
7753
+ } : {
7754
+ views: throughThicknessRenders
7755
+ },
5994
7756
  section: sectionEvidence,
5995
7757
  bbox: session.bbox,
5996
7758
  volume: session.volume,
@@ -6022,6 +7784,7 @@ window.__forgeCaptureInit = async (code, opts) => {
6022
7784
  if (!built.ok) {
6023
7785
  return built;
6024
7786
  }
7787
+ await awaitPendingTextureDecodes();
6025
7788
  captureSession = built.session;
6026
7789
  return {
6027
7790
  ok: true,