forgecad 0.10.4 → 0.11.0

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 (121) hide show
  1. package/dist/assets/{AdminPage-B3L3W1Uo.js → AdminPage-B1nIvqLS.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-YZJbw5nd.js} +2 -2
  3. package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-DIWRApKS.js} +1 -1
  4. package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-ClL6X1hR.js} +8 -22
  5. package/dist/assets/EditorApp-CYBDvSyT.js +17067 -0
  6. package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-Dmfu_LIw.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-XYTiYxfM.js} +2 -2
  8. package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-D5Z3CscF.js} +2 -2
  9. package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-BP4lIGio.js} +2 -2
  10. package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-D3bcPBsC.js} +1 -1
  11. package/dist/assets/{app-BsRYSfxY.js → app-BKjogwIZ.js} +3288 -512
  12. package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-6a9-ilom.js} +80498 -74979
  13. package/dist/assets/cli/{render-XXol_ET7.js → render-CMNudGb0.js} +1264 -113
  14. package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-BuZgc606.js} +8369 -6839
  15. package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-DQ82ueGu.js} +45438 -39996
  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-ymhBV4Ll.js → inspectWorker-Cuby2qfT.js} +4899 -1303
  19. package/dist/assets/{jointPose-B0blBj9A.js → jointPose-CFql5I-u.js} +1 -1
  20. package/dist/assets/{landing-proof-driven-Cpf-MIbI.css → landing-proof-driven-_u4v_xQb.css} +2 -2
  21. package/dist/assets/{manifold-CYlIm-M6.js → manifold-02pmr7O7.js} +2 -2
  22. package/dist/assets/{manifold-B_7QXpGB.js → manifold-C6KU0oII.js} +1 -1
  23. package/dist/assets/{manifold-CNShmpEJ.js → manifold-P1yF3GKn.js} +1 -1
  24. package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-kg065BVL.js} +76583 -65731
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/AI/usage.md +6 -8
  28. package/dist/docs-raw/CLI.md +14 -12
  29. package/dist/docs-raw/component-model.md +28 -9
  30. package/dist/docs-raw/generated/assembly.md +76 -3
  31. package/dist/docs-raw/generated/concepts.md +43 -7
  32. package/dist/docs-raw/generated/core.md +399 -73
  33. package/dist/docs-raw/generated/curves.md +357 -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 +235 -0
  38. package/dist/docs-raw/skills/forgecad-build-model.md +70 -147
  39. package/dist/docs-raw/skills/forgecad-image-prompt.md +1 -1
  40. package/dist/docs-raw/skills/forgecad-project-sync.md +3 -3
  41. package/dist/docs-raw/skills/forgecad-reconstruct-cad-file.md +2 -2
  42. package/dist/docs-raw/skills/forgecad-reconstruct-from-images.md +4 -5
  43. package/dist/docs-raw/skills/forgecad.md +4 -1
  44. package/dist/docs-raw/skills/index.md +1 -5
  45. package/dist/docs-raw/welcome.md +3 -4
  46. package/dist/index.html +1 -1
  47. package/dist/llms.txt +1 -2
  48. package/dist/sitemap.xml +15 -15
  49. package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-UJWUEIDC.js} +1 -1
  50. package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-O2EPDJSY.js} +1 -1
  51. package/dist-cli/{chunk-UHBRMYA6.js → chunk-MNDROM7T.js} +78926 -73392
  52. package/dist-cli/forgecad.js +6306 -1061
  53. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  54. package/dist-skill/CONTEXT.md +1257 -110
  55. package/dist-skill/SKILL.md +4 -1
  56. package/dist-skill/docs/API/core/concepts.md +31 -4
  57. package/dist-skill/docs/CLI.md +14 -12
  58. package/dist-skill/docs/generated/assembly.md +73 -3
  59. package/dist-skill/docs/generated/core.md +395 -74
  60. package/dist-skill/docs/generated/curves.md +356 -6
  61. package/dist-skill/docs/generated/runtime-names.md +12 -12
  62. package/dist-skill/docs/generated/sketch.md +16 -3
  63. package/dist-skill/docs/guides/inspection-bundles.md +5 -3
  64. package/dist-skill/docs/guides/manual-parameters.md +130 -0
  65. package/dist-skill/docs/guides/structural-fea.md +235 -0
  66. package/dist-skill/library/README.md +0 -4
  67. package/dist-skill/library/forgecad-build-model/SKILL.md +57 -150
  68. package/dist-skill/library/forgecad-build-model/references/inspection-feedback.md +58 -0
  69. package/dist-skill/library/forgecad-build-model/references/module-contracts.md +53 -0
  70. package/dist-skill/library/forgecad-build-model/references/parameter-controls.md +22 -0
  71. package/dist-skill/library/forgecad-build-model/references/readiness-review.md +43 -0
  72. package/dist-skill/library/forgecad-build-model/references/simulation-feedback.md +49 -0
  73. package/dist-skill/library/forgecad-build-model/references/stage-1-design-intent.md +21 -0
  74. package/dist-skill/library/forgecad-build-model/references/stage-2-architecture-plan.md +23 -0
  75. package/dist-skill/library/forgecad-build-model/references/stage-3-build-slices.md +39 -0
  76. package/dist-skill/library/forgecad-build-model/references/stage-4-feedback-iteration.md +24 -0
  77. package/dist-skill/library/forgecad-build-model/references/stage-5-readiness-package.md +34 -0
  78. package/dist-skill/library/forgecad-image-prompt/SKILL.md +1 -1
  79. package/dist-skill/library/forgecad-project-sync/SKILL.md +3 -3
  80. package/dist-skill/library/forgecad-reconstruct-cad-file/SKILL.md +2 -2
  81. package/dist-skill/library/forgecad-reconstruct-from-images/SKILL.md +4 -5
  82. package/dist-skill/website/skills/forgecad-build-model.md +70 -147
  83. package/dist-skill/website/skills/forgecad-image-prompt.md +1 -1
  84. package/dist-skill/website/skills/forgecad-project-sync.md +3 -3
  85. package/dist-skill/website/skills/forgecad-reconstruct-cad-file.md +2 -2
  86. package/dist-skill/website/skills/forgecad-reconstruct-from-images.md +4 -5
  87. package/dist-skill/website/skills/forgecad.md +4 -1
  88. package/dist-skill/website/skills/index.md +1 -5
  89. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  90. package/examples/api/blend-full-round.forge.js +37 -0
  91. package/examples/api/blend-variable-radius.forge.js +51 -0
  92. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  93. package/examples/api/extrude-up-to-face.forge.js +47 -0
  94. package/examples/api/param-path2d.forge.js +65 -0
  95. package/examples/api/param-placement2d.forge.js +80 -0
  96. package/examples/api/param-spline2d-g-continuity.forge.js +57 -0
  97. package/examples/api/spoon-full-tang-handle.forge.js +188 -0
  98. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  99. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  100. package/examples/api/surface-variable-thickness-panel.forge.js +62 -0
  101. package/examples/mechanical/airplane-propeller.forge.js +81 -28
  102. package/package.json +5 -2
  103. package/dist/assets/EditorApp-BWUGCdD5.js +0 -16610
  104. package/dist/docs-raw/skills/forgecad-design-spec.md +0 -145
  105. package/dist/docs-raw/skills/forgecad-grade-model.md +0 -84
  106. package/dist/docs-raw/skills/forgecad-inspect-model.md +0 -80
  107. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +0 -78
  108. package/dist-skill/library/forgecad-design-spec/SKILL.md +0 -132
  109. package/dist-skill/library/forgecad-design-spec/references/default-profiles.md +0 -99
  110. package/dist-skill/library/forgecad-design-spec/references/master-prompt.md +0 -73
  111. package/dist-skill/library/forgecad-grade-model/SKILL.md +0 -72
  112. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-inspect-model/SKILL.md +0 -68
  114. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +0 -66
  115. package/dist-skill/website/skills/forgecad-design-spec.md +0 -145
  116. package/dist-skill/website/skills/forgecad-grade-model.md +0 -84
  117. package/dist-skill/website/skills/forgecad-inspect-model.md +0 -80
  118. package/dist-skill/website/skills/forgecad-verify-mujoco.md +0 -78
  119. /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +0 -0
  120. /package/dist-skill/library/{forgecad-verify-mujoco → forgecad-build-model}/scripts/mujoco_verify.py +0 -0
  121. /package/dist-skill/library/{forgecad-inspect-model → forgecad-build-model/scripts}/summarize_manifest.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, bH as BoxGeometry, cm as MeshStandardMaterial, a5 as BackSide, bd as PointLight, M as Mesh, ab as MeshBasicMaterial, c$ as localAabbPlaneRelation, i as Vector2, d0 as ShapeUtils, d1 as analyzePhysicalConnectivity, h as Vector3, a0 as Matrix4, d2 as Frustum, J as Box3, a1 as MathUtils, d3 as meshContactDataFor, d4 as AabbSpatialIndex, d5 as detectPhysicalContact, d6 as resolveThicknessInspectionOptions, R as Raycaster, d7 as thicknessColor, d8 as thicknessClass, bf as BufferAttribute, bQ as MeshBVH, b_ as acceleratedRaycast, d9 as requireFiniteNumber, da as requireIntegerAtLeast, db as requirePositiveFiniteNumber, c_ as initBackendForEvaluation, f as Color, bh as COMPARISON_COLORS, aB as resolveForgeRenderStyle, bX as getRenderStylePreset, az as setParamOverrides, bw as runScript, cK as scanProxyGridForBounds, dc as Group, bl as shapeToGeometry, bx as MeshPhysicalMaterial, bO as AdditiveBlending, c1 as scanMaterialShellColor, bV as descriptorToThreeTexture, bW as applyProjectedTexture, dd as createScanProxyGeometry, b3 as LineBasicMaterial, bZ as NormalBlending, by as LineSegments, P as PerspectiveCamera, cH as DEFAULT_VIEW_CONFIG, bD as worldAuthorPlaneToLocal, de as resolveSectionHatchMetrics, cQ as buildGeometryComparisonPointCloud, cO as triangleSoupFromMeshes, O as OrthographicCamera, l as ShaderMaterial, c7 as ZEBRA_STRIPE_FRAGMENT_SHADER, c8 as ZEBRA_STRIPE_VERTEX_SHADER, c2 as ZEBRA_STRIPE_SOFTNESS, c3 as ZEBRA_STRIPE_SCALE, c4 as ZEBRA_LIGHT_COLOR, c5 as ZEBRA_DARK_COLOR, c6 as ZEBRA_ACCENT_COLOR, bP as geometryWithVisibleVertexColors, df as intersectWithPlane, dg as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, d as SRGBColorSpace, dh as parseCameraCliSpec, di 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, cb as SURFACE_FIELD_FRAGMENT_SHADER, cc as SURFACE_FIELD_VERTEX_SHADER, ca as scanMaterialLayerStyles, c9 as SCAN_PROXY_LAYER_STYLES, aX as resolveJointAnimation, aY as resolveJointViewValues, b2 as BufferGeometry, dj as DEFAULT_ROUGHNESS_COLOR_SCALE, bR as makeColorScaleTexture, bS as colorScaleLUT, bT as makeInspectScalarUniforms, b$ as INSPECT_SCALAR_FRAGMENT_SHADER, c0 as INSPECT_SCALAR_VERTEX_SHADER, bg as heatPointsForSide, dk as analyzeCollisionIntersections, dl as serializeCollisionFinding, dm as summarizeThicknessSamples, dn as THICKNESS_COLORS, dp as DEFAULT_THICKNESS_COLOR_SCALE, bc as SpotLight, cq as CylinderGeometry, dq as TorusGeometry, ce as CatmullRomCurve3, cf as TubeGeometry, cU as resolveScalarSceneSampleBudget, dr as DEFAULT_INSPECT_ISOLINE_SPACING, ch as DEFAULT_COLORMAP, bm as buildComparisonHeatPatchGeometry, bn as EdgesGeometry, ds as SphereGeometry, dt as ConeGeometry, bi as comparisonHeatDepthTest, bj as comparisonHeatEdgeOpacity, bk as comparisonHeatPatchOpacity, cY as comparisonCandidateContextOpacity, du as DEFAULT_COMPARISON_CANDIDATE_OPACITY } from "../backendInit-6C0DLgH0.js";
