forgecad 0.10.2 → 0.10.4

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 (132) hide show
  1. package/README.md +7 -6
  2. package/dist/assets/{AdminPage-CHY6ZN-p.js → AdminPage-B3L3W1Uo.js} +1 -1
  3. package/dist/assets/{BenchmarkPage-BcRT5iGN.js → BenchmarkPage-DXKVXMrJ.js} +2 -2
  4. package/dist/assets/{BlogPage-BssBbnb-.js → BlogPage-B7BWxOCg.js} +1 -1
  5. package/dist/assets/{DocsPage-DsvdiRNK.js → DocsPage-BPGGwht1.js} +28 -48
  6. package/dist/assets/{EditorApp-Bfd3jbtC.js → EditorApp-BWUGCdD5.js} +183 -21
  7. package/dist/assets/{EditorApp-BpjZgzk0.css → EditorApp-C5f24ZN9.css} +8 -0
  8. package/dist/assets/{EmbedViewer-D5t8WamV.js → EmbedViewer-DygByZS2.js} +2 -2
  9. package/dist/assets/{LandingPageProofDriven-DbN7o-Be.js → LandingPageProofDriven-BoVE7JGY.js} +54 -36
  10. package/dist/assets/{LegalPage-DNGrrY0p.js → LegalPage-Din8wv8d.js} +2 -2
  11. package/dist/assets/{PricingPage-Nczr3pRz.js → PricingPage-C2PMzmDc.js} +2 -2
  12. package/dist/assets/{SettingsPage-DZlyu4d4.js → SettingsPage-BlJDCRe8.js} +1 -1
  13. package/dist/assets/{app-C9ct2hRD.js → app-BsRYSfxY.js} +2264 -6259
  14. package/dist/assets/{backendInit-ymjonyQp.js → backendInit-6C0DLgH0.js} +8290 -2136
  15. package/dist/assets/cli/{render-B_0lQwKU.js → render-XXol_ET7.js} +822 -105
  16. package/dist/assets/{constructionHistoryWorker-CZ42Dksy.js → constructionHistoryWorker-cTHWRJEi.js} +699 -284
  17. package/dist/assets/{evalWorker-C2pm8LHP.js → evalWorker-BssDYW9u.js} +2559 -1330
  18. package/dist/assets/{forgecad_geometry-BlMtqluF.js → forgecad_geometry-CZ_IfuvA.js} +1 -9
  19. package/dist/assets/{forgecad_geometry_bg-BllP_WiL.wasm → forgecad_geometry_bg-C3rQHfwg.wasm} +0 -0
  20. package/dist/assets/{inspectWorker-D5T5VbfK.js → inspectWorker-ymhBV4Ll.js} +6254 -671
  21. package/dist/assets/{jointPose-4r8ed8_5.js → jointPose-B0blBj9A.js} +1 -1
  22. package/dist/assets/{landing-proof-driven-ORyigZ6p.css → landing-proof-driven-Cpf-MIbI.css} +73 -13
  23. package/dist/assets/{manifold-5PP1eGLN.js → manifold-B_7QXpGB.js} +1 -1
  24. package/dist/assets/{manifold-DjBkyIc8.js → manifold-CNShmpEJ.js} +1 -1
  25. package/dist/assets/{manifold-C4r6B-XY.js → manifold-CYlIm-M6.js} +2 -2
  26. package/dist/assets/{reportWorker-CwenM7wB.js → reportWorker-Cb5eyM7D.js} +2485 -1275
  27. package/dist/cli/render.html +1 -1
  28. package/dist/docs/index.html +2 -2
  29. package/dist/docs-raw/AI/usage.md +17 -17
  30. package/dist/docs-raw/CLI.md +9 -7
  31. package/dist/docs-raw/README.md +1 -1
  32. package/dist/docs-raw/component-model.md +2 -2
  33. package/dist/docs-raw/generated/assembly.md +1 -1
  34. package/dist/docs-raw/generated/concepts.md +10 -4
  35. package/dist/docs-raw/generated/core.md +96 -1
  36. package/dist/docs-raw/generated/curves.md +8 -1
  37. package/dist/docs-raw/generated/output.md +0 -64
  38. package/dist/docs-raw/generated/runtime-names.md +6 -6
  39. package/dist/docs-raw/generated/viewport.md +3 -12
  40. package/dist/docs-raw/guides/inspection-bundles.md +1 -1
  41. package/dist/docs-raw/simulation-workflow.md +58 -0
  42. package/{dist-skill/website/skills/forgecad-make-a-model.md → dist/docs-raw/skills/forgecad-build-model.md} +18 -8
  43. package/dist/docs-raw/skills/forgecad-design-spec.md +145 -0
  44. package/dist/docs-raw/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  45. package/{dist-skill/website/skills/forgecad-visual-spec.md → dist/docs-raw/skills/forgecad-image-prompt.md} +7 -7
  46. package/dist/docs-raw/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  47. package/{dist-skill/website/skills/forgecad-project.md → dist/docs-raw/skills/forgecad-project-sync.md} +5 -5
  48. package/{dist-skill/website/skills/forgecad-3d-reconstruction.md → dist/docs-raw/skills/forgecad-reconstruct-cad-file.md} +7 -7
  49. package/dist/docs-raw/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  50. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +78 -0
  51. package/dist/docs-raw/skills/forgecad.md +24 -24
  52. package/dist/docs-raw/skills/index.md +9 -13
  53. package/dist/index.html +9 -9
  54. package/dist/llms.txt +7 -7
  55. package/dist/sitemap.xml +16 -16
  56. package/dist-cli/{check-compiler-SP7FAL7R.js → check-compiler-4RPB6SB5.js} +1 -1
  57. package/dist-cli/{check-query-propagation-BRLSHP22.js → check-query-propagation-KN3DFQTX.js} +1 -1
  58. package/dist-cli/{chunk-RQQ42YCP.js → chunk-UHBRMYA6.js} +30770 -29253
  59. package/dist-cli/forgecad.js +3277 -237
  60. package/dist-cli/{forgecad_geometry-7TVSNVUB.js → forgecad_geometry-2IMYCUWW.js} +0 -8
  61. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  62. package/dist-skill/CONTEXT.md +111 -73
  63. package/dist-skill/SKILL.md +1 -1
  64. package/dist-skill/docs/CLI.md +9 -7
  65. package/dist-skill/docs/generated/assembly.md +1 -1
  66. package/dist-skill/docs/generated/core.md +96 -1
  67. package/dist-skill/docs/generated/curves.md +8 -1
  68. package/dist-skill/docs/generated/output.md +0 -64
  69. package/dist-skill/docs/generated/runtime-names.md +6 -6
  70. package/dist-skill/docs/generated/viewport.md +3 -12
  71. package/dist-skill/docs/guides/inspection-bundles.md +1 -1
  72. package/dist-skill/library/README.md +9 -13
  73. package/dist-skill/library/{forgecad-make-a-model → forgecad-build-model}/SKILL.md +16 -6
  74. package/dist-skill/library/forgecad-design-spec/SKILL.md +132 -0
  75. package/dist-skill/library/{forgecad-prepare-prompt → forgecad-design-spec}/references/master-prompt.md +1 -1
  76. package/dist-skill/library/{forgecad-model-grader → forgecad-grade-model}/SKILL.md +6 -4
  77. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +4 -0
  78. package/dist-skill/library/{forgecad-visual-spec → forgecad-image-prompt}/SKILL.md +5 -5
  79. package/dist-skill/library/forgecad-image-prompt/agents/openai.yaml +4 -0
  80. package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/SKILL.md +4 -4
  81. package/dist-skill/library/{forgecad-project → forgecad-project-sync}/SKILL.md +3 -3
  82. package/dist-skill/library/{forgecad-3d-reconstruction → forgecad-reconstruct-cad-file}/SKILL.md +5 -5
  83. package/dist-skill/library/forgecad-reconstruct-cad-file/agents/openai.yaml +4 -0
  84. package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/SKILL.md +10 -10
  85. package/dist-skill/library/forgecad-reconstruct-from-images/agents/openai.yaml +4 -0
  86. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +66 -0
  87. package/dist-skill/library/forgecad-verify-mujoco/scripts/mujoco_verify.py +385 -0
  88. package/{dist/docs-raw/skills/forgecad-make-a-model.md → dist-skill/website/skills/forgecad-build-model.md} +18 -8
  89. package/dist-skill/website/skills/forgecad-design-spec.md +145 -0
  90. package/dist-skill/website/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  91. package/{dist/docs-raw/skills/forgecad-visual-spec.md → dist-skill/website/skills/forgecad-image-prompt.md} +7 -7
  92. package/dist-skill/website/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  93. package/{dist/docs-raw/skills/forgecad-project.md → dist-skill/website/skills/forgecad-project-sync.md} +5 -5
  94. package/{dist/docs-raw/skills/forgecad-3d-reconstruction.md → dist-skill/website/skills/forgecad-reconstruct-cad-file.md} +7 -7
  95. package/dist-skill/website/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  96. package/dist-skill/website/skills/forgecad-verify-mujoco.md +78 -0
  97. package/dist-skill/website/skills/forgecad.md +24 -24
  98. package/dist-skill/website/skills/index.md +9 -13
  99. package/examples/analysis/clearance-fit.forge.js +31 -0
  100. package/examples/analysis/lever-arm-actuator.forge.js +43 -0
  101. package/examples/analysis/tipping-tripod.forge.js +35 -0
  102. package/examples/api/texture-projection.forge.js +75 -0
  103. package/examples/assets/uv-grid.png +0 -0
  104. package/examples/products/sportscar.forge.js +77 -0
  105. package/package.json +1 -3
  106. package/dist/docs-raw/skills/forgecad-blockout-model.md +0 -49
  107. package/dist/docs-raw/skills/forgecad-component-model.md +0 -53
  108. package/dist/docs-raw/skills/forgecad-high-level-spec.md +0 -101
  109. package/dist/docs-raw/skills/forgecad-lld.md +0 -41
  110. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +0 -63
  111. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +0 -60
  112. package/dist-skill/library/forgecad-3d-reconstruction/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-blockout-model/SKILL.md +0 -42
  114. package/dist-skill/library/forgecad-component-model/SKILL.md +0 -46
  115. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +0 -94
  116. package/dist-skill/library/forgecad-image-replicator/agents/openai.yaml +0 -4
  117. package/dist-skill/library/forgecad-lld/SKILL.md +0 -34
  118. package/dist-skill/library/forgecad-model-grader/agents/openai.yaml +0 -4
  119. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +0 -50
  120. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +0 -48
  121. package/dist-skill/library/forgecad-reconstruction-benchmark/agents/openai.yaml +0 -4
  122. package/dist-skill/library/forgecad-visual-spec/agents/openai.yaml +0 -4
  123. package/dist-skill/website/skills/forgecad-blockout-model.md +0 -49
  124. package/dist-skill/website/skills/forgecad-component-model.md +0 -53
  125. package/dist-skill/website/skills/forgecad-high-level-spec.md +0 -101
  126. package/dist-skill/website/skills/forgecad-lld.md +0 -41
  127. package/dist-skill/website/skills/forgecad-prepare-prompt.md +0 -63
  128. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +0 -60
  129. /package/dist/assets/{landing-proof-driven-DiGqdtWa.js → landing-proof-driven-BxZZh5r5.js} +0 -0
  130. /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-design-spec}/references/default-profiles.md +0 -0
  131. /package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/summarize_manifest.py +0 -0
  132. /package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/scripts/compare_images.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, bx as BoxGeometry, c2 as MeshStandardMaterial, a5 as BackSide, b8 as PointLight, M as Mesh, ab as MeshBasicMaterial, cJ as localAabbPlaneRelation, i as Vector2, cK as ShapeUtils, cL as analyzePhysicalConnectivity, h as Vector3, a0 as Matrix4, cM as Frustum, J as Box3, a1 as MathUtils, cN as meshContactDataFor, cO as AabbSpatialIndex, cP as detectPhysicalContact, cQ as resolveThicknessInspectionOptions, R as Raycaster, cR as thicknessColor, cS as thicknessClass, b0 as BufferAttribute, cT as roughnessClassForAngle, cU as resolveRoughnessInspectionOptions, cV as roughnessColorForAngle, cW as roughnessScoreForAngle, cI as initBackendForEvaluation, f as Color, bb as COMPARISON_COLORS, az as resolveForgeRenderStyle, bG as getRenderStylePreset, ay as setParamOverrides, bm as runScript, cs as scanProxyGridForBounds, cX as Group, bf as shapeToGeometry, bn as MeshPhysicalMaterial, bE as AdditiveBlending, bJ as scanMaterialShellColor, cY as createScanProxyGeometry, aP as LineBasicMaterial, bI as NormalBlending, bo as LineSegments, P as PerspectiveCamera, cp as DEFAULT_VIEW_CONFIG, bt as worldAuthorPlaneToLocal, cZ as resolveSectionHatchMetrics, cA as buildGeometryComparisonPointCloud, cy as triangleSoupFromMeshes, O as OrthographicCamera, l as ShaderMaterial, bP as ZEBRA_STRIPE_FRAGMENT_SHADER, bQ as ZEBRA_STRIPE_VERTEX_SHADER, bK as ZEBRA_STRIPE_SOFTNESS, bL as ZEBRA_STRIPE_SCALE, bM as ZEBRA_LIGHT_COLOR, bN as ZEBRA_DARK_COLOR, bO as ZEBRA_ACCENT_COLOR, bF as geometryWithVisibleVertexColors, c_ as intersectWithPlane, c$ as setActiveBackend, W as WebGLRenderer, A as ACESFilmicToneMapping, d as SRGBColorSpace, d0 as parseCameraCliSpec, d1 as PMREMGenerator, b1 as CanvasTexture, b2 as Object3D, b3 as FogExp2, b4 as Fog, b5 as AmbientLight, b9 as DirectionalLight, b6 as HemisphereLight, aJ as findJointAnimationClip, q as Plane, bT as SURFACE_FIELD_FRAGMENT_SHADER, bU as SURFACE_FIELD_VERTEX_SHADER, bS as scanMaterialLayerStyles, bR as SCAN_PROXY_LAYER_STYLES, aK as resolveJointAnimation, aL as resolveJointViewValues, aO as BufferGeometry, bZ as ROUGHNESS_COLORS, d2 as PointsMaterial, d3 as Points, ba as heatPointsForSide, d4 as analyzeCollisionIntersections, d5 as serializeCollisionFinding, d6 as summarizeThicknessSamples, bY as THICKNESS_GRADIENT_COLORS, d7 as THICKNESS_COLORS, b7 as SpotLight, c6 as CylinderGeometry, d8 as TorusGeometry, bW as CatmullRomCurve3, bX as TubeGeometry, cE as resolveScalarSceneSampleBudget, bg as buildComparisonHeatPatchGeometry, bh as EdgesGeometry, d9 as SphereGeometry, da as ConeGeometry, bc as comparisonHeatDepthTest, bd as comparisonHeatEdgeOpacity, be as comparisonHeatPatchOpacity, cG as comparisonCandidateContextOpacity, db as DEFAULT_COMPARISON_CANDIDATE_OPACITY } from "../backendInit-ymjonyQp.js";