5
- import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-B0blBj9A.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, d7 as localAabbPlaneRelation, i as Vector2, d8 as ShapeUtils, d9 as analyzePhysicalConnectivity, h as Vector3, a0 as Matrix4, da as Frustum, J as Box3, a1 as MathUtils, db as meshContactDataFor, dc as AabbSpatialIndex, dd as detectPhysicalContact, de as resolveThicknessInspectionOptions, df as thicknessColor, dg as thicknessClass, bf as BufferAttribute, R as Raycaster, bS as MeshBVH, c1 as acceleratedRaycast, dh as requireFiniteNumber, di as requireIntegerAtLeast, dj as requirePositiveFiniteNumber, d6 as initBackendForEvaluation, f as Color, bh as COMPARISON_COLORS, aB as resolveForgeRenderStyle, b_ as getRenderStylePreset, az as setParamOverrides, bw as runScript, cN as scanProxyGridForBounds, dk as Group, bl as shapeToGeometry, bx as MeshPhysicalMaterial, bQ as AdditiveBlending, c4 as scanMaterialShellColor, bY as descriptorToThreeTexture, bZ as applyProjectedTexture, dl as createScanProxyGeometry, b3 as LineBasicMaterial, c0 as NormalBlending, by as LineSegments, P as PerspectiveCamera, cK as DEFAULT_VIEW_CONFIG, bF as worldAuthorPlaneToLocal, dm as resolveSectionHatchMetrics, cY as buildGeometryComparisonPointCloud, cW 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, dn as intersectWithPlane, dp as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, d as SRGBColorSpace, dq as parseCameraCliSpec, dr 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, ds 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, dt as analyzeCollisionIntersections, du as serializeCollisionFinding, dv as summarizeThicknessSamples, dw as THICKNESS_COLORS, dx as DEFAULT_THICKNESS_COLOR_SCALE, dy as DEFAULT_INSPECT_ISOLINE_SPACING, bc as SpotLight, ct as CylinderGeometry, dz as TorusGeometry, ch as CatmullRomCurve3, ci as TubeGeometry, d0 as resolveScalarSceneSampleBudget, bm as buildComparisonHeatPatchGeometry, bn as EdgesGeometry, ck as DEFAULT_COLORMAP, dA as SphereGeometry, dB as ConeGeometry, bi as comparisonHeatDepthTest, bj as comparisonHeatEdgeOpacity, bk as comparisonHeatPatchOpacity, d4 as comparisonCandidateContextOpacity, dC as DEFAULT_COMPARISON_CANDIDATE_OPACITY } from "../backendInit-6a9-ilom.js";
5
+ import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-CFql5I-u.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,17 +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 makeThicknessRaycastTarget(sourceGeometry, rayMaterial, jumpable) {
1327
+ function makeThicknessRaycastTarget(sourceGeometry, rayMaterial) {
1328
1328
  const geometry = sourceGeometry.clone();
1329
1329
  geometry.boundsTree = new MeshBVH(geometry);
1330
1330
  const mesh = new Mesh(geometry, rayMaterial);
1331
1331
  mesh.raycast = acceleratedRaycast;
1332
- return { mesh, jumpable, geometry };
1332
+ return { mesh, geometry };
1333
1333
  }
1334
- function geometryMaxDimension(geometry) {
1334
+ function geometryMaxDimension$1(geometry) {
1335
1335
  geometry.computeBoundingBox();
1336
1336
  const box = geometry.boundingBox;
1337
1337
  if (!box) return 1;
@@ -1339,34 +1339,47 @@ function geometryMaxDimension(geometry) {
1339
1339
  box.getSize(size);
1340
1340
  return Math.max(1, size.x, size.y, size.z);
1341
1341
  }
1342
- 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) {
1343
1366
  const origin = point.clone().addScaledVector(direction, epsilon);
1344
1367
  raycaster.set(origin, direction);
1345
1368
  raycaster.near = epsilon;
1346
1369
  raycaster.far = far;
1347
1370
  const hits = raycaster.intersectObjects(rayTargetMeshes, false);
1371
+ const minDistance = Math.max(epsilon, contactTolerance);
1348
1372
  for (const hit of hits) {
1349
- if (hit.distance <= epsilon) continue;
1350
- if (hit.distance <= contactTolerance && jumpableMeshes.has(hit.object)) continue;
1373
+ if (hit.distance <= minDistance) continue;
1351
1374
  return hit.distance + epsilon;
1352
1375
  }
1353
1376
  return null;
1354
1377
  }
1355
- function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid, normal, epsilon, far, contactTolerance) {
1356
- const forward = firstOppositeSurfaceDistance(
1357
- raycaster,
1358
- rayTargetMeshes,
1359
- jumpableMeshes,
1360
- centroid,
1361
- normal,
1362
- epsilon,
1363
- far,
1364
- contactTolerance
1365
- );
1378
+ function triangleThickness(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance) {
1379
+ const forward = firstOppositeSurfaceDistance(raycaster, rayTargetMeshes, centroid, normal, epsilon, far, contactTolerance);
1366
1380
  const backward = firstOppositeSurfaceDistance(
1367
1381
  raycaster,
1368
1382
  rayTargetMeshes,
1369
- jumpableMeshes,
1370
1383
  centroid,
1371
1384
  normal.clone().negate(),
1372
1385
  epsilon,
@@ -1377,9 +1390,20 @@ function triangleThickness(raycaster, rayTargetMeshes, jumpableMeshes, centroid,
1377
1390
  if (backward == null) return forward;
1378
1391
  return Math.min(forward, backward);
1379
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
+ }
1380
1404
  function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {}) {
1381
1405
  const options = resolveThicknessInspectionOptions(rawOptions);
1382
- const geometry = cloneGeometryForFaceColors(sourceGeometry);
1406
+ const geometry = cloneGeometryForFaceColors$1(sourceGeometry);
1383
1407
  const position = geometry.getAttribute("position");
1384
1408
  if (!position || position.count < 3) {
1385
1409
  return {
@@ -1395,24 +1419,13 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
1395
1419
  const triangleCount = Math.floor(position.count / 3);
1396
1420
  const surfaceTriangles = readSurfaceTriangles(position);
1397
1421
  const surfaceSamples = sampleSurfaceTriangles(surfaceTriangles, options.maxSamplesPerObject);
1398
- const connectedGeometries = context.connectedGeometries ?? [];
1399
- const maxDim = Math.max(geometryMaxDimension(geometry), ...connectedGeometries.map(geometryMaxDimension));
1400
- const epsilon = Math.max(1e-4, maxDim * 1e-6);
1401
- const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
1402
1422
  const colors = new Float32Array(position.count * 3);
1403
1423
  const triangleThicknessValues = new Array(triangleCount).fill(void 0);
1404
1424
  const samples = [];
1405
1425
  const pointSamples = [];
1406
1426
  const warnings = [];
1407
- const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1408
- const rayTargets = [
1409
- makeThicknessRaycastTarget(geometry, rayMaterial, false),
1410
- ...connectedGeometries.map((connectedGeometry) => makeThicknessRaycastTarget(connectedGeometry, rayMaterial, true))
1411
- ];
1427
+ const rayContext = createThicknessRaycastContext(geometry, options, context);
1412
1428
  try {
1413
- const rayTargetMeshes = rayTargets.map((target) => target.mesh);
1414
- const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
1415
- const raycaster = new Raycaster();
1416
1429
  if (surfaceTriangles.length === 0) {
1417
1430
  warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
1418
1431
  } else if (surfaceSamples.length < surfaceTriangles.length) {
@@ -1423,16 +1436,7 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
1423
1436
  const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1424
1437
  for (const sample of surfaceSamples) {
1425
1438
  sampledTriangleIndexes.add(sample.triangle.index);
1426
- const thickness = triangleThickness(
1427
- raycaster,
1428
- rayTargetMeshes,
1429
- jumpableMeshes,
1430
- sample.position,
1431
- sample.normal,
1432
- epsilon,
1433
- far,
1434
- options.contactTolerance
1435
- );
1439
+ const thickness = measureThicknessAtPoint(rayContext, sample.position, sample.normal);
1436
1440
  samples.push({ thickness, area: sample.area });
1437
1441
  const previous = triangleThicknessValues[sample.triangle.index];
1438
1442
  if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
@@ -1468,14 +1472,743 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
1468
1472
  warnings
1469
1473
  };
1470
1474
  } finally {
1471
- rayTargets.forEach((target) => target.geometry.dispose());
1472
- rayMaterial.dispose();
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);
2077
+ const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
2078
+ const targets = [
2079
+ makeThroughThicknessTarget(sourceGeometry, rayMaterial),
2080
+ ...connectedGeometries.map((geometry) => makeThroughThicknessTarget(geometry, rayMaterial))
2081
+ ];
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();
2092
+ }
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
+ }
2143
+ });
2144
+ }
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();
1473
2205
  }
1474
2206
  }