5
- import { m as mergeViewportRenderSceneStates, v as validateJointOverrides, b as buildBaseJointValues, p as parseRenderSceneCliSpec, g as getSceneObjectTreePath } from "../jointPose-4r8ed8_5.js";
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";
6
6
  const CAD_MATERIAL_PROPS = {
7
7
  color: 6003669,
8
8
  metalness: 0.05,
@@ -1324,6 +1324,13 @@ function clampUnit(value) {
1324
1324
  function cloneGeometryForFaceColors(geometry) {
1325
1325
  return geometry.index ? geometry.toNonIndexed() : geometry.clone();
1326
1326
  }
1327
+ function makeThicknessRaycastTarget(sourceGeometry, rayMaterial, jumpable) {
1328
+ const geometry = sourceGeometry.clone();
1329
+ geometry.boundsTree = new MeshBVH(geometry);
1330
+ const mesh = new Mesh(geometry, rayMaterial);
1331
+ mesh.raycast = acceleratedRaycast;
1332
+ return { mesh, jumpable, geometry };
1333
+ }
1327
1334
  function geometryMaxDimension(geometry) {
1328
1335
  geometry.computeBoundingBox();
1329
1336
  const box = geometry.boundingBox;
@@ -1399,71 +1406,529 @@ function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}, context = {})
1399
1406
  const warnings = [];
1400
1407
  const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
1401
1408
  const rayTargets = [
1402
- { mesh: new Mesh(geometry, rayMaterial), jumpable: false },
1403
- ...connectedGeometries.map((connectedGeometry) => ({
1404
- mesh: new Mesh(connectedGeometry, rayMaterial),
1405
- jumpable: true
1406
- }))
1409
+ makeThicknessRaycastTarget(geometry, rayMaterial, false),
1410
+ ...connectedGeometries.map((connectedGeometry) => makeThicknessRaycastTarget(connectedGeometry, rayMaterial, true))
1407
1411
  ];
1408
- const rayTargetMeshes = rayTargets.map((target) => target.mesh);
1409
- const jumpableMeshes = new Set(rayTargets.filter((target) => target.jumpable).map((target) => target.mesh));
1410
- const raycaster = new Raycaster();
1411
- if (surfaceTriangles.length === 0) {
1412
- warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
1413
- } else if (surfaceSamples.length < surfaceTriangles.length) {
1414
- warnings.push(
1415
- `Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
1416
- );
1412
+ 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
+ if (surfaceTriangles.length === 0) {
1417
+ warnings.push("No non-degenerate triangle surface was available for thickness sampling.");
1418
+ } else if (surfaceSamples.length < surfaceTriangles.length) {
1419
+ warnings.push(
1420
+ `Area sampling budget ${surfaceSamples.length} covers ${surfaceTriangles.length} surface triangles; increase --thickness-samples for denser analysis.`
1421
+ );
1422
+ }
1423
+ const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1424
+ for (const sample of surfaceSamples) {
1425
+ 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
+ );
1436
+ samples.push({ thickness, area: sample.area });
1437
+ const previous = triangleThicknessValues[sample.triangle.index];
1438
+ if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
1439
+ triangleThicknessValues[sample.triangle.index] = thickness;
1440
+ }
1441
+ pointSamples.push({
1442
+ position: [sample.position.x, sample.position.y, sample.position.z],
1443
+ normal: [sample.normal.x, sample.normal.y, sample.normal.z],
1444
+ value: thickness,
1445
+ className: thicknessClass(thickness, options),
1446
+ color: thicknessColor(thickness, options),
1447
+ area: sample.area
1448
+ });
1449
+ }
1450
+ for (let tri = 0; tri < triangleCount; tri += 1) {
1451
+ const color = thicknessColor(triangleThicknessValues[tri], options);
1452
+ const offset = tri * 3;
1453
+ for (let vertex = 0; vertex < 3; vertex += 1) {
1454
+ const colorOffset = (offset + vertex) * 3;
1455
+ colors[colorOffset] = color[0] / 255;
1456
+ colors[colorOffset + 1] = color[1] / 255;
1457
+ colors[colorOffset + 2] = color[2] / 255;
1458
+ }
1459
+ }
1460
+ geometry.setAttribute("color", new BufferAttribute(colors, 3));
1461
+ return {
1462
+ geometry,
1463
+ samples,
1464
+ pointSamples,
1465
+ triangleCount,
1466
+ sampledTriangleCount: sampledTriangleIndexes.size,
1467
+ sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
1468
+ warnings
1469
+ };
1470
+ } finally {
1471
+ rayTargets.forEach((target) => target.geometry.dispose());
1472
+ rayMaterial.dispose();
1417
1473
  }
1418
- const sampledTriangleIndexes = /* @__PURE__ */ new Set();
1419
- for (const sample of surfaceSamples) {
1420
- sampledTriangleIndexes.add(sample.triangle.index);
1421
- const thickness = triangleThickness(
1422
- raycaster,
1423
- rayTargetMeshes,
1424
- jumpableMeshes,
1425
- sample.position,
1426
- sample.normal,
1427
- epsilon,
1428
- far,
1429
- options.contactTolerance
1474
+ }
1475
+ const DEFAULT_VERTEX_CAP = 2e6;
1476
+ const DEFAULT_TARGET_EDGE_SPACING_FACTOR = 2;
1477
+ const DEFAULT_K = 8;
1478
+ const DEFAULT_GATE_DOT = 0.3;
1479
+ const SCATTER_CELL_SPACING_FACTOR = 1.5;
1480
+ const SCATTER_RADIUS_SPACING_FACTOR = 3;
1481
+ const IDW_DISTANCE_FLOOR = 1e-9;
1482
+ const MAX_SUBDIVISION_PASSES = 24;
1483
+ function subVec(a, b) {
1484
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1485
+ }
1486
+ function crossVec(a, b) {
1487
+ 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
+ }
1489
+ function lenVec(a) {
1490
+ return Math.hypot(a[0], a[1], a[2]);
1491
+ }
1492
+ function getVert(positions, i) {
1493
+ const o = i * 3;
1494
+ return [positions[o], positions[o + 1], positions[o + 2]];
1495
+ }
1496
+ function triArea(positions, a, b, c) {
1497
+ const va = getVert(positions, a);
1498
+ return 0.5 * lenVec(crossVec(subVec(getVert(positions, b), va), subVec(getVert(positions, c), va)));
1499
+ }
1500
+ function triNormal(positions, a, b, c) {
1501
+ const va = getVert(positions, a);
1502
+ const n = crossVec(subVec(getVert(positions, b), va), subVec(getVert(positions, c), va));
1503
+ const l = lenVec(n) || 1;
1504
+ return [n[0] / l, n[1] / l, n[2] / l];
1505
+ }
1506
+ function edgeLen(positions, a, b) {
1507
+ return lenVec(subVec(getVert(positions, a), getVert(positions, b)));
1508
+ }
1509
+ function maxEdge(positions, a, b, c) {
1510
+ return Math.max(edgeLen(positions, a, b), edgeLen(positions, b, c), edgeLen(positions, c, a));
1511
+ }
1512
+ function weld(positions) {
1513
+ 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}`
1430
1516
  );
1431
- samples.push({ thickness, area: sample.area });
1432
- const previous = triangleThicknessValues[sample.triangle.index];
1433
- if (previous === void 0 || previous == null || thickness != null && thickness < previous) {
1434
- triangleThicknessValues[sample.triangle.index] = thickness;
1517
+ }
1518
+ positions.length / 3;
1519
+ let minX = Infinity;
1520
+ let minY = Infinity;
1521
+ let minZ = Infinity;
1522
+ let maxX = -Infinity;
1523
+ let maxY = -Infinity;
1524
+ let maxZ = -Infinity;
1525
+ for (let i = 0; i < positions.length; i += 3) {
1526
+ const x = positions[i];
1527
+ const y = positions[i + 1];
1528
+ const z = positions[i + 2];
1529
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
1530
+ throw new Error(`weld: non-finite vertex position at float index ${i}`);
1531
+ }
1532
+ if (x < minX) minX = x;
1533
+ if (y < minY) minY = y;
1534
+ if (z < minZ) minZ = z;
1535
+ if (x > maxX) maxX = x;
1536
+ if (y > maxY) maxY = y;
1537
+ if (z > maxZ) maxZ = z;
1538
+ }
1539
+ const diagonal = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ);
1540
+ const tolerance = Math.max(diagonal * 1e-5, Number.EPSILON);
1541
+ const remap = /* @__PURE__ */ new Map();
1542
+ const out = [];
1543
+ const tris = [];
1544
+ const inv = 1 / tolerance;
1545
+ const quantKey = (i) => {
1546
+ const qx = Math.round(positions[i] * inv);
1547
+ const qy = Math.round(positions[i + 1] * inv);
1548
+ const qz = Math.round(positions[i + 2] * inv);
1549
+ return `${qx},${qy},${qz}`;
1550
+ };
1551
+ for (let tri = 0; tri < positions.length; tri += 9) {
1552
+ const idx = [];
1553
+ for (let corner = 0; corner < 3; corner += 1) {
1554
+ const o = tri + corner * 3;
1555
+ const key = quantKey(o);
1556
+ let vi = remap.get(key);
1557
+ if (vi === void 0) {
1558
+ vi = out.length / 3;
1559
+ out.push(positions[o], positions[o + 1], positions[o + 2]);
1560
+ remap.set(key, vi);
1561
+ }
1562
+ idx.push(vi);
1563
+ }
1564
+ if (idx[0] !== idx[1] && idx[1] !== idx[2] && idx[2] !== idx[0]) {
1565
+ tris.push(idx[0], idx[1], idx[2]);
1435
1566
  }
1436
- pointSamples.push({
1437
- position: [sample.position.x, sample.position.y, sample.position.z],
1438
- normal: [sample.normal.x, sample.normal.y, sample.normal.z],
1439
- value: thickness,
1440
- className: thicknessClass(thickness, options),
1441
- color: thicknessColor(thickness, options),
1442
- area: sample.area
1443
- });
1444
1567
  }