1475
2207
  const DEFAULT_VERTEX_CAP = 2e6;
1476
2208
  const DEFAULT_TARGET_EDGE_SPACING_FACTOR = 2;
1477
2209
  const DEFAULT_K = 8;
1478
2210
  const DEFAULT_GATE_DOT = 0.3;
2211
+ const CREASE_WELD_DOT = Math.SQRT1_2;
1479
2212
  const SCATTER_CELL_SPACING_FACTOR = 1.5;
1480
2213
  const SCATTER_RADIUS_SPACING_FACTOR = 3;
1481
2214
  const IDW_DISTANCE_FLOOR = 1e-9;
@@ -1486,6 +2219,9 @@ function subVec(a, b) {
1486
2219
  function crossVec(a, b) {
1487
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]];
1488
2221
  }
2222
+ function dotVec(a, b) {
2223
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2224
+ }
1489
2225
  function lenVec(a) {
1490
2226
  return Math.hypot(a[0], a[1], a[2]);
1491
2227
  }
@@ -1511,9 +2247,7 @@ function maxEdge(positions, a, b, c) {
1511
2247
  }
1512
2248
  function weld(positions) {
1513
2249
  if (positions.length % 9 !== 0) {
1514
- throw new Error(
1515
- `weld: positions length must be a multiple of 9 (9 floats per triangle), got ${positions.length}`
1516
- );
2250
+ throw new Error(`weld: positions length must be a multiple of 9 (9 floats per triangle), got ${positions.length}`);
1517
2251
  }
1518
2252
  positions.length / 3;
1519
2253
  let minX = Infinity;
@@ -1541,6 +2275,7 @@ function weld(positions) {
1541
2275
  const remap = /* @__PURE__ */ new Map();
1542
2276
  const out = [];
1543
2277
  const tris = [];
2278
+ const weldNormals = [];
1544
2279
  const inv = 1 / tolerance;
1545
2280
  const quantKey = (i) => {
1546
2281
  const qx = Math.round(positions[i] * inv);
@@ -1549,15 +2284,31 @@ function weld(positions) {
1549
2284
  return `${qx},${qy},${qz}`;
1550
2285
  };
1551
2286
  for (let tri = 0; tri < positions.length; tri += 9) {
2287
+ const faceNormal = rawTriangleNormal(positions, tri);
1552
2288
  const idx = [];
1553
2289
  for (let corner = 0; corner < 3; corner += 1) {
1554
2290
  const o = tri + corner * 3;
1555
2291
  const key = quantKey(o);
1556
- let vi = remap.get(key);
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
+ }
1557
2303
  if (vi === void 0) {
1558
2304
  vi = out.length / 3;
1559
2305
  out.push(positions[o], positions[o + 1], positions[o + 2]);
1560
- remap.set(key, vi);
2306
+ weldNormals.push(faceNormal);
2307
+ if (bucket) {
2308
+ bucket.push(vi);
2309
+ } else {
2310
+ remap.set(key, [vi]);
2311
+ }
1561
2312
  }
1562
2313
  idx.push(vi);
1563
2314
  }
@@ -1569,6 +2320,29 @@ function weld(positions) {
1569
2320
  const degenerate = distinctVertexCount < 3 || tris.length === 0;
1570
2321
  return { positions: out, tris, degenerate };
1571
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
+ }
1572
2346
  function adaptiveSubdivide(mesh, targetEdge, vertexCap) {
1573
2347
  requirePositiveFiniteNumber(targetEdge, "adaptiveSubdivide targetEdge");
1574
2348
  requireIntegerAtLeast(vertexCap, "adaptiveSubdivide vertexCap", 3);
@@ -1674,7 +2448,7 @@ function gatherSamples(sampleGrid, p, rings, out) {
1674
2448
  }
1675
2449
  }
1676
2450
  }
1677
- function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
2451
+ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot, normalGateMode = "signed") {
1678
2452
  const cell = spacing * SCATTER_CELL_SPACING_FACTOR;
1679
2453
  const sampleGrid = buildSampleGrid(samples.positions, cell);
1680
2454
  const radius = spacing * SCATTER_RADIUS_SPACING_FACTOR;
@@ -1709,7 +2483,8 @@ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
1709
2483
  const dz = samples.positions[so + 2] - pz;
1710
2484
  const d2 = dx * dx + dy * dy + dz * dz;
1711
2485
  if (d2 > radius2) continue;
1712
- if (samples.normals[so] * nx + samples.normals[so + 1] * ny + samples.normals[so + 2] * nz < gateDot) 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;
1713
2488
  const val = samples.values[si];
1714
2489
  if (count < k) {
1715
2490
  bestD2[count] = d2;
@@ -1737,17 +2512,17 @@ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
1737
2512
  continue;
1738
2513
  }
1739
2514
  let w = 0;
1740
- let acc = 0;
2515
+ let accum = 0;
1741
2516
  for (let t = 0; t < count; t += 1) {
1742
2517
  const ww = 1 / Math.max(bestD2[t], IDW_DISTANCE_FLOOR);
1743
2518
  w += ww;
1744
- acc += ww * bestVal[t];
2519
+ accum += ww * bestVal[t];
1745
2520
  }
1746
- values[vi] = acc / w;
2521
+ values[vi] = accum / w;
1747
2522
  }
1748
2523
  return { values, holeCount };
1749
2524
  }
1750
- function backfillHoles(values, mesh) {
2525
+ function backfillScalarFieldHoles(values, mesh) {
1751
2526
  const vertexCount = values.length;
1752
2527
  const adjacency = Array.from({ length: vertexCount }, () => []);
1753
2528
  for (let t = 0; t < mesh.tris.length; t += 3) {
@@ -1813,6 +2588,10 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
1813
2588
  const targetEdgeSpacingFactor = options.targetEdgeSpacingFactor === void 0 ? DEFAULT_TARGET_EDGE_SPACING_FACTOR : requirePositiveFiniteNumber(options.targetEdgeSpacingFactor, "targetEdgeSpacingFactor");
1814
2589
  const k = options.k === void 0 ? DEFAULT_K : requireIntegerAtLeast(options.k, "k", 1);
1815
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
+ }
1816
2595
  const welded = weld(trianglePositions);
1817
2596
  let surfaceArea = 0;
1818
2597
  for (let t = 0; t < welded.tris.length; t += 3) {
@@ -1828,7 +2607,7 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
1828
2607
  }
1829
2608
  const normals = vertexNormals(subdivided);
1830
2609
  const effectiveSpacing = sampleSpacing > 0 ? sampleSpacing : 1;
1831
- const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot);
2610
+ const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot, normalGateMode);
1832
2611
  let valueMin = Infinity;