1445
- for (let tri = 0; tri < triangleCount; tri += 1) {
1446
- const color = thicknessColor(triangleThicknessValues[tri], options);
1447
- const offset = tri * 3;
1448
- for (let vertex = 0; vertex < 3; vertex += 1) {
1449
- const colorOffset = (offset + vertex) * 3;
1450
- colors[colorOffset] = color[0] / 255;
1451
- colors[colorOffset + 1] = color[1] / 255;
1452
- colors[colorOffset + 2] = color[2] / 255;
1568
+ const distinctVertexCount = out.length / 3;
1569
+ const degenerate = distinctVertexCount < 3 || tris.length === 0;
1570
+ return { positions: out, tris, degenerate };
1571
+ }
1572
+ function adaptiveSubdivide(mesh, targetEdge, vertexCap) {
1573
+ requirePositiveFiniteNumber(targetEdge, "adaptiveSubdivide targetEdge");
1574
+ requireIntegerAtLeast(vertexCap, "adaptiveSubdivide vertexCap", 3);
1575
+ const positions = mesh.positions.slice();
1576
+ let tris = mesh.tris.slice();
1577
+ let capped = false;
1578
+ const keyOf = (a, b) => a < b ? `${a}_${b}` : `${b}_${a}`;
1579
+ for (let pass = 0; pass < MAX_SUBDIVISION_PASSES; pass += 1) {
1580
+ const next = [];
1581
+ const midCache = /* @__PURE__ */ new Map();
1582
+ let changed = false;
1583
+ const getMid = (a, b) => {
1584
+ const key = keyOf(a, b);
1585
+ let mi = midCache.get(key);
1586
+ if (mi === void 0) {
1587
+ mi = positions.length / 3;
1588
+ const ao = a * 3;
1589
+ const bo = b * 3;
1590
+ positions.push(
1591
+ (positions[ao] + positions[bo]) / 2,
1592
+ (positions[ao + 1] + positions[bo + 1]) / 2,
1593
+ (positions[ao + 2] + positions[bo + 2]) / 2
1594
+ );
1595
+ midCache.set(key, mi);
1596
+ }
1597
+ return mi;
1598
+ };
1599
+ for (let t = 0; t < tris.length; t += 3) {
1600
+ const a = tris[t];
1601
+ const b = tris[t + 1];
1602
+ const c = tris[t + 2];
1603
+ const vertexCount = positions.length / 3;
1604
+ if (maxEdge(positions, a, b, c) > targetEdge && vertexCount < vertexCap) {
1605
+ const ab = getMid(a, b);
1606
+ const bc = getMid(b, c);
1607
+ const ca = getMid(c, a);
1608
+ next.push(a, ab, ca, ab, b, bc, ca, bc, c, ab, bc, ca);
1609
+ changed = true;
1610
+ } else {
1611
+ if (positions.length / 3 >= vertexCap) capped = true;
1612
+ next.push(a, b, c);
1613
+ }
1614
+ }
1615
+ tris = next;
1616
+ if (!changed) break;
1617
+ }
1618
+ return { positions, tris, capped };
1619
+ }
1620
+ function vertexNormals(mesh) {
1621
+ const vertexCount = mesh.positions.length / 3;
1622
+ const acc = new Float32Array(vertexCount * 3);
1623
+ for (let t = 0; t < mesh.tris.length; t += 3) {
1624
+ const a = mesh.tris[t];
1625
+ const b = mesh.tris[t + 1];
1626
+ const c = mesh.tris[t + 2];
1627
+ const n = triNormal(mesh.positions, a, b, c);
1628
+ const area = triArea(mesh.positions, a, b, c);
1629
+ const nx = n[0] * area;
1630
+ const ny = n[1] * area;
1631
+ const nz = n[2] * area;
1632
+ for (const vi of [a, b, c]) {
1633
+ acc[vi * 3] += nx;
1634
+ acc[vi * 3 + 1] += ny;
1635
+ acc[vi * 3 + 2] += nz;
1453
1636
  }
1454
1637
  }
1455
- geometry.setAttribute("color", new BufferAttribute(colors, 3));
1456
- rayMaterial.dispose();
1638
+ for (let i = 0; i < vertexCount; i += 1) {
1639
+ const o = i * 3;
1640
+ const l = Math.hypot(acc[o], acc[o + 1], acc[o + 2]) || 1;
1641
+ acc[o] /= l;
1642
+ acc[o + 1] /= l;
1643
+ acc[o + 2] /= l;
1644
+ }
1645
+ return acc;
1646
+ }
1647
+ function buildSampleGrid(positions, cell) {
1648
+ const grid = /* @__PURE__ */ new Map();
1649
+ const sampleCount = positions.length / 3;
1650
+ for (let i = 0; i < sampleCount; i += 1) {
1651
+ const o = i * 3;
1652
+ const key = `${Math.floor(positions[o] / cell)},${Math.floor(positions[o + 1] / cell)},${Math.floor(positions[o + 2] / cell)}`;
1653
+ let arr = grid.get(key);
1654
+ if (!arr) {
1655
+ arr = [];
1656
+ grid.set(key, arr);
1657
+ }
1658
+ arr.push(i);
1659
+ }
1660
+ return { grid, cell };
1661
+ }
1662
+ function gatherSamples(sampleGrid, p, rings, out) {
1663
+ out.length = 0;
1664
+ const { cell, grid } = sampleGrid;
1665
+ const bx = Math.floor(p[0] / cell);
1666
+ const by = Math.floor(p[1] / cell);
1667
+ const bz = Math.floor(p[2] / cell);
1668
+ for (let dx = -rings; dx <= rings; dx += 1) {
1669
+ for (let dy = -rings; dy <= rings; dy += 1) {
1670
+ for (let dz = -rings; dz <= rings; dz += 1) {
1671
+ const arr = grid.get(`${bx + dx},${by + dy},${bz + dz}`);
1672
+ if (arr) for (const i of arr) out.push(i);
1673
+ }
1674
+ }
1675
+ }
1676
+ }
1677
+ function scatterToVertices(positions, vnormals, samples, spacing, k, gateDot) {
1678
+ const cell = spacing * SCATTER_CELL_SPACING_FACTOR;
1679
+ const sampleGrid = buildSampleGrid(samples.positions, cell);
1680
+ const radius = spacing * SCATTER_RADIUS_SPACING_FACTOR;
1681
+ const radius2 = radius * radius;
1682
+ const vertexCount = positions.length / 3;
1683
+ const values = new Float32Array(vertexCount);
1684
+ let holeCount = 0;
1685
+ const candidates = [];
1686
+ const scratchP = [0, 0, 0];
1687
+ const bestD2 = new Float64Array(k);
1688
+ const bestVal = new Float64Array(k);
1689
+ for (let vi = 0; vi < vertexCount; vi += 1) {
1690
+ const pi = vi * 3;
1691
+ const px = positions[pi];
1692
+ const py = positions[pi + 1];
1693
+ const pz = positions[pi + 2];
1694
+ const nx = vnormals[pi];
1695
+ const ny = vnormals[pi + 1];
1696
+ const nz = vnormals[pi + 2];
1697
+ scratchP[0] = px;
1698
+ scratchP[1] = py;
1699
+ scratchP[2] = pz;
1700
+ gatherSamples(sampleGrid, scratchP, 3, candidates);
1701
+ let count = 0;
1702
+ let worst = 0;
1703
+ let worstD2 = -Infinity;
1704
+ for (let c = 0; c < candidates.length; c += 1) {
1705
+ const si = candidates[c];
1706
+ const so = si * 3;
1707
+ const dx = samples.positions[so] - px;
1708
+ const dy = samples.positions[so + 1] - py;
1709
+ const dz = samples.positions[so + 2] - pz;
1710
+ const d2 = dx * dx + dy * dy + dz * dz;
1711
+ if (d2 > radius2) continue;
1712
+ if (samples.normals[so] * nx + samples.normals[so + 1] * ny + samples.normals[so + 2] * nz < gateDot) continue;
1713
+ const val = samples.values[si];
1714
+ if (count < k) {
1715
+ bestD2[count] = d2;
1716
+ bestVal[count] = val;
1717
+ if (d2 > worstD2) {
1718
+ worstD2 = d2;
1719
+ worst = count;
1720
+ }
1721
+ count += 1;
1722
+ } else if (d2 < worstD2) {
1723
+ bestD2[worst] = d2;
1724
+ bestVal[worst] = val;
1725
+ worstD2 = -Infinity;
1726
+ for (let t = 0; t < k; t += 1) {
1727
+ if (bestD2[t] > worstD2) {
1728
+ worstD2 = bestD2[t];
1729
+ worst = t;
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+ if (count === 0) {
1735
+ holeCount += 1;
1736
+ values[vi] = NaN;
1737
+ continue;
1738
+ }
1739
+ let w = 0;
1740
+ let acc = 0;
1741
+ for (let t = 0; t < count; t += 1) {
1742
+ const ww = 1 / Math.max(bestD2[t], IDW_DISTANCE_FLOOR);
1743
+ w += ww;
1744
+ acc += ww * bestVal[t];
1745
+ }
1746
+ values[vi] = acc / w;
1747
+ }
1748
+ return { values, holeCount };
1749
+ }
1750
+ function backfillHoles(values, mesh) {
1751
+ const vertexCount = values.length;
1752
+ const adjacency = Array.from({ length: vertexCount }, () => []);
1753
+ for (let t = 0; t < mesh.tris.length; t += 3) {
1754
+ const a = mesh.tris[t];
1755
+ const b = mesh.tris[t + 1];
1756
+ const c = mesh.tris[t + 2];
1757
+ adjacency[a].push(b, c);
1758
+ adjacency[b].push(a, c);
1759
+ adjacency[c].push(a, b);
1760
+ }
1761
+ let frontier = [];
1762
+ for (let i = 0; i < vertexCount; i += 1) {
1763
+ if (Number.isFinite(values[i])) frontier.push(i);
1764
+ }
1765
+ if (frontier.length === 0) {
1766
+ values.fill(0);
1767
+ return;
1768
+ }
1769
+ while (frontier.length > 0) {
1770
+ const nextFrontier = [];
1771
+ for (const v of frontier) {
1772
+ const val = values[v];
1773
+ for (const n of adjacency[v]) {
1774
+ if (!Number.isFinite(values[n])) {
1775
+ values[n] = val;
1776
+ nextFrontier.push(n);
1777
+ }
1778
+ }
1779
+ }
1780
+ frontier = nextFrontier;
1781
+ }
1782
+ }
1783
+ function reconstructSurfaceScalarField(trianglePositions, samples, options = {}) {
1784
+ if (!(trianglePositions instanceof Float32Array)) {
1785
+ throw new Error("reconstructSurfaceScalarField: trianglePositions must be a Float32Array");
1786
+ }
1787
+ if (trianglePositions.length === 0 || trianglePositions.length % 9 !== 0) {
1788
+ throw new Error(
1789
+ `reconstructSurfaceScalarField: trianglePositions length must be a positive multiple of 9, got ${trianglePositions.length}`
1790
+ );
1791
+ }
1792
+ if (!(samples.positions instanceof Float32Array) || !(samples.values instanceof Float32Array) || !(samples.normals instanceof Float32Array)) {
1793
+ throw new Error("reconstructSurfaceScalarField: samples positions/values/normals must be Float32Arrays");
1794
+ }
1795
+ const sampleCount = samples.values.length;
1796
+ if (sampleCount === 0) {
1797
+ throw new Error("reconstructSurfaceScalarField: samples must be non-empty");
1798
+ }
1799
+ if (samples.positions.length !== sampleCount * 3) {
1800
+ throw new Error(
1801
+ `reconstructSurfaceScalarField: samples.positions length ${samples.positions.length} must equal values*3 (${sampleCount * 3})`
1802
+ );
1803
+ }
1804
+ if (samples.normals.length !== sampleCount * 3) {
1805
+ throw new Error(
1806
+ `reconstructSurfaceScalarField: samples.normals length ${samples.normals.length} must equal values*3 (${sampleCount * 3})`
1807
+ );
1808
+ }
1809
+ for (let i = 0; i < sampleCount; i += 1) {
1810
+ requireFiniteNumber(samples.values[i], `samples.values[${i}]`);
1811
+ }
1812
+ const vertexCap = options.vertexCap === void 0 ? DEFAULT_VERTEX_CAP : requireIntegerAtLeast(options.vertexCap, "vertexCap", 3);
1813
+ const targetEdgeSpacingFactor = options.targetEdgeSpacingFactor === void 0 ? DEFAULT_TARGET_EDGE_SPACING_FACTOR : requirePositiveFiniteNumber(options.targetEdgeSpacingFactor, "targetEdgeSpacingFactor");
1814
+ const k = options.k === void 0 ? DEFAULT_K : requireIntegerAtLeast(options.k, "k", 1);
1815
+ const gateDot = options.gateDot === void 0 ? DEFAULT_GATE_DOT : requireFiniteNumber(options.gateDot, "gateDot");
1816
+ const welded = weld(trianglePositions);
1817
+ let surfaceArea = 0;
1818
+ for (let t = 0; t < welded.tris.length; t += 3) {
1819
+ surfaceArea += triArea(welded.positions, welded.tris[t], welded.tris[t + 1], welded.tris[t + 2]);
1820
+ }
1821
+ const sampleSpacing = surfaceArea > 0 ? Math.sqrt(surfaceArea / sampleCount) : 0;
1822
+ let subdivided;
1823
+ if (welded.degenerate || !(sampleSpacing > 0)) {
1824
+ subdivided = { positions: welded.positions, tris: welded.tris, capped: false };
1825
+ } else {
1826
+ const targetEdge = targetEdgeSpacingFactor * sampleSpacing;
1827
+ subdivided = adaptiveSubdivide(welded, targetEdge, vertexCap);
1828
+ }
1829
+ const normals = vertexNormals(subdivided);
1830
+ const effectiveSpacing = sampleSpacing > 0 ? sampleSpacing : 1;
1831
+ const scatter = scatterToVertices(subdivided.positions, normals, samples, effectiveSpacing, k, gateDot);
1832
+ let valueMin = Infinity;
1833
+ let valueMax = -Infinity;
1834
+ for (let i = 0; i < scatter.values.length; i += 1) {
1835
+ const v = scatter.values[i];
1836
+ if (!Number.isFinite(v)) continue;
1837
+ if (v < valueMin) valueMin = v;
1838
+ if (v > valueMax) valueMax = v;
1839
+ }
1840
+ if (!Number.isFinite(valueMin) || !Number.isFinite(valueMax)) {
1841
+ valueMin = 0;
1842
+ valueMax = 0;
1843
+ }
1844
+ backfillHoles(scatter.values, subdivided);
1845
+ const vertexCount = subdivided.positions.length / 3;
1457
1846
  return {
1458
- geometry,
1459
- samples,
1460
- pointSamples,
1461
- triangleCount,
1462
- sampledTriangleCount: sampledTriangleIndexes.size,
1463
- sampleStride: Math.max(1, Math.ceil(Math.max(1, surfaceTriangles.length) / Math.max(1, sampledTriangleIndexes.size))),
1464
- warnings
1847
+ positions: Float32Array.from(subdivided.positions),
1848
+ normals,
1849
+ index: Uint32Array.from(subdivided.tris),
1850
+ values: scatter.values,
1851
+ valueMin,
1852
+ valueMax,
1853
+ capped: subdivided.capped,
1854
+ degenerate: welded.degenerate,
1855
+ holeCount: scatter.holeCount,
1856
+ vertexCount
1465
1857
  };
1466
1858
  }
1859
+ const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
1860
+ smoothAngleDeg: 5,
1861
+ sharpAngleDeg: 30,
1862
+ harshAngleDeg: 90,
1863
+ maxSamplesPerObject: 5e3
1864
+ };
1865
+ const ROUGHNESS_COLORS = {
1866
+ smooth: [62, 72, 84],
1867
+ moderate: [255, 214, 0],
1868
+ sharp: [255, 124, 34],
1869
+ harsh: [255, 42, 96]
1870
+ };
1871
+ function resolveRoughnessInspectionOptions(raw = {}) {
1872
+ const options = {
1873
+ smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
1874
+ sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
1875
+ harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg,
1876
+ maxSamplesPerObject: raw.maxSamplesPerObject ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.maxSamplesPerObject
1877
+ };
1878
+ if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
1879
+ throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
1880
+ }
1881
+ if (!Number.isFinite(options.sharpAngleDeg) || options.sharpAngleDeg <= options.smoothAngleDeg) {
1882
+ throw new Error(`sharpAngleDeg must be greater than smoothAngleDeg (got ${options.sharpAngleDeg}).`);
1883
+ }
1884
+ if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
1885
+ throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
1886
+ }
1887
+ if (!Number.isFinite(options.maxSamplesPerObject) || options.maxSamplesPerObject <= 0) {
1888
+ throw new Error(`maxSamplesPerObject must be a positive finite number (got ${options.maxSamplesPerObject}).`);
1889
+ }
1890
+ return {
1891
+ ...options,
1892
+ maxSamplesPerObject: Math.max(1, Math.floor(options.maxSamplesPerObject))
1893
+ };
1894
+ }
1895
+ function roughnessClassForAngle(angleDeg, options) {
1896
+ if (angleDeg >= options.harshAngleDeg) return "harsh";
1897
+ if (angleDeg >= options.sharpAngleDeg) return "sharp";
1898
+ if (angleDeg >= options.smoothAngleDeg) return "moderate";
1899
+ return "smooth";
1900
+ }
1901
+ function roughnessScoreForAngle(angleDeg, options) {
1902
+ if (angleDeg < options.sharpAngleDeg) return 0;
1903
+ if (angleDeg < options.harshAngleDeg) {
1904
+ return MathUtils.lerp(0.48, 0.82, (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg));
1905
+ }
1906
+ return 1;
1907
+ }
1908
+ function roughnessColorForAngle(angleDeg, options) {
1909
+ const cls = roughnessClassForAngle(angleDeg, options);
1910
+ if (cls === "smooth" || cls === "harsh") return ROUGHNESS_COLORS[cls];
1911
+ if (cls === "moderate") {
1912
+ return lerpRgb(
1913
+ ROUGHNESS_COLORS.moderate,
1914
+ ROUGHNESS_COLORS.sharp,
1915
+ (angleDeg - options.smoothAngleDeg) / (options.sharpAngleDeg - options.smoothAngleDeg)
1916
+ );
1917
+ }
1918
+ return lerpRgb(
1919
+ ROUGHNESS_COLORS.sharp,
1920
+ ROUGHNESS_COLORS.harsh,
1921
+ (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg)
1922
+ );
1923
+ }
1924
+ function lerpRgb(a, b, t) {
1925
+ const clamped = MathUtils.clamp(t, 0, 1);
1926
+ return [
1927
+ Math.round(MathUtils.lerp(a[0], b[0], clamped)),
1928
+ Math.round(MathUtils.lerp(a[1], b[1], clamped)),
1929
+ Math.round(MathUtils.lerp(a[2], b[2], clamped))
1930
+ ];
1931
+ }
1467
1932
  function emptyRoughnessSummary() {
1468
1933
  return {
1469
1934
  triangleCount: 0,
@@ -1928,6 +2393,16 @@ function createCaptureDebugLogger(enabled) {
1928
2393
  console.info(`[forge-capture:debug] +${sinceStart}ms Δ${delta}ms ${phase}${detailText}`);
1929
2394
  };
1930
2395
  }
2396
+ function setInspectProfileEnabled(enabled) {
2397
+ window.__forgeInspectProfile = enabled === true;
2398
+ }
2399
+ function inspectProfileEnabled() {
2400
+ return window.__forgeInspectProfile === true;
2401
+ }
2402
+ function inspectProfileLog(phase, detail) {
2403
+ if (!inspectProfileEnabled()) return;
2404
+ console.info(`[forge-inspect-profile] ${phase} ${JSON.stringify(detail)}`);
2405
+ }
1931
2406
  class EmptyInspectionShape {
1932
2407
  intersect() {
1933
2408
  return {
@@ -3602,21 +4077,100 @@ function bboxFromGeometry(geometry) {
3602
4077
  max: bbox ? [bbox.max.x, bbox.max.y, bbox.max.z] : [0, 0, 0]
3603
4078
  };
3604
4079
  }
3605
- function pointBuffersFromSamples(samples, offset = 0.025) {
3606
- const positions = new Float32Array(samples.length * 3);
3607
- const colors = new Float32Array(samples.length * 3);
3608
- samples.forEach((sample, index) => {
4080
+ const SCALAR_SURFACE_VERTEX_CAP = 2e6;
4081
+ const SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR = 8;
4082
+ function scalarSamplesFromPointSamples(samples) {
4083
+ const finite = samples.filter((sample) => sample.value != null && Number.isFinite(sample.value));
4084
+ const positions = new Float32Array(finite.length * 3);
4085
+ const values = new Float32Array(finite.length);
4086
+ const normals = new Float32Array(finite.length * 3);
4087
+ finite.forEach((sample, index) => {
3609
4088
  const base = index * 3;
3610
- positions[base] = sample.position[0] + sample.normal[0] * offset;
3611
- positions[base + 1] = sample.position[1] + sample.normal[1] * offset;
3612
- positions[base + 2] = sample.position[2] + sample.normal[2] * offset;
3613
- colors[base] = sample.color[0] / 255;
3614
- colors[base + 1] = sample.color[1] / 255;
3615
- colors[base + 2] = sample.color[2] / 255;
4089
+ positions[base] = sample.position[0];
4090
+ positions[base + 1] = sample.position[1];
4091
+ positions[base + 2] = sample.position[2];
4092
+ normals[base] = sample.normal[0];
4093
+ normals[base + 1] = sample.normal[1];
4094
+ normals[base + 2] = sample.normal[2];
4095
+ values[index] = sample.value;
4096
+ });
4097
+ return { positions, values, normals };
4098
+ }
4099
+ const DEFAULT_SCALAR_FIELD_PARAMS = {
4100
+ quantizeBands: 0,
4101
+ isoEnabled: false,
4102
+ isoSpacing: 0,
4103
+ isoLineWidthPx: 1.5,
4104
+ criticalEnabled: false,
4105
+ criticalThreshold: 0,
4106
+ shadingEnabled: true
4107
+ };
4108
+ function mergeScalarFieldParams(override) {
4109
+ if (!override) return DEFAULT_SCALAR_FIELD_PARAMS;
4110
+ const merged = { ...DEFAULT_SCALAR_FIELD_PARAMS };
4111
+ if (override.quantizeBands !== void 0) {
4112
+ const bands = override.quantizeBands;
4113
+ if (!Number.isInteger(bands) || bands < 0) {
4114
+ throw new RangeError(`Heatmap band count must be a non-negative integer, got ${bands}.`);
4115
+ }
4116
+ merged.quantizeBands = bands;
4117
+ }
4118
+ if (override.isoEnabled !== void 0) {
4119
+ merged.isoEnabled = override.isoEnabled;
4120
+ }
4121
+ if (merged.isoEnabled && merged.isoSpacing <= 0) {
4122
+ merged.isoSpacing = DEFAULT_INSPECT_ISOLINE_SPACING;
4123
+ }
4124
+ if (override.isoLineWidthPx !== void 0) {
4125
+ const width = override.isoLineWidthPx;
4126
+ if (!Number.isFinite(width) || width <= 0) {
4127
+ throw new RangeError(`Heatmap isoline width must be a finite positive number of pixels, got ${width}.`);
4128
+ }
4129
+ merged.isoLineWidthPx = width;
4130
+ }
4131
+ return merged;
4132
+ }
4133
+ function buildScalarSurfaceOverlay(renderable, trianglePositions, samples, domain, fieldParams) {
4134
+ const started = performance.now();
4135
+ const scalarSamples = scalarSamplesFromPointSamples(samples);
4136
+ if (scalarSamples.values.length === 0) return null;
4137
+ const reconstructStarted = performance.now();
4138
+ const field = reconstructSurfaceScalarField(trianglePositions, scalarSamples, {
4139
+ vertexCap: SCALAR_SURFACE_VERTEX_CAP,
4140
+ targetEdgeSpacingFactor: SCALAR_SURFACE_TARGET_EDGE_SPACING_FACTOR
3616
4141
  });
3617
- return { positions, colors };
4142
+ const reconstructMs = performance.now() - reconstructStarted;
4143
+ inspectProfileLog("scalar-overlay", {
4144
+ object: renderable.name,
4145
+ samples: scalarSamples.values.length,
4146
+ inputTriangles: trianglePositions.length / 9,
4147
+ vertices: field.vertexCount,
4148
+ capped: field.capped,
4149
+ holes: field.holeCount,
4150
+ reconstructMs: Number(reconstructMs.toFixed(1)),
4151
+ totalMs: Number((performance.now() - started).toFixed(1))
4152
+ });
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
+ };
3618
4171
  }
3619
- function renderScalarPointOverlays(session, overlays) {
4172
+ function renderScalarSurfaceOverlays(session, overlays) {
4173
+ const started = performance.now();
3620
4174
  const r = getRenderer(session.size, session.pixelRatio);
3621
4175
  const overlayById = new Map(overlays.map((overlay) => [overlay.renderable.id, overlay]));
3622
4176
  const replacements = session.renderables.map((renderable) => {
@@ -3633,62 +4187,108 @@ function renderScalarPointOverlays(session, overlays) {
3633
4187
  ghostMaterial.toneMapped = false;
3634
4188
  renderable.solid.material = ghostMaterial;
3635
4189
  const overlay = overlayById.get(renderable.id);
3636
- if (!overlay || overlay.positions.length === 0)
3637
- return { renderable, previousMaterial, ghostMaterial, points: null, geometry: null, material: null };
4190
+ if (!overlay || overlay.positions.length === 0) {
4191
+ return { renderable, previousMaterial, ghostMaterial, mesh: null, geometry: null, material: null, lut: null };
4192
+ }
3638
4193
  const geometry = new BufferGeometry();
3639
4194
  geometry.setAttribute("position", new BufferAttribute(overlay.positions, 3));
3640
- geometry.setAttribute("color", new BufferAttribute(overlay.colors, 3));
3641
- const material = new PointsMaterial({
3642
- size: 3,
3643
- sizeAttenuation: false,
3644
- vertexColors: true,
3645
- depthTest: true,
3646
- depthWrite: false,
3647
- clippingPlanes: renderable.solidMaterial.clippingPlanes ?? void 0
4195
+ geometry.setAttribute("normal", new BufferAttribute(overlay.normals, 3));
4196
+ geometry.setAttribute("aValue", new BufferAttribute(overlay.aValue, 1));
4197
+ geometry.setIndex(new BufferAttribute(overlay.index, 1));
4198
+ const lut = makeColorScaleTexture(colorScaleLUT(overlay.colorScale.colormap));
4199
+ trackTextureDecode(lut);
4200
+ const uniforms = makeInspectScalarUniforms({
4201
+ colorScale: lut,
4202
+ domainMin: overlay.colorScale.domainMin,
4203
+ domainMax: overlay.colorScale.domainMax,
4204
+ quantizeBands: overlay.fieldParams.quantizeBands,
4205
+ isoEnabled: overlay.fieldParams.isoEnabled,
4206
+ isoSpacing: overlay.fieldParams.isoSpacing,
4207
+ isoLineWidthPx: overlay.fieldParams.isoLineWidthPx,
4208
+ criticalEnabled: overlay.fieldParams.criticalEnabled,
4209
+ criticalThreshold: overlay.fieldParams.criticalThreshold,
4210
+ shadingEnabled: overlay.fieldParams.shadingEnabled
4211
+ });
4212
+ const clippingPlanes = renderable.solidMaterial.clippingPlanes ?? void 0;
4213
+ const material = new ShaderMaterial({
4214
+ vertexShader: INSPECT_SCALAR_VERTEX_SHADER,
4215
+ fragmentShader: INSPECT_SCALAR_FRAGMENT_SHADER,
4216
+ uniforms,
4217
+ side: DoubleSide,
4218
+ clippingPlanes,
4219
+ // Engage the shader's `#include <clipping_planes_*>` chunks only when section
4220
+ // planes are actually present (NUM_CLIPPING_PLANES define is gated on this),
4221
+ // so heatmap surfaces honor section cuts the same as the ghosted solid does.
4222
+ clipping: Boolean(clippingPlanes && clippingPlanes.length > 0)
3648
4223
  });
3649
4224
  material.toneMapped = false;
3650
- const points = new Points(geometry, material);
3651
- points.renderOrder = 5;
3652
- points.raycast = () => null;
3653
- renderable.root.add(points);
3654
- return { renderable, previousMaterial, ghostMaterial, points, geometry, material };
4225
+ const mesh = new Mesh(geometry, material);
4226
+ mesh.renderOrder = 5;
4227
+ mesh.raycast = () => null;
4228
+ renderable.root.add(mesh);
4229
+ return { renderable, previousMaterial, ghostMaterial, mesh, geometry, material, lut };
3655
4230
  });
3656
4231
  try {
3657
- return withSolidOnlyVisibility(
4232
+ const png = withSolidOnlyVisibility(
3658
4233
  session,
3659
4234
  () => withTemporarySceneBackground(session, new Color(0), () => {
3660
4235
  r.render(session.scene, session.camera);
3661
4236
  return captureRenderedPng(session.size);
3662
4237
  })
3663
4238
  );
4239
+ inspectProfileLog("scalar-render", {
4240
+ overlays: overlays.length,
4241
+ totalMs: Number((performance.now() - started).toFixed(1))
4242
+ });
4243
+ return png;
3664
4244
  } finally {
3665
- replacements.forEach(({ renderable, previousMaterial, ghostMaterial, points, geometry, material }) => {
3666
- if (points) renderable.root.remove(points);
4245
+ replacements.forEach(({ renderable, previousMaterial, ghostMaterial, mesh, geometry, material, lut }) => {
4246
+ if (mesh) renderable.root.remove(mesh);
3667
4247
  renderable.solid.material = previousMaterial;
3668
4248
  ghostMaterial.dispose();
3669
4249
  geometry == null ? void 0 : geometry.dispose();
3670
4250
  material == null ? void 0 : material.dispose();
4251
+ lut == null ? void 0 : lut.dispose();
3671
4252
  });
3672
4253
  }
3673
4254
  }
3674
- function getSessionThicknessInspection(session, rawOptions) {
4255
+ function getSessionThicknessInspection(session, rawOptions, fieldOverride) {
3675
4256
  var _a;
3676
4257
  const resolvedOptions = resolveThicknessInspectionOptions(rawOptions);
3677
4258
  const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
3678
- const optionsKey = inspectionOptionsKey({ options, sampleBudget });
4259
+ const fieldParams = mergeScalarFieldParams(fieldOverride);
4260
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
3679
4261
  if (((_a = session.thicknessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.thicknessInspection;
3680
4262
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
3681
4263
  const warnings = [];
3682
4264
  maybePushSceneSampleBudgetWarning(warnings, "Thickness", sampleBudget);
4265
+ const thicknessColorScale = {
4266
+ colormap: DEFAULT_THICKNESS_COLOR_SCALE.colormap,
4267
+ domainMin: options.colorMinThickness,
4268
+ domainMax: options.colorMaxThickness
4269
+ };
3683
4270
  const objects = [];
3684
4271
  const cloudObjects = [];
3685
4272
  const points = [];
3686
4273
  const overlays = [];
4274
+ const connectivityStarted = performance.now();
3687
4275
  const raycastConnectivity = buildThicknessRaycastConnectivityContext(session);
4276
+ inspectProfileLog("thickness-connectivity", {
4277
+ ms: Number((performance.now() - connectivityStarted).toFixed(1)),
4278
+ neighborSets: raycastConnectivity.neighborIdsByObjectId.size
4279
+ });
3688
4280
  session.renderables.forEach((renderable, index) => {
4281
+ var _a2;
3689
4282
  const sourceObject = byId.get(renderable.id);
3690
- const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, {
3691
- connectedGeometries: connectedThicknessGeometriesFor(raycastConnectivity, renderable)
4283
+ const connectedGeometries = connectedThicknessGeometriesFor(raycastConnectivity, renderable);
4284
+ const analysisStarted = performance.now();
4285
+ const analysis = analyzeThicknessGeometry(renderable.solid.geometry, options, { connectedGeometries });
4286
+ inspectProfileLog("thickness-analysis", {
4287
+ object: renderable.name,
4288
+ samples: analysis.pointSamples.length,
4289
+ triangles: analysis.triangleCount,
4290
+ connectedGeometries: connectedGeometries.length,
4291
+ ms: Number((performance.now() - analysisStarted).toFixed(1))
3692
4292
  });
3693
4293
  const bbox = bboxFromGeometry(analysis.geometry);
3694
4294
  const summary = summarizeThicknessSamples(analysis.samples, options);
@@ -3727,7 +4327,27 @@ function getSessionThicknessInspection(session, rawOptions) {
3727
4327
  ...sample
3728
4328
  });
3729
4329
  });
3730
- overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples) });
4330
+ const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
4331
+ if (trianglePositions instanceof Float32Array) {
4332
+ const built = buildScalarSurfaceOverlay(
4333
+ renderable,
4334
+ trianglePositions,
4335
+ analysis.pointSamples,
4336
+ { min: thicknessColorScale.domainMin, max: thicknessColorScale.domainMax },
4337
+ fieldParams
4338
+ );
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
+ }
4349
+ }
4350
+ }
3731
4351
  analysis.geometry.dispose();
3732
4352
  });
3733
4353
  const state = {
@@ -3750,7 +4370,7 @@ function getSessionThicknessInspection(session, rawOptions) {
3750
4370
  objects,
3751
4371
  warnings,
3752
4372
  style: {
3753
- gradientColors: THICKNESS_GRADIENT_COLORS.map((color) => [...color]),
4373
+ colorScale: thicknessColorScale,
3754
4374
  colorMinThickness: options.colorMinThickness,
3755
4375
  colorMaxThickness: options.colorMaxThickness,
3756
4376
  unknownColor: THICKNESS_COLORS.unknown
@@ -3760,21 +4380,22 @@ function getSessionThicknessInspection(session, rawOptions) {
3760
4380
  session.thicknessInspection = state;
3761
4381
  return state;
3762
4382
  }
3763
- function renderCurrentThickness(session, rawOptions) {
3764
- const state = getSessionThicknessInspection(session, rawOptions);
3765
- return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
4383
+ function renderCurrentThickness(session, rawOptions, fieldOverride) {
4384
+ const state = getSessionThicknessInspection(session, rawOptions, fieldOverride);
4385
+ return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
3766
4386
  }
3767
4387
  const ROUGHNESS_SMOOTH_OPACITY = 0.16;
3768
4388
  const ROUGHNESS_HARSH_OPACITY = 1;
3769
- function renderCurrentRoughness(session, rawOptions) {
3770
- const state = getSessionRoughnessInspection(session, rawOptions);
3771
- return { png: renderScalarPointOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
4389
+ function renderCurrentRoughness(session, rawOptions, fieldOverride) {
4390
+ const state = getSessionRoughnessInspection(session, rawOptions, fieldOverride);
4391
+ return { png: renderScalarSurfaceOverlays(session, state.overlays), report: state.report, pointCloud: state.pointCloud };
3772
4392
  }
3773
- function getSessionRoughnessInspection(session, rawOptions) {
4393
+ function getSessionRoughnessInspection(session, rawOptions, fieldOverride) {
3774
4394
  var _a;
3775
4395
  const resolvedOptions = resolveRoughnessInspectionOptions(rawOptions);
3776
4396
  const { options, sampleBudget } = withSceneSampleBudget(session, resolvedOptions, (rawOptions == null ? void 0 : rawOptions.maxSamplesPerObject) !== void 0);
3777
- const optionsKey = inspectionOptionsKey({ options, sampleBudget });
4397
+ const fieldParams = mergeScalarFieldParams(fieldOverride);
4398
+ const optionsKey = inspectionOptionsKey({ options, sampleBudget, fieldParams });
3778
4399
  if (((_a = session.roughnessInspection) == null ? void 0 : _a.optionsKey) === optionsKey) return session.roughnessInspection;
3779
4400
  const byId = new Map(session.objects.map((obj) => [obj.id, obj]));
3780
4401
  const warnings = [];
@@ -3783,8 +4404,23 @@ function getSessionRoughnessInspection(session, rawOptions) {
3783
4404
  const cloudObjects = [];
3784
4405
  const points = [];
3785
4406
  const overlays = [];
3786
- session.renderables.forEach((renderable, index) => {
4407
+ let roughnessMin = Infinity;
4408
+ let roughnessMax = -Infinity;
4409
+ const analysesByIndex = [];
4410
+ session.renderables.forEach((renderable) => {
3787
4411
  const analysis = analyzeRoughnessGeometry(renderable.solid.geometry, options);
4412
+ analysesByIndex.push(analysis);
4413
+ for (const sample of analysis.pointSamples) {
4414
+ if (sample.value != null && Number.isFinite(sample.value)) {
4415
+ if (sample.value < roughnessMin) roughnessMin = sample.value;
4416
+ if (sample.value > roughnessMax) roughnessMax = sample.value;
4417
+ }
4418
+ }
4419
+ });
4420
+ const roughnessColorScale = Number.isFinite(roughnessMin) && roughnessMax > roughnessMin ? { colormap: DEFAULT_ROUGHNESS_COLOR_SCALE.colormap, domainMin: roughnessMin, domainMax: roughnessMax } : DEFAULT_ROUGHNESS_COLOR_SCALE;
4421
+ session.renderables.forEach((renderable, index) => {
4422
+ var _a2;
4423
+ const analysis = analysesByIndex[index];
3788
4424
  const bbox = bboxFromGeometry(analysis.geometry);
3789
4425
  const sourceObject = byId.get(renderable.id);
3790
4426
  if (analysis.warnings.length > 0) {
@@ -3819,7 +4455,21 @@ function getSessionRoughnessInspection(session, rawOptions) {
3819
4455
  ...sample
3820
4456
  });
3821
4457
  });
3822
- overlays.push({ renderable, ...pointBuffersFromSamples(analysis.pointSamples) });
4458
+ const trianglePositions = (_a2 = analysis.geometry.getAttribute("position")) == null ? void 0 : _a2.array;
4459
+ if (trianglePositions instanceof Float32Array) {
4460
+ const built = buildScalarSurfaceOverlay(renderable, trianglePositions, analysis.pointSamples, null, fieldParams);
4461
+ if (built) {
4462
+ overlays.push(built.overlay);
4463
+ if (built.capped) {
4464
+ warnings.push(
4465
+ `${renderable.name}: scalar surface hit the ${SCALAR_SURFACE_VERTEX_CAP.toLocaleString()} vertex cap; raise the sample budget or reduce object size for full resolution.`
4466
+ );
4467
+ }
4468
+ if (built.holeCount > 0) {
4469
+ warnings.push(`${renderable.name}: ${built.holeCount} surface vertices had no in-gate sample (filled by nearest neighbor).`);
4470
+ }
4471
+ }
4472
+ }
3823
4473
  analysis.geometry.dispose();
3824
4474
  });
3825
4475
  const state = {
@@ -3842,6 +4492,9 @@ function getSessionRoughnessInspection(session, rawOptions) {
3842
4492
  objects,
3843
4493
  warnings,
3844
4494
  style: {
4495
+ // Legacy class colors kept for back-compat; colorScale is the truth the
4496
+ // viewport/CLI now render with (continuous viridis over the data domain).
4497
+ colorScale: roughnessColorScale,
3845
4498
  smoothColor: ROUGHNESS_COLORS.smooth,
3846
4499
  moderateColor: ROUGHNESS_COLORS.moderate,
3847
4500
  sharpColor: ROUGHNESS_COLORS.sharp,
@@ -4968,6 +5621,60 @@ Available renderable objects: ${available}`;
4968
5621
  }
4969
5622
  return "No visible renderable objects found.";
4970
5623
  }
5624
+ const PENDING_TEXTURE_DECODES = /* @__PURE__ */ new Set();
5625
+ function trackTextureDecode(texture) {
5626
+ PENDING_TEXTURE_DECODES.add(textureDecoded(texture));
5627
+ }
5628
+ function textureDecoded(texture) {
5629
+ return new Promise((resolve) => {
5630
+ var _a;
5631
+ const finish = () => {
5632
+ texture.needsUpdate = true;
5633
+ resolve();
5634
+ };
5635
+ const forgeDecoded = (_a = texture.userData) == null ? void 0 : _a.forgeDecoded;
5636
+ if (forgeDecoded) {
5637
+ forgeDecoded.then(
5638
+ () => finish(),
5639
+ () => finish()
5640
+ );
5641
+ return;
5642
+ }
5643
+ const image = texture.image;
5644
+ if (image && typeof ImageBitmap !== "undefined" && image instanceof ImageBitmap) {
5645
+ finish();
5646
+ return;
5647
+ }
5648
+ if (image && typeof image.decode === "function") {
5649
+ image.decode().then(finish, finish);
5650
+ return;
5651
+ }
5652
+ if (image && (image.naturalWidth > 0 || image.complete && image.width > 0)) {
5653
+ finish();
5654
+ return;
5655
+ }
5656
+ if (image && "onload" in image) {
5657
+ const prevLoad = image.onload;
5658
+ const prevError = image.onerror;
5659
+ image.onload = (ev) => {
5660
+ if (typeof prevLoad === "function") prevLoad.call(image, ev);
5661
+ finish();
5662
+ };
5663
+ image.onerror = (ev) => {
5664
+ if (typeof prevError === "function") prevError.call(image, ev);
5665
+ finish();
5666
+ };
5667
+ return;
5668
+ }
5669
+ finish();
5670
+ });
5671
+ }
5672
+ async function awaitPendingTextureDecodes() {
5673
+ if (PENDING_TEXTURE_DECODES.size === 0) return;
5674
+ const pending = Array.from(PENDING_TEXTURE_DECODES);
5675
+ PENDING_TEXTURE_DECODES.clear();
5676
+ await Promise.all(pending);
5677
+ }
4971
5678
  function createSession(code, opts) {
4972
5679
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
4973
5680
  const size = (opts == null ? void 0 : opts.size) ?? 1024;
@@ -5321,6 +6028,12 @@ Fix one:
5321
6028
  depthWrite: materialOpacity >= 0.99 && materialTransmission <= 0,
5322
6029
  clippingPlanes: applicableCutPlanes
5323
6030
  });
6031
+ const textureDescriptor = mp == null ? void 0 : mp.texture;
6032
+ if (textureDescriptor && solidMaterial instanceof MeshPhysicalMaterial) {
6033
+ const projectedTexture = descriptorToThreeTexture(textureDescriptor.image, { colorSpace: "srgb" });
6034
+ applyProjectedTexture(solidMaterial, textureDescriptor.projection, projectedTexture);
6035
+ trackTextureDecode(projectedTexture);
6036
+ }
5324
6037
  solid = new Mesh(geo.solid, solidMaterial);
5325
6038
  if (isScanRenderStyle) {
5326
6039
  const scanProxyGeometry = createScanProxyGeometry(geo.solid, { grid: scanProxyGrid ?? void 0 });
@@ -5513,6 +6226,7 @@ async function emitInspectProgress(opts, event) {
5513
6226
  }
5514
6227
  window.__forgeRender = async (code, opts) => {
5515
6228
  var _a, _b, _c;
6229
+ setInspectProfileEnabled(opts == null ? void 0 : opts.debug);
5516
6230
  const requestedCameraTokens = (opts == null ? void 0 : opts.cameras) ?? (opts == null ? void 0 : opts.angles);
5517
6231
  const hasDirectionalCameraTokens = Array.isArray(requestedCameraTokens) && requestedCameraTokens.length > 0;
5518
6232
  if ((opts == null ? void 0 : opts.viewName) && hasDirectionalCameraTokens) {
@@ -5530,6 +6244,7 @@ window.__forgeRender = async (code, opts) => {
5530
6244
  const built = createSession(code, {
5531
6245
  size: (opts == null ? void 0 : opts.size) || 1024,
5532
6246
  pixelRatio: (opts == null ? void 0 : opts.pixelRatio) || 1,
6247
+ debug: opts == null ? void 0 : opts.debug,
5533
6248
  quality: opts == null ? void 0 : opts.quality,
5534
6249
  allFiles: opts == null ? void 0 : opts.allFiles,
5535
6250
  binaryFiles: opts == null ? void 0 : opts.binaryFiles,
@@ -5592,6 +6307,7 @@ window.__forgeRender = async (code, opts) => {
5592
6307
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
5593
6308
  }
5594
6309
  }
6310
+ await awaitPendingTextureDecodes();
5595
6311
  await emitInspectProgress(opts, { type: "session-done", objectCount: session.objects.length });
5596
6312
  const renderMode = (opts == null ? void 0 : opts.renderMode) === "wireframe" ? "wireframe" : "solid";
5597
6313
  const edgePreset = (opts == null ? void 0 : opts.edges) ?? "off";
@@ -5717,7 +6433,7 @@ window.__forgeRender = async (code, opts) => {
5717
6433
  }
5718
6434
  if (requestedChannels.has("roughness")) {
5719
6435
  await markChannelViewStart("roughness", label);
5720
- const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness);
6436
+ const roughness = renderCurrentRoughness(session, opts == null ? void 0 : opts.roughness, opts == null ? void 0 : opts.scalarFieldParams);
5721
6437
  roughnessRenders[label] = roughness.png;
5722
6438
  roughnessReport = roughness.report;
5723
6439
  roughnessPointCloud = roughness.pointCloud;
@@ -5772,7 +6488,7 @@ window.__forgeRender = async (code, opts) => {
5772
6488
  }
5773
6489
  if (requestedChannels.has("thickness")) {
5774
6490
  await markChannelViewStart("thickness", label);
5775
- const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness);
6491
+ const thickness = renderCurrentThickness(session, opts == null ? void 0 : opts.thickness, opts == null ? void 0 : opts.scalarFieldParams);
5776
6492
  thicknessRenders[label] = thickness.png;
5777
6493
  thicknessReport = thickness.report;
5778
6494
  thicknessPointCloud = thickness.pointCloud;
@@ -5917,6 +6633,7 @@ window.__forgeCaptureInit = async (code, opts) => {
5917
6633
  if (!built.ok) {
5918
6634
  return built;
5919
6635
  }
6636
+ await awaitPendingTextureDecodes();
5920
6637
  captureSession = built.session;
5921
6638
  return {
5922
6639
  ok: true,