1833
2612
  let valueMax = -Infinity;
1834
2613
  for (let i = 0; i < scatter.values.length; i += 1) {
@@ -1841,7 +2620,7 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
1841
2620
  valueMin = 0;
1842
2621
  valueMax = 0;
1843
2622
  }
1844
- backfillHoles(scatter.values, subdivided);
2623
+ backfillScalarFieldHoles(scatter.values, subdivided);
1845
2624
  const vertexCount = subdivided.positions.length / 3;
1846
2625
  return {
1847
2626
  positions: Float32Array.from(subdivided.positions),
@@ -1856,6 +2635,22 @@ function reconstructSurfaceScalarField(trianglePositions, samples, options = {})
1856
2635
  vertexCount
1857
2636
  };
1858
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
+ }
1859
2654
  const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1860
2655
  smoothAngleDeg: 5,
1861
2656
  sharpAngleDeg: 30,
@@ -1997,13 +2792,15 @@ function percentile(sorted, q) {
1997
2792
  return Number(sorted[index].toFixed(2));
1998
2793
  }
1999
2794
  const DEG_PER_RAD = 180 / Math.PI;
2795
+ const ROUGHNESS_HOTSPOT_LIMIT = 16;
2796
+ const ROUGHNESS_HOTSPOT_RADIUS_FRACTION = 0.03;
2000
2797
  function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
2001
2798
  const options = resolveRoughnessInspectionOptions(rawOptions);
2002
2799
  const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
2003
2800
  const position = geometry.getAttribute("position");
2004
2801
  const warnings = [];
2005
2802
  if (!position || position.count < 3) {
2006
- return { geometry, pointSamples: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
2803
+ return { geometry, pointSamples: [], hotSpots: [], summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
2007
2804
  }
2008
2805
  const triangleCount = Math.floor(position.count / 3);
2009
2806
  const normals = new Array(triangleCount);
@@ -2076,16 +2873,66 @@ function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
2076
2873
  area: sample.area
2077
2874
  };
2078
2875
  });
2876
+ const hotSpots = selectRoughnessHotSpots(pointSamples, bbox, options.sharpAngleDeg);
2079
2877
  geometry.setAttribute("color", new BufferAttribute(colors, 3));
2080
2878
  geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
2081
2879
  geometry.computeBoundingBox();
2082
2880
  return {
2083
2881
  geometry,
2084
2882
  pointSamples,
2883
+ hotSpots,
2085
2884
  summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
2086
2885
  warnings
2087
2886
  };
2088
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
+ }
2089
2936
  function markTriangleRoughness(edges, triangles, normals, triangleEdgeAngles) {
2090
2937
  const edgeAngles = [];
2091
2938
  let boundaryEdgeCount = 0;
@@ -4130,14 +4977,47 @@ function mergeScalarFieldParams(override) {
4130
4977
  }
4131
4978
  return merged;
4132
4979
  }
4133
- function buildScalarSurfaceOverlay(renderable, trianglePositions, samples, domain, fieldParams) {
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 = {}) {
4134
5014
  const started = performance.now();
4135
- const scalarSamples = scalarSamplesFromPointSamples(samples);
4136
5015
  if (scalarSamples.values.length === 0) return null;
4137
5016
  const reconstructStarted = performance.now();
4138
5017
  const field = reconstructSurfaceScalarField(trianglePositions, scalarSamples, {
4139
5018
  vertexCap: SCALAR_SURFACE_VERTEX_CAP,
4140
- targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR
5019
+ targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR,
5020
+ ...fieldOptions
4141
5021
  });
4142
5022
  const reconstructMs = performance.now() - reconstructStarted;
4143
5023
  inspectProfileLog("scalar-overlay", {
@@ -4150,24 +5030,11 @@ function buildScalarSurfaceOverlay(renderable, trianglePositions, samples, domai
4150
5030
  reconstructMs: Number(reconstructMs.toFixed(1)),
4151
5031
  totalMs: Number((performance.now() - started).toFixed(1))
4152
5032
  });
4153
- const colorScale = domain ? { colormap: DEFAULT_COLORMAP, domainMin: domain.min, domainMax: domain.max } : {
4154
- colormap: DEFAULT_COLORMAP,
4155
- domainMin: field.valueMin,
4156
- domainMax: field.valueMax > field.valueMin ? field.valueMax : field.valueMin + 1
4157
- };
4158
- return {
4159
- overlay: {
4160
- renderable,
4161
- positions: field.positions,
4162
- normals: field.normals,
4163
- index: field.index,
4164
- aValue: field.values,
4165
- colorScale,
4166
- fieldParams
4167
- },
4168
- capped: field.capped,
4169
- holeCount: field.holeCount
4170
- };
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);
4171
5038
  }
4172
5039
  function renderScalarSurfaceOverlays(session, overlays) {
4173
5040
  const started = performance.now();
@@ -4187,20 +5054,37 @@ function renderScalarSurfaceOverlays(session, overlays) {
4187
5054
  ghostMaterial.toneMapped = false;
4188
5055
  renderable.solid.material = ghostMaterial;
4189
5056
  const overlay = overlayById.get(renderable.id);
5057
+ const previousVisible = renderable.solid.visible;
5058
+ if (overlay) renderable.solid.visible = false;
4190
5059
  if (!overlay || overlay.positions.length === 0) {
4191
- return { renderable, previousMaterial, ghostMaterial, mesh: null, geometry: null, material: null, lut: null };
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
+ };
4192
5071
  }
4193
5072
  const geometry = new BufferGeometry();
4194
5073
  geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
4195
5074
  geometry.setAttribute("normal", new BufferAttribute(overlay.normals, 3));
4196
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));
4197
5077
  geometry.setIndex(new BufferAttribute(overlay.index, 1));
4198
5078
  const lut = makeColorScaleTexture(colorScaleLUT(overlay.colorScale.colormap));
4199
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);
4200
5082
  const uniforms = makeInspectScalarUniforms({
4201
5083
  colorScale: lut,
5084
+ scalarTexture,
4202
5085
  domainMin: overlay.colorScale.domainMin,
4203
5086
  domainMax: overlay.colorScale.domainMax,
5087
+ colorScaleReversed: overlay.colorScale.reversed === true,
4204
5088
  quantizeBands: overlay.fieldParams.quantizeBands,
4205
5089
  isoEnabled: overlay.fieldParams.isoEnabled,
4206
5090
  isoSpacing: overlay.fieldParams.isoSpacing,
@@ -4226,7 +5110,7 @@ function renderScalarSurfaceOverlays(session, overlays) {
4226
5110
  mesh.renderOrder = 5;
4227
5111
  mesh.raycast = () => null;
4228
5112
  renderable.root.add(mesh);
4229
- return { renderable, previousMaterial, ghostMaterial, mesh, geometry, material, lut };
5113
+ return { renderable, previousMaterial, previousVisible, ghostMaterial, mesh, geometry, material, lut, scalarTexture };
4230
5114
  });
4231
5115
  try {
4232
5116
  const png = withSolidOnlyVisibility(
@@ -4242,21 +5126,25 @@ function renderScalarSurfaceOverlays(session, overlays) {
4242
5126
  });
4243
5127
  return png;
4244
5128
  } finally {
4245
- replacements.forEach(({ renderable, previousMaterial, ghostMaterial, mesh, geometry, material, lut }) => {
4246
- if (mesh) renderable.root.remove(mesh);
4247
- renderable.solid.material = previousMaterial;
4248
- ghostMaterial.dispose();
4249
- geometry == null ? void 0 : geometry.dispose();
4250
- material == null ? void 0 : material.dispose();
4251
- lut == null ? void 0 : lut.dispose();
4252
- });
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
+ );
4253
5141
  }
4254
5142
  }
4255
5143
  function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
4256
5144
  var _a;
4257
5145
  const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
4258
5146
  const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
4259
- const fieldParams = mergeScalarFieldParams(fieldOverride);
5147
+ const fieldParams = { ...mergeScalarFieldParams(fieldOverride), shadingEnabled: false };
4260
5148
  const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
4261
5149
  if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
4262
5150
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
@@ -4265,7 +5153,8 @@ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
4265
5153
  const thicknessColorScale = {
4266
5154
  colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
4267
5155
  domainMin: options.colorMinThickness,
4268
- domainMax: options.colorMaxThickness
5156
+ domainMax: options.colorMaxThickness,
5157
+ ...{ reversed: true }
4269
5158
  };
4270
5159
  const objects = [];
4271
5160
  const cloudObjects = [];
@@ -4278,7 +5167,6 @@ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
4278
5167
  neighborSets: raycastConnectivity.neighborIdsByObjectId.size
4279
5168
  });
4280
5169
  session.renderables.forEach((renderable, index) => {
4281
- var _a2;
4282
5170
  const sourceObject = byId.get(renderable.id);
4283
5171
  const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
4284
5172
  const analysisStarted = performance.now();
@@ -4327,25 +5215,32 @@ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
4327
5215
  ...sample
4328
5216
  });
4329
5217
  });
4330
- const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
4331
- if (trianglePositions instanceof Float32Array) {
4332
- const built = buildScalarSurfaceOverlay(
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(
4333
5229
  renderable,
4334
- trianglePositions,
4335
- analysis.pointSamples,
5230
+ field,
4336
5231
  { min: thicknessColorScale.domainMin, max: thicknessColorScale.domainMax },
4337
5232
  fieldParams
4338
5233
  );
4339
- if (built) {
4340
- overlays.push(built.overlay);
4341
- if (built.capped) {
4342
- warnings.push(
4343
- `${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
4344
- );
4345
- }
4346
- if (built.holeCount > 0) {
4347
- warnings.push(`${renderable.name}: ${built.holeCount} surface vertices had no in-gate sample (filled by nearest neighbor).`);
4348
- }
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
+ );
4349
5244
  }
4350
5245
  }
4351
5246
  analysis.geometry.dispose();
@@ -4384,12 +5279,212 @@ function renderCurrentThickness(session, rawOptions, fieldOverride) {
4384
5279
  const state = getSessionThicknessInspection(session, rawOptions, fieldOverride);
4385
5280
  return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
4386
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 };
5417
+ }
4387
5418
  const ROUGHNESS_SMOOTH_OPACITY = 0.16;
4388
5419
  const ROUGHNESS_HARSH_OPACITY = 1;
4389
5420
  function renderCurrentRoughness(session, rawOptions, fieldOverride) {
4390
5421
  const state = getSessionRoughnessInspection(session, rawOptions, fieldOverride);
4391
5422
  return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
4392
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
+ };
5487
+ }
4393
5488
  function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
4394
5489
  var _a;
4395
5490
  const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
@@ -4401,6 +5496,7 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
4401
5496
  const warnings = [];
4402
5497
  maybePushSceneSampleBudgetWarning(warnings, "Roughness", sampleBudget);
4403
5498
  const objects = [];
5499
+ const hotSpotCandidates = [];
4404
5500
  const cloudObjects = [];
4405
5501
  const points = [];
4406
5502
  const overlays = [];
@@ -4455,6 +5551,16 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
4455
5551
  ...sample
4456
5552
  });
4457
5553
  });
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
+ });
4458
5564
  const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
4459
5565
  if (trianglePositions instanceof Float32Array) {
4460
5566
  const built = buildScalarSurfaceOverlay(renderable, trianglePositions, analysis.pointSamples, null, fieldParams);
@@ -4472,6 +5578,15 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
4472
5578
  }
4473
5579
  analysis.geometry.dispose();
4474
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 }));
4475
5590
  const state = {
4476
5591
  optionsKey,
4477
5592
  overlays,
@@ -4490,10 +5605,11 @@ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
4490
5605
  sampleBudget,
4491
5606
  objectCount: objects.length,
4492
5607
  objects,
5608
+ hotSpots,
4493
5609
  warnings,
4494
5610
  style: {
4495
5611
  // Legacy class colors kept for back-compat; colorScale is the truth the
4496
- // viewport/CLI now render with (continuous viridis over the data domain).
5612
+ // viewport/CLI now render with (continuous scalar colormap over the data domain).
4497
5613
  colorScale: roughnessColorScale,
4498
5614
  smoothColor: ROUGHNESS_COLORS.smooth,
4499
5615
  moderateColor: ROUGHNESS_COLORS.moderate,
@@ -6180,6 +7296,7 @@ Fix one:
6180
7296
  collisionEntries,
6181
7297
  collisionReport: null,
6182
7298
  thicknessInspection: null,
7299
+ throughThicknessInspection: null,
6183
7300
  roughnessInspection: null,
6184
7301
  joints,
6185
7302
  jointCouplings,
@@ -6264,7 +7381,7 @@ window.__forgeRender = async (code, opts) => {
6264
7381
  scanGranularity: opts == null ? void 0 : opts.scanGranularity,
6265
7382
  respectAuthoredSceneStyle: opts == null ? void 0 : opts.respectAuthoredSceneStyle,
6266
7383
  cutaway: (opts == null ? void 0 : opts.cutaway) ?? null,
6267
- 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"),
6268
7385
  includeCollisions: requestedChannels.has("collisions"),
6269
7386
  capture: "orbit"
6270
7387
  });
@@ -6343,7 +7460,9 @@ window.__forgeRender = async (code, opts) => {
6343
7460
  const comparisonRenders = {};
6344
7461
  const collisionRenders = {};
6345
7462
  const thicknessRenders = {};
7463
+ const throughThicknessRenders = {};
6346
7464
  const roughnessRenders = {};
7465
+ const stressRenders = {};
6347
7466
  let maskObjects = [];
6348
7467
  let connectivityReport = null;
6349
7468
  let floatingReport = null;
@@ -6353,8 +7472,11 @@ window.__forgeRender = async (code, opts) => {
6353
7472
  let comparisonPointCloud = null;
6354
7473
  let collisionReport = null;
6355
7474
  let thicknessReport = null;
7475
+ let throughThicknessReport = null;
6356
7476
  let roughnessReport = null;
7477
+ let stressReport = null;
6357
7478
  let thicknessPointCloud = null;
7479
+ let throughThicknessPointCloud = null;
6358
7480
  let roughnessPointCloud = null;
6359
7481
  let sectionEvidence = null;
6360
7482
  const framingViews = {};
@@ -6494,6 +7616,22 @@ window.__forgeRender = async (code, opts) => {
6494
7616
  thicknessPointCloud = thickness.pointCloud;
6495
7617
  await markChannelViewDone("thickness", label);
6496
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
+ }
6497
7635
  } catch (e) {
6498
7636
  if (comparisonSession) disposeSession(comparisonSession);
6499
7637
  disposeSession(session);
@@ -6602,6 +7740,19 @@ window.__forgeRender = async (code, opts) => {
6602
7740
  } : {
6603
7741
  views: thicknessRenders
6604
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
+ },
6605
7756
  section: sectionEvidence,
6606
7757
  bbox: session.bbox,
6607
7758
  volume: session.volume,