forgecad 0.9.15 → 0.10.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 (166) hide show
  1. package/dist/assets/{AdminPage-CDyGUinA.js → AdminPage-DwYHz72L.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DfPMY_-d.js → BenchmarkPage-a9_f-1US.js} +1 -1
  3. package/dist/assets/{BlogPage-kF0fkdJT.js → BlogPage-DodHpvmf.js} +1 -1
  4. package/dist/assets/{DocsPage-B954L3YN.js → DocsPage-B5LePEuj.js} +8 -858
  5. package/dist/assets/{EditorApp-CuDLxKqL.css → EditorApp-BpjZgzk0.css} +148 -0
  6. package/dist/assets/EditorApp-QXsAISLR.js +16307 -0
  7. package/dist/assets/{EmbedViewer-C77B-TrF.js → EmbedViewer-DdEHGUMU.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-Cr6fXMDj.js → LandingPageProofDriven-yhhOodbf.js} +2 -2
  9. package/dist/assets/{LegalPage-Dzklqmmg.js → LegalPage-5RbKRGYK.js} +1 -1
  10. package/dist/assets/{PricingPage-zWXkvlwl.js → PricingPage-E3Rma7aV.js} +1 -1
  11. package/dist/assets/{SettingsPage-Bz0of4KQ.js → SettingsPage-BJZcM97j.js} +1 -1
  12. package/dist/assets/{app-D3kDkggg.js → app-DSYrDg0V.js} +1846 -352
  13. package/dist/assets/cli/{render-DSY3mMQa.js → render-ZMHR9HkV.js} +161 -70
  14. package/dist/assets/{constructionHistoryWorker-gpDo-uH2.js → constructionHistoryWorker-AwMMWSxg.js} +1104 -349
  15. package/dist/assets/{evalWorker-CU0Ke6DP.js → evalWorker-DbNs7Dkp.js} +5155 -3772
  16. package/dist/assets/{inspectWorker-COyp8XXA.js → inspectWorker-CZsCFtQT.js} +1415 -439
  17. package/dist/assets/{targets-B9sGB5nB.js → jointPose-DO6mnXn_.js} +71 -3
  18. package/dist/assets/{manifold-DNkrUWpA.js → manifold-BGlQBBH9.js} +1 -1
  19. package/dist/assets/{manifold-BRI5prcH.js → manifold-BU-tJwQh.js} +1 -1
  20. package/dist/assets/{manifold-C-3h2M7p.js → manifold-fy2MV7K1.js} +2 -2
  21. package/dist/assets/{reportWorker-CdBz5bNg.js → reportWorker-DO6hcQbh.js} +8474 -4549
  22. package/dist/assets/{scalar-sampling-budget-wJF98aY9.js → scalar-sampling-budget-o90NSNmF.js} +5347 -3906
  23. package/dist/assets/{scanProxyWorker-B-9VbLIs.js → scanProxyWorker-2GtDLk-R.js} +19 -6
  24. package/dist/assets/{javascript-1kQXfVaz.js → typescript-DBQ6RN5l.js} +874 -22
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +3 -3
  27. package/dist/docs-raw/AI/usage.md +3 -1
  28. package/dist/docs-raw/CLI.md +65 -239
  29. package/dist/docs-raw/README.md +6 -0
  30. package/dist/docs-raw/component-model.md +17 -150
  31. package/dist/docs-raw/generated/assembly.md +159 -520
  32. package/dist/docs-raw/generated/concepts.md +245 -3491
  33. package/dist/docs-raw/generated/core.md +277 -1251
  34. package/dist/docs-raw/generated/curves.md +387 -1608
  35. package/dist/docs-raw/generated/legacy.md +162 -0
  36. package/dist/docs-raw/generated/lib.md +238 -112
  37. package/dist/docs-raw/generated/output.md +51 -76
  38. package/dist/docs-raw/generated/runtime-names.md +30 -22
  39. package/dist/docs-raw/generated/sdf.md +68 -284
  40. package/dist/docs-raw/generated/sheet-metal.md +68 -335
  41. package/dist/docs-raw/generated/sketch.md +240 -1161
  42. package/dist/docs-raw/generated/viewport.md +75 -316
  43. package/dist/docs-raw/generated/wood.md +21 -49
  44. package/dist/docs-raw/guides/coordinate-system.md +4 -42
  45. package/dist/docs-raw/guides/inspection-bundles.md +44 -442
  46. package/dist/docs-raw/guides/joint-design.md +18 -79
  47. package/dist/docs-raw/guides/positioning.md +21 -143
  48. package/dist/docs-raw/guides/scene-presentation.md +89 -0
  49. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  50. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  51. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  52. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  53. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  54. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  55. package/dist/docs-raw/skills/forgecad-make-a-model.md +113 -532
  56. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  57. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  58. package/dist/docs-raw/skills/forgecad-project.md +13 -129
  59. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  60. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  61. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  62. package/dist/docs-raw/skills/forgecad.md +19 -18
  63. package/dist/docs-raw/skills/index.md +2 -0
  64. package/dist/docs-raw/welcome.md +4 -2
  65. package/dist/index.html +1 -1
  66. package/dist/llms.txt +1 -2
  67. package/dist/sitemap.xml +13 -13
  68. package/dist-cli/{check-compiler-SDX5QIXI.js → check-compiler-JTVBITCR.js} +1 -1
  69. package/dist-cli/{check-query-propagation-EAYEFT77.js → check-query-propagation-3FFLSMVN.js} +1 -1
  70. package/dist-cli/{chunk-N4O47JLF.js → chunk-OAN5T4XD.js} +5722 -4287
  71. package/dist-cli/forgecad.js +2195 -656
  72. package/dist-skill/CONTEXT.md +1778 -7912
  73. package/dist-skill/SKILL.md +15 -15
  74. package/dist-skill/docs/API/core/concepts.md +27 -157
  75. package/dist-skill/docs/CLI.md +65 -239
  76. package/dist-skill/docs/generated/assembly.md +160 -493
  77. package/dist-skill/docs/generated/core.md +277 -1251
  78. package/dist-skill/docs/generated/curves.md +387 -1609
  79. package/dist-skill/docs/generated/lib.md +238 -112
  80. package/dist-skill/docs/generated/output.md +51 -76
  81. package/dist-skill/docs/generated/runtime-names.md +16 -22
  82. package/dist-skill/docs/generated/sdf.md +68 -284
  83. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  84. package/dist-skill/docs/generated/sketch.md +240 -1160
  85. package/dist-skill/docs/generated/viewport.md +75 -223
  86. package/dist-skill/docs/generated/wood.md +21 -49
  87. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  88. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  89. package/dist-skill/docs/guides/joint-design.md +18 -79
  90. package/dist-skill/docs/guides/positioning.md +21 -143
  91. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  92. package/dist-skill/docs/guides/surface-members.md +26 -0
  93. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  94. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  95. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  96. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  97. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  98. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  99. package/dist-skill/library/forgecad-make-a-model/SKILL.md +111 -532
  100. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  101. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  102. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  103. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  104. package/dist-skill/library/forgecad-project/SKILL.md +13 -131
  105. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  106. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  107. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  108. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  109. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  110. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  111. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  112. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  113. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  114. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  115. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  116. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  117. package/dist-skill/website/skills/forgecad-project.md +26 -0
  118. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  119. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  120. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  121. package/dist-skill/website/skills/forgecad.md +122 -0
  122. package/dist-skill/website/skills/index.md +26 -0
  123. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  124. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  125. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  126. package/examples/api/extrude-options.forge.js +4 -2
  127. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  128. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  129. package/examples/api/helix-basics.forge.js +2 -2
  130. package/examples/api/highlight-debug.forge.js +10 -10
  131. package/examples/api/mesh-import-slats.forge.js +1 -1
  132. package/examples/api/real-product-curves.forge.js +1 -1
  133. package/examples/api/route3d-elbow.forge.js +3 -0
  134. package/examples/api/sculpt-box-circle-booleans.forge.js +1 -1
  135. package/examples/api/sdf-shapes.forge.js +2 -5
  136. package/examples/api/sketch-rounding-strategies.forge.js +6 -6
  137. package/examples/api/surface-member-bottle-cage.forge.js +3 -3
  138. package/examples/api/surface-member-conformal-product-ribbon.forge.js +3 -3
  139. package/examples/api/surface-member-razor-inlay.forge.js +1 -1
  140. package/examples/api/variable-sweep-test.forge.js +4 -2
  141. package/examples/mechanical/airplane-propeller.forge.js +74 -39
  142. package/examples/nurbs-surface.forge.js +1 -1
  143. package/examples/products/iphone.forge.js +1 -1
  144. package/package.json +4 -1
  145. package/dist/assets/EditorApp-Beb-IZ0y.js +0 -14014
  146. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  147. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  148. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  149. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  150. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
  151. package/examples/api/bolted-service-cover.forge.js +0 -17
  152. package/examples/api/cable-gland-anchor.forge.js +0 -14
  153. package/examples/api/captured-cartridge-guide.forge.js +0 -14
  154. package/examples/api/captured-linear-slide.forge.js +0 -13
  155. package/examples/api/clevis-pin-joint.forge.js +0 -13
  156. package/examples/api/datum-enclosure.forge.js +0 -16
  157. package/examples/api/hose-barb-port.forge.js +0 -14
  158. package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
  159. package/examples/api/living-hinge-cover.forge.js +0 -14
  160. package/examples/api/pcb-terminal-block.forge.js +0 -22
  161. package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
  162. package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
  163. package/examples/api/routed-tube-clip.forge.js +0 -15
  164. package/examples/api/seated-bearing-stack.forge.js +0 -30
  165. package/examples/api/snap-latch-cover.forge.js +0 -14
  166. package/examples/api/thumb-screw-clamp.forge.js +0 -15
@@ -1,10 +1,10 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/EditorApp-CuDLxKqL.css","assets/landing-proof-driven-ORyigZ6p.css","assets/BenchmarkPage-BAbsyMaF.css","assets/PricingPage-BPF6HKyO.css","assets/LegalPage-BRlScr9A.css"])))=>i.map(i=>d[i]);
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/EditorApp-BpjZgzk0.css","assets/landing-proof-driven-ORyigZ6p.css","assets/BenchmarkPage-BAbsyMaF.css","assets/PricingPage-BPF6HKyO.css","assets/LegalPage-BRlScr9A.css"])))=>i.map(i=>d[i]);
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
  var _a2;
6
6
  import { c as create, j as jsxRuntimeExports, r as reactExports, a as createWithEqualityFn, R as React, T as Tb, s as schedulerExports, b as clientExports, d as reactDomExports, u as useParams, e as useSearchParams, f as useNavigate, L as Link, g as useLocation, B as BrowserRouter, h as Routes, i as Route, N as Navigate } from "./vendor-react-6j1Kke-Y.js";
7
- import { _ as __vitePreload, z as zipSync, s as strToU8, W as WebGLRenderer, R as Raycaster, O as OrthographicCamera$1, P as PerspectiveCamera$1, S as Scene, a as PCFSoftShadowMap, V as VSMShadowMap, b as PCFShadowMap, B as BasicShadowMap, C as ColorManagement, L as LinearSRGBColorSpace, c as SRGBColorSpace, N as NoToneMapping, A as ACESFilmicToneMapping, d as Layers, e as Color, f as RGBAFormat, U as UnsignedByteType, g as Vector3, h as Vector2, i as Clock, T as THREE, D as DoubleSide, j as REVISION, M as Mesh, I as IcosahedronGeometry, k as ShaderMaterial, l as Spherical, Q as Quaternion, m as MOUSE, n as TOUCH, o as Ray, p as Plane, q as DataTextureLoader, H as HalfFloatType, F as FloatType, r as DataUtils, t as LinearFilter, u as RedFormat, v as InstancedBufferGeometry, w as Float32BufferAttribute, x as InstancedInterleavedBuffer, y as InterleavedBufferAttribute, E as WireframeGeometry, G as Box3, J as Sphere, K as UniformsUtils, X as UniformsLib, Y as Vector4, Z as Line3, $ as Matrix4, a0 as MathUtils, a1 as Uniform, a2 as WebGLRenderTarget, a3 as DepthTexture, a4 as BackSide, a5 as ClampToEdgeWrapping, a6 as PlaneGeometry, a7 as UVMapping, a8 as DataTexture, a9 as Texture, aa as MeshBasicMaterial, ab as IntType, ac as ShortType, ad as ByteType, ae as UnsignedIntType, af as Loader, ag as LoadingManager, ah as LinearMipMapLinearFilter, ai as FileLoader, aj as NoBlending, ak as CubeReflectionMapping, al as EquirectangularReflectionMapping, am as CubeTextureLoader, an as WebGLCubeRenderTarget, ao as ConstraintSketch, ap as setSketchPlacement3D, aq as Sketch, ar as PROFILE_BACKEND_MARKER, as as FrozenShape, at as setShapeCompilePlan, au as hasAnyPorts, av as setShapePortsInternal, aw as markShapePortsUsed, ax as setParamOverrides, ay as resolveForgeRenderStyle, az as DEFAULT_ACTIVE_BACKEND, aA as isConstraintSketch, aB as updateConstraintValue, aC as linearizeMultiObjectSteps, aD as getShapeCompilePlan, aE as SCAN_PROXY_GRANULARITY_DEFAULT, aF as publishSolverWasmRunDebug, aG as resolveForgeQualityPreset, aH as SCAN_PROXY_MATRIX_GRANULARITY_MAX, aI as findJointAnimationClip, aJ as resolveJointAnimation, aK as resolveJointViewValues, aL as resolveImportPath, aM as resolveScanProxyGranularity, aN as BufferGeometry, aO as LineBasicMaterial, aP as Line$1, aQ as LineDashedMaterial, aR as DepthStencilFormat, aS as UnsignedInt248Type, aT as MeshNormalMaterial, aU as NearestFilter, aV as BasicDepthPacking, aW as EventDispatcher$1, aX as NoColorSpace, aY as FrontSide, aZ as Material, a_ as AlwaysDepth, a$ as BufferAttribute, b0 as CanvasTexture, b1 as Object3D, b2 as FogExp2, b3 as Fog, b4 as AmbientLight, b5 as HemisphereLight, b6 as SpotLight, b7 as PointLight, b8 as DirectionalLight, b9 as heatPointsForSide, ba as COMPARISON_COLORS, bb as comparisonHeatDepthTest, bc as comparisonHeatEdgeOpacity, bd as comparisonHeatPatchOpacity, be as shapeToGeometry, bf as buildComparisonHeatPatchGeometry, bg as EdgesGeometry, bh as buildShapeFromCompilePlan, bi as buildVisibleHistoryStacks, bj as sketchToSvg, bk as sketchToDxf, bl as runScript, bm as MeshPhysicalMaterial, bn as LineSegments, bo as findDesignTraceNodeForConstructionStep, bp as formatDesignTraceAnchor, bq as waitForAnimationFrame, br as selectBuildLedgerNodes, bs as worldAuthorPlaneToLocal, bt as scanProxySourceBytes, bu as disposeScanProxyGeometry, bv as scanProxyGeometryFromPayload, bw as AdditiveBlending, bx as geometryWithVisibleVertexColors, by as getRenderStylePreset, bz as SCAN_RENDER_COLORS, bA as NormalBlending, bB as ZEBRA_STRIPE_SOFTNESS, bC as ZEBRA_STRIPE_SCALE, bD as ZEBRA_LIGHT_COLOR, bE as ZEBRA_DARK_COLOR, bF as ZEBRA_ACCENT_COLOR, bG as ZEBRA_STRIPE_FRAGMENT_SHADER, bH as ZEBRA_STRIPE_VERTEX_SHADER, bI as SCAN_PROXY_LAYER_STYLES, bJ as SURFACE_FIELD_FRAGMENT_SHADER, bK as SURFACE_FIELD_VERTEX_SHADER, bL as WORLD_UP, bM as CatmullRomCurve3, bN as TubeGeometry, bO as DEFAULT_ROUGHNESS_INSPECTION_OPTIONS, bP as ROUGHNESS_COLORS, bQ as DEFAULT_THICKNESS_INSPECTION_OPTIONS, bR as THICKNESS_COLORS, bS as PERFORMANCE_SAMPLE_INTERVAL_SEC, bT as formatPerformanceCount, bU as NON_TEXT_INPUT_TYPES, bV as MeshStandardMaterial, bW as compileSdfNode3, bX as buildSdfRaymarchFragmentShader, bY as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bZ as Shape, b_ as ShapeGeometry, b$ as ShaderLib, c0 as CylinderGeometry, c1 as VIEWPORT_CAMERA_STORAGE_KEY, c2 as parseViewportCameraState, c3 as createResolvedExplodeConfig, c4 as explodeBoundsCenter, c5 as explodeMergeBounds, c6 as resolveExplodeDirective, c7 as computeExplodeMotion, c8 as getSketchWorldMatrix, c9 as explodeAdd, ca as hasExplodeOverride, cb as resolveExplodeLocalFanDirection, cc as explodeMul, cd as explodeLeafFanStage, ce as normalizeCutPlane, cf as toClippingPlane, cg as isObjectExcludedFromCutPlane, ch as getShapePorts, ci as getShapeUsedPorts, cj as DEFAULT_VIEW_CONFIG, ck as SECTION_EXPLORER_PLANE_NAME, cl as ZERO_OFFSET, cm as scanProxyGridForBounds, cn as IDENTITY_MATRIX$2, co as OBJECT_CONTEXT_MENU_MARGIN, cp as OBJECT_CONTEXT_MENU_WIDTH, cq as OBJECT_CONTEXT_MENU_HEIGHT, cr as getKernelFaceNameForTriangle, cs as triangleSoupFromMeshes, ct as compareTriangleSoups, cu as buildGeometryComparisonPointCloud, cv as aabbOverlaps, cw as aabbOverlapVolume, cx as DEFAULT_COLLISION_INSPECTION_OPTIONS, cy as resolveScalarSceneSampleBudget, cz as FOCUS_MODE_DIM_OPACITY, cA as comparisonCandidateContextOpacity, cB as initKernel, cC as initSolverWasm } from "./scalar-sampling-budget-wJF98aY9.js";
7
+ import { _ as __vitePreload, z as zipSync, s as strToU8, W as WebGLRenderer, R as Raycaster, O as OrthographicCamera$1, P as PerspectiveCamera$1, S as Scene, a as PCFSoftShadowMap, V as VSMShadowMap, b as PCFShadowMap, B as BasicShadowMap, C as ColorManagement, L as LinearSRGBColorSpace, c as SRGBColorSpace, N as NoToneMapping, A as ACESFilmicToneMapping, d as Layers, e as Color, f as RGBAFormat, U as UnsignedByteType, g as Vector3, h as Vector2, i as Clock, T as THREE, D as DoubleSide, j as REVISION, M as Mesh, I as IcosahedronGeometry, k as ShaderMaterial, l as Spherical, Q as Quaternion, m as MOUSE, n as TOUCH, o as Ray, p as Plane, q as DataTextureLoader, H as HalfFloatType, F as FloatType, r as DataUtils, t as LinearFilter, u as RedFormat, v as InstancedBufferGeometry, w as Float32BufferAttribute, x as InstancedInterleavedBuffer, y as InterleavedBufferAttribute, E as WireframeGeometry, G as Box3, J as Sphere, K as UniformsUtils, X as UniformsLib, Y as Vector4, Z as Line3, $ as Matrix4, a0 as MathUtils, a1 as Uniform, a2 as WebGLRenderTarget, a3 as DepthTexture, a4 as BackSide, a5 as ClampToEdgeWrapping, a6 as PlaneGeometry, a7 as UVMapping, a8 as DataTexture, a9 as Texture, aa as MeshBasicMaterial, ab as IntType, ac as ShortType, ad as ByteType, ae as UnsignedIntType, af as Loader, ag as LoadingManager, ah as LinearMipMapLinearFilter, ai as FileLoader, aj as NoBlending, ak as CubeReflectionMapping, al as EquirectangularReflectionMapping, am as CubeTextureLoader, an as WebGLCubeRenderTarget, ao as ConstraintSketch, ap as setSketchPlacement3D, aq as Sketch, ar as PROFILE_BACKEND_MARKER, as as FrozenShape, at as setShapeCompilePlan, au as hasAnyPorts, av as setShapePortsInternal, aw as markShapePortsUsed, ax as setParamOverrides, ay as resolveForgeRenderStyle, az as DEFAULT_ACTIVE_BACKEND, aA as isConstraintSketch, aB as updateConstraintValue, aC as linearizeMultiObjectSteps, aD as getShapeCompilePlan, aE as SCAN_PROXY_GRANULARITY_DEFAULT, aF as publishSolverWasmRunDebug, aG as resolveForgeQualityPreset, aH as SCAN_PROXY_MATRIX_GRANULARITY_MAX, aI as findJointAnimationClip, aJ as resolveJointAnimation, aK as resolveJointViewValues, aL as resolveImportPath, aM as resolveScanProxyGranularity, aN as BufferGeometry, aO as LineBasicMaterial, aP as Line$1, aQ as LineDashedMaterial, aR as DepthStencilFormat, aS as UnsignedInt248Type, aT as MeshNormalMaterial, aU as NearestFilter, aV as BasicDepthPacking, aW as EventDispatcher$1, aX as NoColorSpace, aY as FrontSide, aZ as Material, a_ as AlwaysDepth, a$ as BufferAttribute, b0 as CanvasTexture, b1 as Object3D, b2 as FogExp2, b3 as Fog, b4 as AmbientLight, b5 as HemisphereLight, b6 as SpotLight, b7 as PointLight, b8 as DirectionalLight, b9 as heatPointsForSide, ba as COMPARISON_COLORS, bb as comparisonHeatDepthTest, bc as comparisonHeatEdgeOpacity, bd as comparisonHeatPatchOpacity, be as shapeToGeometry, bf as buildComparisonHeatPatchGeometry, bg as EdgesGeometry, bh as buildShapeFromCompilePlan, bi as buildVisibleHistoryStacks, bj as sketchToSvg, bk as sketchToDxf, bl as runScript, bm as MeshPhysicalMaterial, bn as LineSegments, bo as findDesignTraceNodeForConstructionStep, bp as formatDesignTraceAnchor, bq as waitForAnimationFrame, br as selectBuildLedgerNodes, bs as worldAuthorPlaneToLocal, bt as scanProxySourceBytes, bu as disposeScanProxyGeometry, bv as scanProxyGeometryFromPayload, bw as AdditiveBlending, bx as geometryWithVisibleVertexColors, by as getRenderStylePreset, bz as SCAN_RENDER_COLORS, bA as NormalBlending, bB as scanMaterialShellColor, bC as ZEBRA_STRIPE_SOFTNESS, bD as ZEBRA_STRIPE_SCALE, bE as ZEBRA_LIGHT_COLOR, bF as ZEBRA_DARK_COLOR, bG as ZEBRA_ACCENT_COLOR, bH as ZEBRA_STRIPE_FRAGMENT_SHADER, bI as ZEBRA_STRIPE_VERTEX_SHADER, bJ as SCAN_PROXY_LAYER_STYLES, bK as scanMaterialLayerStyles, bL as SURFACE_FIELD_FRAGMENT_SHADER, bM as SURFACE_FIELD_VERTEX_SHADER, bN as WORLD_UP, bO as CatmullRomCurve3, bP as TubeGeometry, bQ as THICKNESS_GRADIENT_COLORS, bR as ROUGHNESS_COLORS, bS as DEFAULT_ROUGHNESS_INSPECTION_OPTIONS, bT as PERFORMANCE_SAMPLE_INTERVAL_SEC, bU as formatPerformanceCount, bV as NON_TEXT_INPUT_TYPES, bW as MeshStandardMaterial, bX as compileSdfNode3, bY as buildSdfRaymarchFragmentShader, bZ as SDF_RAYMARCH_PROXY_VERTEX_SHADER, b_ as Shape, b$ as ShapeGeometry, c0 as ShaderLib, c1 as CylinderGeometry, c2 as VIEWPORT_CAMERA_STORAGE_KEY, c3 as parseViewportCameraState, c4 as createResolvedExplodeConfig, c5 as explodeBoundsCenter, c6 as explodeMergeBounds, c7 as resolveExplodeDirective, c8 as computeExplodeMotion, c9 as getSketchWorldMatrix, ca as explodeAdd, cb as hasExplodeOverride, cc as resolveExplodeLocalFanDirection, cd as explodeMul, ce as explodeLeafFanStage, cf as normalizeCutPlane, cg as toClippingPlane, ch as isObjectExcludedFromCutPlane, ci as getShapePorts, cj as getShapeUsedPorts, ck as DEFAULT_VIEW_CONFIG, cl as SECTION_EXPLORER_PLANE_NAME, cm as ZERO_OFFSET, cn as scanProxyGridForBounds, co as IDENTITY_MATRIX$2, cp as OBJECT_CONTEXT_MENU_MARGIN, cq as OBJECT_CONTEXT_MENU_WIDTH, cr as OBJECT_CONTEXT_MENU_HEIGHT, cs as getKernelFaceNameForTriangle, ct as triangleSoupFromMeshes, cu as compareTriangleSoups, cv as buildGeometryComparisonPointCloud, cw as aabbOverlaps, cx as aabbOverlapVolume, cy as DEFAULT_COLLISION_INSPECTION_OPTIONS, cz as resolveScalarSceneSampleBudget, cA as FOCUS_MODE_DIM_OPACITY, cB as comparisonCandidateContextOpacity, cC as Matrix3, cD as initKernel, cE as initSolverWasm } from "./scalar-sampling-budget-o90NSNmF.js";
8
8
  function getCsrfToken() {
9
9
  const match = document.cookie.match(/(?:^|;\s*)fc-csrf-token=([^;]+)/);
10
10
  return match == null ? void 0 : match[1];
@@ -39,7 +39,15 @@ async function doFetch(url2, init) {
39
39
  credentials: "include",
40
40
  headers: { ...headers, ...init == null ? void 0 : init.headers }
41
41
  });
42
- const body = await res.json();
42
+ const text = await res.text();
43
+ let body = {};
44
+ if (text.trim().length > 0) {
45
+ try {
46
+ body = JSON.parse(text);
47
+ } catch {
48
+ body = { error: text };
49
+ }
50
+ }
43
51
  return { res, body };
44
52
  }
45
53
  async function authFetch(url2, init) {
@@ -288,6 +296,8 @@ function lookupCache(filePath, code, files, paramOverrides, assemblyState, quali
288
296
  if (!entry) return null;
289
297
  if (entry.code !== code || entry.quality !== quality || JSON.stringify(entry.paramOverrides) !== JSON.stringify(paramOverrides) || JSON.stringify(entry.assemblyState) !== JSON.stringify(assemblyState) || JSON.stringify(entry.files) !== JSON.stringify(files))
290
298
  return null;
299
+ runResultCache.delete(key);
300
+ runResultCache.set(key, entry);
291
301
  return entry.result;
292
302
  }
293
303
  function storeCache(filePath, code, files, paramOverrides, assemblyState, quality, backend, result, serialized) {
@@ -14721,6 +14731,7 @@ const MOUSE_BUTTONS_DRAW = {
14721
14731
  MIDDLE: MOUSE.PAN,
14722
14732
  RIGHT: MOUSE.PAN
14723
14733
  };
14734
+ const MOUSE_BUTTONS_VOXEL = MOUSE_BUTTONS_3D;
14724
14735
  const dark = {
14725
14736
  bg: "#0d1117",
14726
14737
  bgPanel: "#161b22",
@@ -15574,7 +15585,7 @@ const movePath = (value, from, to) => {
15574
15585
  if (value.startsWith(`${from}/`)) return `${to}${value.slice(from.length)}`;
15575
15586
  return value;
15576
15587
  };
15577
- const FORGE_IMPORT_RE = /\b(?:importMesh|importStep|importSvgSketch|Import\.dxfSketch|compareWith)\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
15588
+ const FORGE_IMPORT_RE = /\b(?:importMesh|importStep|importSvgSketch|Import\.mesh|Import\.step|Import\.svgSketch|Import\.dxfSketch|compareWith)\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
15578
15589
  const REQUIRE_RE = /\brequire\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
15579
15590
  const ES_IMPORT_RE = /\bfrom\s+(?:"([^"]+)"|'([^']+)')/g;
15580
15591
  const VIRTUAL_MODULES = /* @__PURE__ */ new Set(["forgecad", "@forge/runtime", "@forgecad/runtime"]);
@@ -15592,6 +15603,9 @@ function extractImports(code) {
15592
15603
  importMesh: "forgeMesh",
15593
15604
  importStep: "forgeMesh",
15594
15605
  importSvgSketch: "forgeSvg",
15606
+ "Import.mesh": "forgeMesh",
15607
+ "Import.step": "forgeMesh",
15608
+ "Import.svgSketch": "forgeSvg",
15595
15609
  "Import.dxfSketch": "forgeDxf",
15596
15610
  compareWith: "compareWith"
15597
15611
  };
@@ -15599,7 +15613,9 @@ function extractImports(code) {
15599
15613
  const forgeRe = new RegExp(FORGE_IMPORT_RE.source, FORGE_IMPORT_RE.flags);
15600
15614
  while ((m2 = forgeRe.exec(code)) !== null) {
15601
15615
  const path = m2[1] ?? m2[2];
15602
- const fn = (_a3 = m2[0].match(/\b(importMesh|importStep|importSvgSketch|Import\.dxfSketch|compareWith)/)) == null ? void 0 : _a3[1];
15616
+ const fn = (_a3 = m2[0].match(
15617
+ /\b(importMesh|importStep|importSvgSketch|Import\.mesh|Import\.step|Import\.svgSketch|Import\.dxfSketch|compareWith)/
15618
+ )) == null ? void 0 : _a3[1];
15603
15619
  add(path, kindMap[fn] ?? "forgeMesh");
15604
15620
  }
15605
15621
  const reqRe = new RegExp(REQUIRE_RE.source, REQUIRE_RE.flags);
@@ -15825,7 +15841,7 @@ const CRASH_COOLDOWN_MS = 2e3;
15825
15841
  class EvalWorkerClient {
15826
15842
  constructor(workerFactory = () => new Worker(new URL(
15827
15843
  /* @vite-ignore */
15828
- "/assets/evalWorker-CU0Ke6DP.js",
15844
+ "/assets/evalWorker-DbNs7Dkp.js",
15829
15845
  import.meta.url
15830
15846
  ), { type: "module" })) {
15831
15847
  __publicField(this, "worker", null);
@@ -16070,6 +16086,10 @@ class EvalWorkerClient {
16070
16086
  runIrPlan(options) {
16071
16087
  return this.startCancellableRun(options, "run-ir-plan");
16072
16088
  }
16089
+ clearCaches() {
16090
+ var _a3;
16091
+ (_a3 = this.worker) == null ? void 0 : _a3.postMessage({ type: "clear-caches" });
16092
+ }
16073
16093
  startCancellableRun(options, type) {
16074
16094
  if (this.shouldReplaceWorkerForSupersedingRun()) {
16075
16095
  this.replaceWorkerForSupersedingRun();
@@ -16262,7 +16282,89 @@ const removeParamSnapshotsForFile = (snapshotsByFile, file) => {
16262
16282
  delete next[file];
16263
16283
  return next;
16264
16284
  };
16265
- const IMPORTABLE_PROJECT_TEXT_FILE_EXTS = [".forge.js", ".js", ".svg", ".dxf"];
16285
+ const EDITABLE_PROJECT_TEXT_FILE_EXTS = [
16286
+ ".forge.js",
16287
+ ".sketch.js",
16288
+ ".js",
16289
+ ".mjs",
16290
+ ".cjs",
16291
+ ".jsx",
16292
+ ".ts",
16293
+ ".tsx",
16294
+ ".json",
16295
+ ".md",
16296
+ ".txt",
16297
+ ".svg",
16298
+ ".dxf",
16299
+ ".html",
16300
+ ".css",
16301
+ ".scss",
16302
+ ".yml",
16303
+ ".yaml",
16304
+ ".toml",
16305
+ ".xml",
16306
+ ".csv",
16307
+ ".ini"
16308
+ ];
16309
+ function hasProjectTextFileExtension(path) {
16310
+ const lower = path.toLowerCase();
16311
+ return EDITABLE_PROJECT_TEXT_FILE_EXTS.some((ext) => lower.endsWith(ext));
16312
+ }
16313
+ function projectTextFileTemplate(path) {
16314
+ const lower = path.toLowerCase();
16315
+ if (lower.endsWith(".svg")) {
16316
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
16317
+ <path d="M 10 10 L 90 10 L 90 90 L 10 90 Z" fill="none" stroke="#000" stroke-width="4" />
16318
+ </svg>
16319
+ `;
16320
+ }
16321
+ if (lower.endsWith(".dxf")) {
16322
+ return "0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n8\n0\n90\n4\n70\n1\n10\n0\n20\n0\n10\n100\n20\n0\n10\n100\n20\n60\n10\n0\n20\n60\n0\nENDSEC\n0\nEOF\n";
16323
+ }
16324
+ if (lower.endsWith(".forge.js") || lower.endsWith(".sketch.js")) {
16325
+ return "// 3D Part\n\nreturn box(50, 30, 10);\n";
16326
+ }
16327
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs")) {
16328
+ return "// Shared JS utilities for ForgeCAD.\n\nexport function exampleValue() {\n return 42;\n}\n";
16329
+ }
16330
+ if (lower.endsWith(".md")) return "# Notes\n\n";
16331
+ if (lower.endsWith(".json")) return "{\n \n}\n";
16332
+ return "";
16333
+ }
16334
+ function monacoLanguageForProjectFile(path) {
16335
+ const lower = path.toLowerCase();
16336
+ if (lower.endsWith(".md")) return "markdown";
16337
+ if (lower.endsWith(".json")) return "json";
16338
+ if (lower.endsWith(".ts") || lower.endsWith(".tsx")) return "typescript";
16339
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs") || lower.endsWith(".jsx")) return "javascript";
16340
+ if (lower.endsWith(".html")) return "html";
16341
+ if (lower.endsWith(".css") || lower.endsWith(".scss")) return "css";
16342
+ if (lower.endsWith(".svg") || lower.endsWith(".xml")) return "xml";
16343
+ if (lower.endsWith(".yml") || lower.endsWith(".yaml")) return "yaml";
16344
+ return "plaintext";
16345
+ }
16346
+ function highlightLanguageForProjectFile(path) {
16347
+ const lower = path.toLowerCase();
16348
+ if (lower.endsWith(".md")) return "markdown";
16349
+ if (lower.endsWith(".json")) return "json";
16350
+ if (lower.endsWith(".ts") || lower.endsWith(".tsx")) return "typescript";
16351
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs") || lower.endsWith(".jsx")) return "javascript";
16352
+ if (lower.endsWith(".html") || lower.endsWith(".svg") || lower.endsWith(".xml")) return "xml";
16353
+ if (lower.endsWith(".css") || lower.endsWith(".scss")) return "css";
16354
+ return null;
16355
+ }
16356
+ function projectTextFileMimeType(path) {
16357
+ const lower = path.toLowerCase();
16358
+ if (lower.endsWith(".svg")) return "image/svg+xml";
16359
+ if (lower.endsWith(".dxf")) return "application/dxf";
16360
+ if (lower.endsWith(".json")) return "application/json";
16361
+ if (lower.endsWith(".md")) return "text/markdown";
16362
+ if (lower.endsWith(".html")) return "text/html";
16363
+ if (lower.endsWith(".css") || lower.endsWith(".scss")) return "text/css";
16364
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs") || lower.endsWith(".jsx")) return "text/javascript";
16365
+ return "text/plain";
16366
+ }
16367
+ const IMPORTABLE_PROJECT_TEXT_FILE_EXTS = EDITABLE_PROJECT_TEXT_FILE_EXTS;
16266
16368
  const IMPORTABLE_PROJECT_MESH_FILE_EXTS = [".stl", ".obj", ".3mf"];
16267
16369
  const IMPORTABLE_PROJECT_EXACT_FILE_EXTS = [".step", ".stp"];
16268
16370
  const IMPORTABLE_PROJECT_BINARY_FILE_EXTS = [...IMPORTABLE_PROJECT_MESH_FILE_EXTS, ...IMPORTABLE_PROJECT_EXACT_FILE_EXTS];
@@ -16273,7 +16375,7 @@ function hasImportableExt(path, extensions) {
16273
16375
  return extensions.some((ext) => lower.endsWith(ext));
16274
16376
  }
16275
16377
  function isImportableProjectTextFile(path) {
16276
- return IMPORTABLE_PROJECT_TEXT_FILE_EXTS.some((ext) => path.endsWith(ext));
16378
+ return hasProjectTextFileExtension(path);
16277
16379
  }
16278
16380
  function isImportableProjectMeshFile(path) {
16279
16381
  return hasImportableExt(path, IMPORTABLE_PROJECT_MESH_FILE_EXTS);
@@ -16976,7 +17078,9 @@ if (sharedBundle) {
16976
17078
  }
16977
17079
  }
16978
17080
  const LAST_ACTIVE_FILE_KEY_BASE = "fc-last-active-file";
17081
+ const RECENT_FILES_KEY_BASE = "fc-recent-files";
16979
17082
  const lastActiveFileKey = () => userScopedKey(LAST_ACTIVE_FILE_KEY_BASE);
17083
+ const recentFilesKey = () => userScopedKey(RECENT_FILES_KEY_BASE);
16980
17084
  const readLastActiveFileForUser = (userId) => {
16981
17085
  try {
16982
17086
  return localStorage.getItem(`${LAST_ACTIVE_FILE_KEY_BASE}-${userId}`);
@@ -16984,6 +17088,25 @@ const readLastActiveFileForUser = (userId) => {
16984
17088
  return null;
16985
17089
  }
16986
17090
  };
17091
+ const readRecentFiles = () => {
17092
+ try {
17093
+ if (typeof localStorage === "undefined") return [];
17094
+ const raw = localStorage.getItem(recentFilesKey());
17095
+ if (!raw) return [];
17096
+ const parsed = JSON.parse(raw);
17097
+ if (!Array.isArray(parsed)) return [];
17098
+ return parsed.filter((value) => typeof value === "string" && value.length > 0);
17099
+ } catch {
17100
+ return [];
17101
+ }
17102
+ };
17103
+ const writeRecentFiles = (files) => {
17104
+ try {
17105
+ if (typeof localStorage === "undefined") return;
17106
+ localStorage.setItem(recentFilesKey(), JSON.stringify(files));
17107
+ } catch {
17108
+ }
17109
+ };
16987
17110
  const clampJointValue = (value, min, max2) => {
16988
17111
  let next = Number.isFinite(value) ? value : 0;
16989
17112
  if (min !== void 0) next = Math.max(min, next);
@@ -17208,6 +17331,13 @@ const DEFAULT_INSPECT_POINT_SAMPLE_COUNT = 2e3;
17208
17331
  const DEFAULT_COMPARISON_INSPECT_MODE = "difference-only";
17209
17332
  const DEFAULT_COMPARISON_CANDIDATE_OPACITY = 0.42;
17210
17333
  const DEFAULT_COMPARISON_REFERENCE_OPACITY = 0.2;
17334
+ const THICKNESS_COLOR_RANGE_MIN = 0;
17335
+ const THICKNESS_COLOR_RANGE_MAX = 1e3;
17336
+ const THICKNESS_COLOR_RANGE_MIN_SPAN = 1e-3;
17337
+ const DEFAULT_THICKNESS_COLOR_RANGE = {
17338
+ min: 0,
17339
+ max: 6
17340
+ };
17211
17341
  const DEFAULT_MANUAL_SCENE_SETTINGS = {
17212
17342
  enabled: false,
17213
17343
  backgroundColor: "#f6f7f8",
@@ -17235,6 +17365,21 @@ const resolveClampedNumber = (value, fallback, min, max2) => {
17235
17365
  return Math.max(min, Math.min(max2, numeric));
17236
17366
  };
17237
17367
  const resolveComparisonOpacity = (value, fallback) => resolveClampedNumber(value, fallback, 0, 1);
17368
+ const resolveThicknessColorRange = (value) => {
17369
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17370
+ return DEFAULT_THICKNESS_COLOR_RANGE;
17371
+ }
17372
+ const raw = value;
17373
+ const min = resolveClampedNumber(raw.min, DEFAULT_THICKNESS_COLOR_RANGE.min, THICKNESS_COLOR_RANGE_MIN, THICKNESS_COLOR_RANGE_MAX);
17374
+ const max2 = resolveClampedNumber(raw.max, DEFAULT_THICKNESS_COLOR_RANGE.max, THICKNESS_COLOR_RANGE_MIN, THICKNESS_COLOR_RANGE_MAX);
17375
+ if (max2 - min >= THICKNESS_COLOR_RANGE_MIN_SPAN) return { min, max: max2 };
17376
+ const defaultSpan = DEFAULT_THICKNESS_COLOR_RANGE.max - DEFAULT_THICKNESS_COLOR_RANGE.min;
17377
+ const expandedMax = Math.min(THICKNESS_COLOR_RANGE_MAX, min + defaultSpan);
17378
+ if (expandedMax - min >= THICKNESS_COLOR_RANGE_MIN_SPAN) return { min, max: expandedMax };
17379
+ const expandedMin = Math.max(THICKNESS_COLOR_RANGE_MIN, max2 - defaultSpan);
17380
+ if (max2 - expandedMin >= THICKNESS_COLOR_RANGE_MIN_SPAN) return { min: expandedMin, max: max2 };
17381
+ return DEFAULT_THICKNESS_COLOR_RANGE;
17382
+ };
17238
17383
  const resolveManualSceneSettings = (value) => {
17239
17384
  if (!value || typeof value !== "object" || Array.isArray(value)) {
17240
17385
  return DEFAULT_MANUAL_SCENE_SETTINGS;
@@ -17381,6 +17526,47 @@ const initialActive = (() => {
17381
17526
  return fallback;
17382
17527
  })();
17383
17528
  const INITIAL_SAVED = {};
17529
+ const MAX_RECENT_FILES = 200;
17530
+ function touchRecentFile(recentFiles, files, file) {
17531
+ const availableFiles = new Set(Object.keys(files));
17532
+ const next = [];
17533
+ const seen = /* @__PURE__ */ new Set();
17534
+ if (file && availableFiles.has(file)) {
17535
+ next.push(file);
17536
+ seen.add(file);
17537
+ }
17538
+ for (const recentFile of recentFiles) {
17539
+ if (!availableFiles.has(recentFile) || seen.has(recentFile)) continue;
17540
+ next.push(recentFile);
17541
+ seen.add(recentFile);
17542
+ if (next.length >= MAX_RECENT_FILES) break;
17543
+ }
17544
+ return next;
17545
+ }
17546
+ function pruneRecentFiles(recentFiles, files) {
17547
+ return touchRecentFile(recentFiles, files, "");
17548
+ }
17549
+ function remapRecentFiles(recentFiles, files, oldPath, newPath) {
17550
+ const remapped = recentFiles.map((file) => movePath(file, oldPath, newPath));
17551
+ return pruneRecentFiles(remapped, files);
17552
+ }
17553
+ function persistRecentFiles(recentFiles) {
17554
+ writeRecentFiles(recentFiles);
17555
+ return recentFiles;
17556
+ }
17557
+ function touchAndPersistRecentFile(recentFiles, files, file) {
17558
+ return persistRecentFiles(touchRecentFile(recentFiles, files, file));
17559
+ }
17560
+ function pruneAndPersistRecentFiles(recentFiles, files) {
17561
+ return persistRecentFiles(pruneRecentFiles(recentFiles, files));
17562
+ }
17563
+ function remapAndPersistRecentFiles(recentFiles, files, oldPath, newPath) {
17564
+ return persistRecentFiles(remapRecentFiles(recentFiles, files, oldPath, newPath));
17565
+ }
17566
+ function readRecentFilesOrFallback(fallback) {
17567
+ const stored = readRecentFiles();
17568
+ return stored.length > 0 ? stored : fallback;
17569
+ }
17384
17570
  const VIEW_INSPECT_CHANNELS = /* @__PURE__ */ new Set([
17385
17571
  "none",
17386
17572
  "mask",
@@ -17396,6 +17582,9 @@ const VIEW_INSPECT_CHANNELS = /* @__PURE__ */ new Set([
17396
17582
  "roughness"
17397
17583
  ]);
17398
17584
  const INSPECT_DISPLAY_MODES = /* @__PURE__ */ new Set(["heatmap", "points", "both", "scan"]);
17585
+ const SAVE_PICKER_TEXT_FILE_EXTS = EDITABLE_PROJECT_TEXT_FILE_EXTS.filter(
17586
+ (ext) => ![".forge.js", ".sketch.js", ".js", ".svg", ".dxf", ".md"].includes(ext)
17587
+ );
17399
17588
  function resolveViewInspectChannel(value) {
17400
17589
  return typeof value === "string" && VIEW_INSPECT_CHANNELS.has(value) ? value : "none";
17401
17590
  }
@@ -17411,8 +17600,17 @@ function resolveInitialViewInspectChannel(value) {
17411
17600
  function resolveInspectDisplayMode(value) {
17412
17601
  return typeof value === "string" && INSPECT_DISPLAY_MODES.has(value) ? value : "heatmap";
17413
17602
  }
17603
+ function isScalarInspectChannel(channel) {
17604
+ return channel === "thickness" || channel === "roughness";
17605
+ }
17606
+ function shouldUseScanRenderStyle(channel, displayMode) {
17607
+ return isScalarInspectChannel(channel) && displayMode === "scan";
17608
+ }
17414
17609
  const initialViewPreferences = readViewPreferences();
17415
- const initialRenderStyle = resolveForgeRenderStyle(initialViewPreferences.renderStyle);
17610
+ const initialInspectChannel = resolveInitialViewInspectChannel(initialViewPreferences.inspectChannel);
17611
+ const initialInspectDisplayMode = resolveInspectDisplayMode(initialViewPreferences.inspectDisplayMode);
17612
+ const initialStoredRenderStyle = resolveForgeRenderStyle(initialViewPreferences.renderStyle);
17613
+ const initialRenderStyle = shouldUseScanRenderStyle(initialInspectChannel, initialInspectDisplayMode) ? "scan" : initialStoredRenderStyle;
17416
17614
  const resolveFiniteNumber = (value, fallback) => {
17417
17615
  const numeric = typeof value === "number" ? value : Number(value);
17418
17616
  return Number.isFinite(numeric) ? numeric : fallback;
@@ -17497,6 +17695,10 @@ function abortServerCompute() {
17497
17695
  function canUseServerCompute() {
17498
17696
  return true;
17499
17697
  }
17698
+ function canHaveProjectImports(filename) {
17699
+ const lower = filename.toLowerCase();
17700
+ return lower.endsWith(".forge.js") || lower.endsWith(".sketch.js") || lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs") || lower.endsWith(".jsx") || lower.endsWith(".ts") || lower.endsWith(".tsx");
17701
+ }
17500
17702
  const PARAM_DEBOUNCE_MS = navigator.maxTouchPoints > 0 && window.innerWidth < 768 ? 250 : 80;
17501
17703
  async function lazyLoadFileAndDeps(filename, get, set) {
17502
17704
  if (!fileSystem.fetchFileContent) return;
@@ -17519,6 +17721,7 @@ async function lazyLoadFileAndDeps(filename, get, set) {
17519
17721
  }
17520
17722
  }
17521
17723
  if (!content) continue;
17724
+ if (!canHaveProjectImports(file)) continue;
17522
17725
  for (const ref of extractImports(content)) {
17523
17726
  const resolved = resolveImportPath(file, ref.raw);
17524
17727
  const matchKey = Object.keys(storeFiles).find((k2) => k2 === resolved || k2.replace(/^\.\//, "") === resolved.replace(/^\.\//, ""));
@@ -17572,6 +17775,7 @@ const useForgeStore = create((set, get) => ({
17572
17775
  savedFiles: { ...INITIAL_SAVED },
17573
17776
  folders: [...INITIAL_FOLDERS],
17574
17777
  activeFile: initialActive,
17778
+ recentFiles: touchRecentFile(readRecentFiles(), INITIAL_FILES, initialActive),
17575
17779
  setActiveFile: (name) => {
17576
17780
  abortServerCompute();
17577
17781
  if (name) {
@@ -17595,6 +17799,7 @@ const useForgeStore = create((set, get) => ({
17595
17799
  const restored = targetPreviewFile && nextByFile[targetPreviewFile] ? nextByFile[targetPreviewFile] : {};
17596
17800
  set({
17597
17801
  activeFile: name,
17802
+ recentFiles: touchAndPersistRecentFile(get().recentFiles, files, name),
17598
17803
  meshPreviewFile: null,
17599
17804
  // Clear mesh preview when switching files
17600
17805
  lastValidResult: null,
@@ -17643,15 +17848,13 @@ const useForgeStore = create((set, get) => ({
17643
17848
  if (!normalized) return;
17644
17849
  if (get().files[normalized] !== void 0) return;
17645
17850
  if (get().folders.includes(normalized)) return;
17646
- const template = normalized.endsWith(".svg") ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
17647
- <path d="M 10 10 L 90 10 L 90 90 L 10 90 Z" fill="none" stroke="#000" stroke-width="4" />
17648
- </svg>
17649
- ` : normalized.endsWith(".dxf") ? "0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n8\n0\n90\n4\n70\n1\n10\n0\n20\n0\n10\n100\n20\n0\n10\n100\n20\n60\n10\n0\n20\n60\n0\nENDSEC\n0\nEOF\n" : normalized.endsWith(".forge.js") ? "// 3D Part\n\nreturn box(50, 30, 10);\n" : "// Shared JS utilities for ForgeCAD.\n\nexport function exampleValue() {\n return 42;\n}\n";
17851
+ const template = projectTextFileTemplate(normalized);
17650
17852
  const newFolders = Array.from(/* @__PURE__ */ new Set([...get().folders, ...collectParentPaths(normalized)])).sort();
17651
17853
  set((s) => ({
17652
17854
  files: { ...s.files, [normalized]: template },
17653
17855
  savedFiles: { ...s.savedFiles, [normalized]: template },
17654
17856
  activeFile: normalized,
17857
+ recentFiles: touchAndPersistRecentFile(s.recentFiles, { ...s.files, [normalized]: template }, normalized),
17655
17858
  paramOverrides: {},
17656
17859
  jointValues: {},
17657
17860
  jointAnimationClip: null,
@@ -17689,6 +17892,7 @@ const useForgeStore = create((set, get) => ({
17689
17892
  files: remaining,
17690
17893
  savedFiles: remainingSaved,
17691
17894
  activeFile: newActive,
17895
+ recentFiles: touchAndPersistRecentFile(get().recentFiles, remaining, newActive),
17692
17896
  objectSettingsByFile: nextObjectSettingsByFile,
17693
17897
  paramSnapshotsByFile: nextParamSnapshotsByFile,
17694
17898
  paramOverrides: {},
@@ -17722,6 +17926,7 @@ const useForgeStore = create((set, get) => ({
17722
17926
  files: remaining,
17723
17927
  savedFiles: remainingSaved,
17724
17928
  activeFile: oldName === activeFile ? normalized : activeFile,
17929
+ recentFiles: remapAndPersistRecentFiles(get().recentFiles, remaining, oldName, normalized),
17725
17930
  objectSettingsByFile: nextObjectSettingsByFile,
17726
17931
  paramSnapshotsByFile: nextParamSnapshotsByFile,
17727
17932
  folders: Array.from(/* @__PURE__ */ new Set([...get().folders, ...collectParentPaths(normalized)])).sort()
@@ -17778,6 +17983,7 @@ const useForgeStore = create((set, get) => ({
17778
17983
  savedFiles: updatedSaved,
17779
17984
  folders: updatedFolders,
17780
17985
  activeFile: nextActive,
17986
+ recentFiles: remapAndPersistRecentFiles(get().recentFiles, updatedFiles, normalizedOld, normalizedNew),
17781
17987
  objectSettingsByFile: nextObjectSettingsByFile,
17782
17988
  paramSnapshotsByFile: nextParamSnapshotsByFile,
17783
17989
  paramOverrides: {},
@@ -17800,22 +18006,29 @@ const useForgeStore = create((set, get) => ({
17800
18006
  const prefix = `${normalized}/`;
17801
18007
  const remainingFiles = {};
17802
18008
  const remainingSaved = {};
17803
- const deletedFiles = [];
18009
+ const deletedFiles = /* @__PURE__ */ new Set();
17804
18010
  for (const key of Object.keys(files)) {
17805
18011
  if (key.startsWith(prefix)) {
17806
- deletedFiles.push(key);
18012
+ deletedFiles.add(key);
17807
18013
  } else {
17808
18014
  remainingFiles[key] = files[key];
17809
18015
  }
17810
18016
  }
17811
18017
  for (const key of Object.keys(savedFiles)) {
17812
- if (!key.startsWith(prefix)) {
18018
+ if (key.startsWith(prefix)) {
18019
+ deletedFiles.add(key);
18020
+ } else {
17813
18021
  remainingSaved[key] = savedFiles[key];
17814
18022
  }
17815
18023
  }
17816
- if (Object.keys(remainingFiles).length === 0) return;
18024
+ const replacementFile = Object.keys(remainingFiles).length === 0 ? "main.forge.js" : null;
18025
+ if (replacementFile) {
18026
+ const template = projectTextFileTemplate(replacementFile);
18027
+ remainingFiles[replacementFile] = template;
18028
+ remainingSaved[replacementFile] = template;
18029
+ }
17817
18030
  const remainingFolders = folders.filter((f2) => f2 !== normalized && !f2.startsWith(prefix));
17818
- const newActive = (activeFile == null ? void 0 : activeFile.startsWith(prefix)) ? Object.keys(remainingFiles)[0] : activeFile;
18031
+ const newActive = (activeFile == null ? void 0 : activeFile.startsWith(prefix)) || !activeFile ? Object.keys(remainingFiles)[0] : activeFile;
17819
18032
  let nextObjectSettingsByFile = objectSettingsByFile;
17820
18033
  let nextParamSnapshotsByFile = paramSnapshotsByFile;
17821
18034
  for (const f2 of deletedFiles) {
@@ -17828,6 +18041,7 @@ const useForgeStore = create((set, get) => ({
17828
18041
  savedFiles: remainingSaved,
17829
18042
  folders: remainingFolders,
17830
18043
  activeFile: newActive,
18044
+ recentFiles: touchAndPersistRecentFile(get().recentFiles, remainingFiles, newActive),
17831
18045
  objectSettingsByFile: nextObjectSettingsByFile,
17832
18046
  paramSnapshotsByFile: nextParamSnapshotsByFile,
17833
18047
  paramOverrides: {},
@@ -17837,7 +18051,7 @@ const useForgeStore = create((set, get) => ({
17837
18051
  jointAnimationPlaying: false,
17838
18052
  hoveredJointName: null
17839
18053
  });
17840
- Promise.all(deletedFiles.map((f2) => fileSystem.delete(f2))).then(() => fileSystem.deleteFolder(normalized)).catch((e2) => console.error("Delete folder failed:", e2));
18054
+ Promise.all(Array.from(deletedFiles).map((f2) => fileSystem.delete(f2))).then(() => replacementFile ? fileSystem.save(replacementFile, remainingFiles[replacementFile]) : void 0).then(() => fileSystem.deleteFolder(normalized)).catch((e2) => console.error("Delete folder failed:", e2));
17841
18055
  setTimeout(() => get().execute(), 0);
17842
18056
  },
17843
18057
  moveEntry: (oldPath, newPath) => {
@@ -18003,7 +18217,8 @@ const useForgeStore = create((set, get) => ({
18003
18217
  assemblyState,
18004
18218
  ...useServer ? {} : { assemblyDisplayMode: "instanceTransforms" },
18005
18219
  isNotebook: false,
18006
- activeBackend: executionBackend
18220
+ activeBackend: executionBackend,
18221
+ disableShapeBuildCache: get().disableRunCache
18007
18222
  };
18008
18223
  let serialized;
18009
18224
  if (useServer) {
@@ -18254,6 +18469,19 @@ const useForgeStore = create((set, get) => ({
18254
18469
  });
18255
18470
  if (isAssemblyDrivenResult(state2.lastValidResult)) void get().updateAssemblyPose();
18256
18471
  },
18472
+ resetJointValues: () => {
18473
+ const state2 = get();
18474
+ const hasPoseState = Object.keys(state2.jointValues).length > 0 || state2.jointAnimationClip !== null || state2.jointAnimationProgress !== 0 || state2.jointAnimationPlaying;
18475
+ if (!hasPoseState) return;
18476
+ const assemblyDriven = isAssemblyDrivenResult(state2.lastValidResult);
18477
+ set({
18478
+ jointValues: {},
18479
+ jointAnimationClip: null,
18480
+ jointAnimationProgress: 0,
18481
+ jointAnimationPlaying: false
18482
+ });
18483
+ if (assemblyDriven) void get().updateAssemblyPose();
18484
+ },
18257
18485
  setJointAnimationClip: (name) => {
18258
18486
  const assemblyDriven = isAssemblyDrivenResult(get().lastValidResult);
18259
18487
  set((state2) => {
@@ -18347,17 +18575,31 @@ const useForgeStore = create((set, get) => ({
18347
18575
  writeViewPreferences({ manualScene: DEFAULT_MANUAL_SCENE_SETTINGS });
18348
18576
  set({ manualScene: DEFAULT_MANUAL_SCENE_SETTINGS });
18349
18577
  },
18350
- inspectChannel: resolveInitialViewInspectChannel(initialViewPreferences.inspectChannel),
18578
+ inspectChannel: initialInspectChannel,
18351
18579
  setInspectChannel: (channel) => {
18352
18580
  const next = resolveViewInspectChannel(channel);
18353
- writeViewPreferences({ inspectChannel: next === "collisions" ? "none" : next });
18354
- set({ inspectChannel: next });
18581
+ set((state2) => {
18582
+ const renderStyle = shouldUseScanRenderStyle(next, state2.inspectDisplayMode) ? "scan" : state2.renderStyle;
18583
+ const scanGranularity = resolveViewportScanGranularity(state2.scanGranularity, renderStyle);
18584
+ writeViewPreferences({
18585
+ inspectChannel: next === "collisions" ? "none" : next,
18586
+ ...renderStyle !== state2.renderStyle ? { renderStyle, scanGranularity } : {}
18587
+ });
18588
+ return { inspectChannel: next, renderStyle, scanGranularity };
18589
+ });
18355
18590
  },
18356
- inspectDisplayMode: resolveInspectDisplayMode(initialViewPreferences.inspectDisplayMode),
18591
+ inspectDisplayMode: initialInspectDisplayMode,
18357
18592
  setInspectDisplayMode: (mode) => {
18358
18593
  const next = resolveInspectDisplayMode(mode);
18359
- writeViewPreferences({ inspectDisplayMode: next });
18360
- set({ inspectDisplayMode: next });
18594
+ set((state2) => {
18595
+ const renderStyle = shouldUseScanRenderStyle(state2.inspectChannel, next) ? "scan" : state2.renderStyle;
18596
+ const scanGranularity = resolveViewportScanGranularity(state2.scanGranularity, renderStyle);
18597
+ writeViewPreferences({
18598
+ inspectDisplayMode: next,
18599
+ ...renderStyle !== state2.renderStyle ? { renderStyle, scanGranularity } : {}
18600
+ });
18601
+ return { inspectDisplayMode: next, renderStyle, scanGranularity };
18602
+ });
18361
18603
  },
18362
18604
  scanGranularity: resolveViewportScanGranularity(
18363
18605
  initialViewPreferences.scanGranularity ?? SCAN_PROXY_GRANULARITY_DEFAULT,
@@ -18376,6 +18618,14 @@ const useForgeStore = create((set, get) => ({
18376
18618
  writeViewPreferences({ inspectPointSampleCount: next });
18377
18619
  set({ inspectPointSampleCount: next });
18378
18620
  },
18621
+ thicknessColorRange: resolveThicknessColorRange(initialViewPreferences.thicknessColorRange),
18622
+ setThicknessColorRange: (range) => {
18623
+ set((state2) => {
18624
+ const next = resolveThicknessColorRange({ ...state2.thicknessColorRange, ...range });
18625
+ writeViewPreferences({ thicknessColorRange: next });
18626
+ return { thicknessColorRange: next };
18627
+ });
18628
+ },
18379
18629
  comparisonInspectMode: resolveComparisonInspectMode(initialViewPreferences.comparisonInspectMode),
18380
18630
  setComparisonInspectMode: (mode) => {
18381
18631
  const next = resolveComparisonInspectMode(mode);
@@ -18763,7 +19013,7 @@ const useForgeStore = create((set, get) => ({
18763
19013
  if (!next) {
18764
19014
  return { measureMode: false, measureSelections: [], measurements: [] };
18765
19015
  }
18766
- return { measureMode: true };
19016
+ return { measureMode: true, voxelIntentMode: false };
18767
19017
  });
18768
19018
  },
18769
19019
  measureSelections: [],
@@ -18797,6 +19047,73 @@ const useForgeStore = create((set, get) => ({
18797
19047
  writeViewPreferences({ measureSnapPx: value });
18798
19048
  set({ measureSnapPx: value });
18799
19049
  },
19050
+ voxelIntentMode: false,
19051
+ toggleVoxelIntentMode: () => set((s) => {
19052
+ const next = !s.voxelIntentMode;
19053
+ if (!next) return { voxelIntentMode: false };
19054
+ return {
19055
+ voxelIntentMode: true,
19056
+ measureMode: false,
19057
+ measureSelections: [],
19058
+ measurements: [],
19059
+ constructionGhost: null
19060
+ };
19061
+ }),
19062
+ setVoxelIntentMode: (enabled) => set(
19063
+ () => enabled ? {
19064
+ voxelIntentMode: true,
19065
+ measureMode: false,
19066
+ measureSelections: [],
19067
+ measurements: [],
19068
+ constructionGhost: null
19069
+ } : { voxelIntentMode: false }
19070
+ ),
19071
+ voxelIntentTool: "red",
19072
+ setVoxelIntentTool: (tool) => set({ voxelIntentTool: tool }),
19073
+ voxelIntentPlacement: "surface",
19074
+ setVoxelIntentPlacement: (placement) => set({ voxelIntentPlacement: placement }),
19075
+ voxelIntentCellSize: 10,
19076
+ setVoxelIntentCellSize: (cellSize) => set((s) => {
19077
+ if (Object.keys(s.voxelIntentBlocks).length > 0) return {};
19078
+ const nextCellSize = Math.max(1, Math.min(200, Number.isFinite(cellSize) ? Math.round(cellSize) : 10));
19079
+ return { voxelIntentCellSize: nextCellSize };
19080
+ }),
19081
+ voxelIntentCellSizePreviewVisible: false,
19082
+ setVoxelIntentCellSizePreviewVisible: (visible) => set({ voxelIntentCellSizePreviewVisible: visible }),
19083
+ voxelIntentGhostOpacity: 0.24,
19084
+ setVoxelIntentGhostOpacity: (opacity) => set({ voxelIntentGhostOpacity: Math.max(0.05, Math.min(0.75, opacity)) }),
19085
+ voxelIntentContextFile: null,
19086
+ setVoxelIntentContextFile: (filePath) => set((s) => {
19087
+ if (s.voxelIntentContextFile === filePath) return {};
19088
+ return {
19089
+ voxelIntentContextFile: filePath,
19090
+ voxelIntentBlocks: filePath ? s.voxelIntentBlocksByFile[filePath] ?? {} : {}
19091
+ };
19092
+ }),
19093
+ voxelIntentBlocksByFile: {},
19094
+ voxelIntentBlocks: {},
19095
+ setVoxelIntentBlock: (cell, block) => set((s) => {
19096
+ const key = `${cell.x},${cell.y},${cell.z}`;
19097
+ const voxelIntentBlocks = { ...s.voxelIntentBlocks, [key]: block };
19098
+ return {
19099
+ voxelIntentBlocks,
19100
+ voxelIntentBlocksByFile: s.voxelIntentContextFile ? { ...s.voxelIntentBlocksByFile, [s.voxelIntentContextFile]: voxelIntentBlocks } : s.voxelIntentBlocksByFile
19101
+ };
19102
+ }),
19103
+ eraseVoxelIntentBlock: (cell) => set((s) => {
19104
+ const key = `${cell.x},${cell.y},${cell.z}`;
19105
+ if (!(key in s.voxelIntentBlocks)) return {};
19106
+ const next = { ...s.voxelIntentBlocks };
19107
+ delete next[key];
19108
+ return {
19109
+ voxelIntentBlocks: next,
19110
+ voxelIntentBlocksByFile: s.voxelIntentContextFile ? { ...s.voxelIntentBlocksByFile, [s.voxelIntentContextFile]: next } : s.voxelIntentBlocksByFile
19111
+ };
19112
+ }),
19113
+ clearVoxelIntentBlocks: () => set((s) => ({
19114
+ voxelIntentBlocks: {},
19115
+ voxelIntentBlocksByFile: s.voxelIntentContextFile ? { ...s.voxelIntentBlocksByFile, [s.voxelIntentContextFile]: {} } : s.voxelIntentBlocksByFile
19116
+ })),
18800
19117
  dimensionsVisible: initialViewPreferences.dimensionsVisible ?? true,
18801
19118
  toggleDimensions: () => set((s) => {
18802
19119
  const nextDimensionsVisible = !s.dimensionsVisible;
@@ -18885,6 +19202,7 @@ const useForgeStore = create((set, get) => ({
18885
19202
  savedFiles: { ...INITIAL_SAVED },
18886
19203
  folders: [],
18887
19204
  activeFile: initialActive,
19205
+ recentFiles: touchAndPersistRecentFile([], INITIAL_FILES, initialActive),
18888
19206
  previewFile: initialPreviewFile,
18889
19207
  objectSettings: getObjectSettingsForPreviewFile(initialObjectSettingsByFile, initialPreviewFile),
18890
19208
  objectSettingsByFile: initialObjectSettingsByFile,
@@ -18925,7 +19243,9 @@ const useForgeStore = create((set, get) => ({
18925
19243
  const handle = await window.showSaveFilePicker({
18926
19244
  suggestedName: activeFile,
18927
19245
  types: [
18928
- { description: "ForgeCAD scripts", accept: { "text/javascript": [".forge.js", ".js"] } },
19246
+ { description: "ForgeCAD scripts", accept: { "text/javascript": [".forge.js", ".sketch.js", ".js"] } },
19247
+ { description: "Text files", accept: { "text/plain": SAVE_PICKER_TEXT_FILE_EXTS } },
19248
+ { description: "Markdown", accept: { "text/markdown": [".md"] } },
18929
19249
  { description: "SVG", accept: { "image/svg+xml": [".svg"] } },
18930
19250
  { description: "DXF", accept: { "application/dxf": [".dxf"] } }
18931
19251
  ]
@@ -18940,7 +19260,7 @@ const useForgeStore = create((set, get) => ({
18940
19260
  // This assumes we are satisfied with current content being "saved"
18941
19261
  }));
18942
19262
  } else {
18943
- const mime = activeFile.endsWith(".svg") ? "image/svg+xml" : activeFile.endsWith(".dxf") ? "application/dxf" : "text/javascript";
19263
+ const mime = projectTextFileMimeType(activeFile);
18944
19264
  const blob = new Blob([code], { type: mime });
18945
19265
  triggerDownload(blob, activeFile);
18946
19266
  set((s) => ({
@@ -18959,6 +19279,7 @@ const useForgeStore = create((set, get) => ({
18959
19279
  files: { ...s.files, [normalized]: text },
18960
19280
  savedFiles: { ...s.savedFiles, [normalized]: text },
18961
19281
  activeFile: normalized,
19282
+ recentFiles: touchAndPersistRecentFile(s.recentFiles, { ...s.files, [normalized]: text }, normalized),
18962
19283
  fileHandle: null,
18963
19284
  dirty: false,
18964
19285
  previewFile: null,
@@ -19017,6 +19338,7 @@ const useForgeStore = create((set, get) => ({
19017
19338
  return {
19018
19339
  files: nextFiles,
19019
19340
  activeFile: nextActive,
19341
+ recentFiles: touchAndPersistRecentFile(s.recentFiles, nextFiles, nextActive),
19020
19342
  fileHandle: null,
19021
19343
  dirty: true,
19022
19344
  previewFile: null,
@@ -19095,9 +19417,15 @@ const useForgeStore = create((set, get) => ({
19095
19417
  const prevFiles = state2.files;
19096
19418
  const prevActiveFile = state2.activeFile;
19097
19419
  const nextState = computeServerSnapshot(state2, serverFiles, serverFolders, sharedModel, sharedBundle, initialFile);
19098
- set({ ...nextState, filesLoading: false });
19099
19420
  const nextFiles = nextState.files;
19100
- const newActiveFile = nextState.activeFile;
19421
+ const newActiveFile = nextState.activeFile ?? state2.activeFile;
19422
+ const baseRecentFiles = readRecentFilesOrFallback(state2.recentFiles);
19423
+ set({
19424
+ ...nextState,
19425
+ activeFile: newActiveFile,
19426
+ recentFiles: touchAndPersistRecentFile(baseRecentFiles, nextFiles, newActiveFile),
19427
+ filesLoading: false
19428
+ });
19101
19429
  const needsLazyLoad = newActiveFile && newActiveFile in nextFiles && nextFiles[newActiveFile] === "" && fileSystem.fetchFileContent;
19102
19430
  postApplyServerSnapshot(
19103
19431
  prevActiveFile,
@@ -19118,7 +19446,7 @@ const useForgeStore = create((set, get) => ({
19118
19446
  const state2 = get();
19119
19447
  const nextState = computeServerFileChange(state2, filename, content);
19120
19448
  if (!nextState) return;
19121
- set(nextState);
19449
+ set({ ...nextState, recentFiles: pruneAndPersistRecentFiles(state2.recentFiles, nextState.files) });
19122
19450
  const previewFile = resolvePreviewFile(state2.activeFile, nextState.files);
19123
19451
  if (previewFile === filename) setTimeout(() => get().execute(), 0);
19124
19452
  },
@@ -19127,13 +19455,18 @@ const useForgeStore = create((set, get) => ({
19127
19455
  const prevActiveFile = state2.activeFile;
19128
19456
  const nextState = computeServerFileDelete(state2, filename);
19129
19457
  if (!nextState) return;
19130
- set(nextState);
19131
- if (nextState.activeFile && nextState.activeFile !== prevActiveFile) {
19458
+ const newActiveFile = nextState.activeFile ?? "";
19459
+ set({
19460
+ ...nextState,
19461
+ activeFile: newActiveFile,
19462
+ recentFiles: touchAndPersistRecentFile(state2.recentFiles, nextState.files, newActiveFile)
19463
+ });
19464
+ if (newActiveFile && newActiveFile !== prevActiveFile) {
19132
19465
  set({ paramOverrides: {}, lastValidResult: null });
19133
19466
  setParamOverrides({});
19134
- window.history.replaceState(null, "", `#${nextState.activeFile}`);
19467
+ window.history.replaceState(null, "", `#${newActiveFile}`);
19135
19468
  try {
19136
- localStorage.setItem(lastActiveFileKey(), nextState.activeFile);
19469
+ localStorage.setItem(lastActiveFileKey(), newActiveFile);
19137
19470
  } catch {
19138
19471
  }
19139
19472
  setTimeout(() => get().execute(), 0);
@@ -19160,6 +19493,9 @@ const useForgeStore = create((set, get) => ({
19160
19493
  shareOpen: false,
19161
19494
  openShare: () => set({ shareOpen: true }),
19162
19495
  closeShare: () => set({ shareOpen: false }),
19496
+ captureOpen: false,
19497
+ openCapture: () => set({ captureOpen: true }),
19498
+ closeCapture: () => set({ captureOpen: false }),
19163
19499
  aiSkillOpen: false,
19164
19500
  openAISkill: () => set({ aiSkillOpen: true }),
19165
19501
  closeAISkill: () => set({ aiSkillOpen: false }),
@@ -19172,7 +19508,10 @@ const useForgeStore = create((set, get) => ({
19172
19508
  disableRunCache: initialViewPreferences.disableRunCache ?? false,
19173
19509
  setDisableRunCache: (disabled) => {
19174
19510
  writeViewPreferences({ disableRunCache: disabled });
19175
- if (disabled) clearRunResultCache();
19511
+ if (disabled) {
19512
+ clearRunResultCache();
19513
+ evalWorkerClient.clearCaches();
19514
+ }
19176
19515
  set({ disableRunCache: disabled });
19177
19516
  }
19178
19517
  }));
@@ -26998,12 +27337,15 @@ const COMPARISON_REFERENCE_COLOR = [32, 199, 255];
26998
27337
  const COMPARISON_CONTEXT_COLOR = [174, 184, 194];
26999
27338
  const SCAN_CONTEXT_COLOR = [19, 201, 255];
27000
27339
  const SCAN_CONTEXT_EDGE_COLOR = [145, 231, 255];
27001
- const SCAN_RISK_MID_COLOR = [255, 193, 77];
27002
- const SCAN_RISK_HOT_COLOR = [255, 138, 31];
27003
27340
  const FLOATING_HIGHLIGHT_COLOR = [255, 68, 16];
27004
27341
  const FLOATING_CONTEXT_COLOR = [38, 49, 58];
27005
27342
  const RIG_SHADOW_COLOR = [6, 10, 18];
27006
- const RIG_LINK_COLOR = [20, 220, 255];
27343
+ const RIG_LINK_COLOR = [142, 106, 255];
27344
+ const RIG_KINEMATIC_LINK_COLOR = [255, 226, 106];
27345
+ const RIG_KINEMATIC_EDGE_COLOR = [20, 220, 255];
27346
+ const RIG_FIXED_LINK_COLOR = [240, 253, 255];
27347
+ const RIG_DERIVED_LINK_COLOR = [255, 76, 196];
27348
+ const RIG_CONTROL_LINK_COLOR = [255, 122, 26];
27007
27349
  const RIG_PART_LINK_COLOR = [174, 255, 92];
27008
27350
  const RIG_HIDDEN_LINK_COLOR = [116, 136, 148];
27009
27351
  const COLLISION_PALETTE = [
@@ -27280,7 +27622,7 @@ function ConstructionGhostOverlay({ matrix }) {
27280
27622
  class ConstructionHistoryWorkerClient {
27281
27623
  constructor(workerFactory = () => new Worker(new URL(
27282
27624
  /* @vite-ignore */
27283
- "/assets/constructionHistoryWorker-gpDo-uH2.js",
27625
+ "/assets/constructionHistoryWorker-AwMMWSxg.js",
27284
27626
  import.meta.url
27285
27627
  ), { type: "module" })) {
27286
27628
  __publicField(this, "worker", null);
@@ -29436,14 +29778,89 @@ ensureNotFinalized_fn = function() {
29436
29778
  throw new Error("Cannot add new video or audio chunks after the file has been finalized.");
29437
29779
  }
29438
29780
  };
29781
+ const H264_HIGH_LEVEL_4_CODEC = "avc1.640028";
29782
+ const H264_LEVEL_4_MAX_CODED_AREA = 2097152;
29783
+ const H264_MACROBLOCK_SIZE = 16;
29439
29784
  function supportsMP4Recording() {
29440
29785
  return typeof VideoEncoder !== "undefined" && typeof VideoFrame !== "undefined";
29441
29786
  }
29787
+ function evenFloor(value) {
29788
+ return Math.max(2, Math.floor(value) & -2);
29789
+ }
29790
+ function codedDimension(value) {
29791
+ return Math.ceil(value / H264_MACROBLOCK_SIZE) * H264_MACROBLOCK_SIZE;
29792
+ }
29793
+ function codedArea(width, height) {
29794
+ return codedDimension(width) * codedDimension(height);
29795
+ }
29796
+ function resolveCanvasBackground(canvas) {
29797
+ const transparent = /* @__PURE__ */ new Set(["transparent", "rgba(0, 0, 0, 0)"]);
29798
+ let node = canvas;
29799
+ while (node) {
29800
+ const backgroundColor = getComputedStyle(node).backgroundColor;
29801
+ if (backgroundColor && !transparent.has(backgroundColor)) return backgroundColor;
29802
+ node = node.parentElement;
29803
+ }
29804
+ return "#252526";
29805
+ }
29806
+ function fitH264Level4RecordingSize(sourceWidth, sourceHeight) {
29807
+ if (!Number.isFinite(sourceWidth) || !Number.isFinite(sourceHeight) || sourceWidth <= 0 || sourceHeight <= 0) {
29808
+ throw new Error(`Cannot record MP4 from invalid canvas size ${sourceWidth}x${sourceHeight}.`);
29809
+ }
29810
+ const evenSourceWidth = evenFloor(sourceWidth);
29811
+ const evenSourceHeight = evenFloor(sourceHeight);
29812
+ let width = evenSourceWidth;
29813
+ let height = evenSourceHeight;
29814
+ if (codedArea(width, height) > H264_LEVEL_4_MAX_CODED_AREA) {
29815
+ let low = 0;
29816
+ let high = 1;
29817
+ let bestWidth = 2;
29818
+ let bestHeight = 2;
29819
+ for (let i = 0; i < 32; i += 1) {
29820
+ const scale = (low + high) / 2;
29821
+ const candidateWidth = evenFloor(evenSourceWidth * scale);
29822
+ const candidateHeight = evenFloor(evenSourceHeight * scale);
29823
+ if (codedArea(candidateWidth, candidateHeight) <= H264_LEVEL_4_MAX_CODED_AREA) {
29824
+ bestWidth = candidateWidth;
29825
+ bestHeight = candidateHeight;
29826
+ low = scale;
29827
+ } else {
29828
+ high = scale;
29829
+ }
29830
+ }
29831
+ width = bestWidth;
29832
+ height = bestHeight;
29833
+ }
29834
+ const codedWidth = codedDimension(width);
29835
+ const codedHeight = codedDimension(height);
29836
+ if (codedWidth * codedHeight > H264_LEVEL_4_MAX_CODED_AREA) {
29837
+ throw new Error(`Could not fit ${sourceWidth}x${sourceHeight} into the MP4 encoder limit.`);
29838
+ }
29839
+ return {
29840
+ sourceWidth: evenSourceWidth,
29841
+ sourceHeight: evenSourceHeight,
29842
+ width,
29843
+ height,
29844
+ codedWidth,
29845
+ codedHeight,
29846
+ scale: Math.min(width / evenSourceWidth, height / evenSourceHeight)
29847
+ };
29848
+ }
29849
+ async function requireSupportedEncoderConfig(config) {
29850
+ if (typeof VideoEncoder.isConfigSupported !== "function") return config;
29851
+ const support = await VideoEncoder.isConfigSupported(config);
29852
+ if (!support.supported) {
29853
+ throw new Error(`MP4 recording is not supported at ${config.width}x${config.height} in this browser.`);
29854
+ }
29855
+ return support.config ?? config;
29856
+ }
29442
29857
  class HistoryRecorder {
29443
29858
  constructor(canvas, options) {
29444
29859
  __publicField(this, "encoder", null);
29445
29860
  __publicField(this, "muxer", null);
29446
29861
  __publicField(this, "target", null);
29862
+ __publicField(this, "captureCanvas", null);
29863
+ __publicField(this, "captureContext", null);
29447
29864
  __publicField(this, "frameCount", 0);
29448
29865
  __publicField(this, "recording", false);
29449
29866
  __publicField(this, "startTime", 0);
@@ -29460,37 +29877,71 @@ class HistoryRecorder {
29460
29877
  /** Start recording. Must be called before captureFrame(). */
29461
29878
  async start() {
29462
29879
  if (this.recording) return;
29463
- const width = this.canvas.width & -2;
29464
- const height = this.canvas.height & -2;
29465
- this.target = new ArrayBufferTarget();
29466
- this.muxer = new Muxer({
29467
- target: this.target,
29468
- video: {
29469
- codec: "avc",
29470
- width,
29471
- height
29472
- },
29473
- fastStart: "in-memory",
29474
- firstTimestampBehavior: "offset"
29475
- });
29476
- this.encoder = new VideoEncoder({
29477
- output: (chunk, meta) => {
29478
- this.muxer.addVideoChunk(chunk, meta);
29479
- },
29480
- error: (e2) => console.error("VideoEncoder error:", e2)
29481
- });
29482
- this.encoder.configure({
29483
- codec: "avc1.640028",
29880
+ if (!supportsMP4Recording()) {
29881
+ throw new Error("MP4 recording requires WebCodecs VideoEncoder and VideoFrame support.");
29882
+ }
29883
+ const size = fitH264Level4RecordingSize(this.canvas.width, this.canvas.height);
29884
+ const config = await requireSupportedEncoderConfig({
29885
+ codec: H264_HIGH_LEVEL_4_CODEC,
29484
29886
  // H.264 High Profile Level 4.0
29485
- width,
29486
- height,
29887
+ width: size.width,
29888
+ height: size.height,
29487
29889
  bitrate: this.bitrate,
29488
29890
  framerate: this.fps
29489
29891
  });
29892
+ try {
29893
+ this.prepareCaptureSurface(size);
29894
+ this.target = new ArrayBufferTarget();
29895
+ this.muxer = new Muxer({
29896
+ target: this.target,
29897
+ video: {
29898
+ codec: "avc",
29899
+ width: size.width,
29900
+ height: size.height
29901
+ },
29902
+ fastStart: "in-memory",
29903
+ firstTimestampBehavior: "offset"
29904
+ });
29905
+ this.encoder = new VideoEncoder({
29906
+ output: (chunk, meta) => {
29907
+ this.muxer.addVideoChunk(chunk, meta);
29908
+ },
29909
+ error: (e2) => {
29910
+ this.recording = false;
29911
+ console.error("VideoEncoder error:", e2);
29912
+ }
29913
+ });
29914
+ this.encoder.configure(config);
29915
+ } catch (err) {
29916
+ this.abort();
29917
+ throw err;
29918
+ }
29490
29919
  this.frameCount = 0;
29491
29920
  this.startTime = performance.now();
29492
29921
  this.recording = true;
29493
29922
  }
29923
+ prepareCaptureSurface(size) {
29924
+ this.captureCanvas = null;
29925
+ this.captureContext = null;
29926
+ const captureCanvas = document.createElement("canvas");
29927
+ captureCanvas.width = size.width;
29928
+ captureCanvas.height = size.height;
29929
+ const captureContext = captureCanvas.getContext("2d", { alpha: false });
29930
+ if (!captureContext) {
29931
+ throw new Error("Could not create MP4 recording capture canvas.");
29932
+ }
29933
+ this.captureCanvas = captureCanvas;
29934
+ this.captureContext = captureContext;
29935
+ }
29936
+ frameSourceCanvas() {
29937
+ if (!this.captureCanvas || !this.captureContext) {
29938
+ throw new Error("MP4 recording capture surface is not initialized.");
29939
+ }
29940
+ this.captureContext.fillStyle = resolveCanvasBackground(this.canvas);
29941
+ this.captureContext.fillRect(0, 0, this.captureCanvas.width, this.captureCanvas.height);
29942
+ this.captureContext.drawImage(this.canvas, 0, 0, this.captureCanvas.width, this.captureCanvas.height);
29943
+ return this.captureCanvas;
29944
+ }
29494
29945
  /**
29495
29946
  * Capture the current canvas frame. Forces a render so the drawing buffer
29496
29947
  * has fresh content — this avoids needing `preserveDrawingBuffer: true` on
@@ -29521,7 +29972,11 @@ class HistoryRecorder {
29521
29972
  this.encodeFrame(timestampMicros);
29522
29973
  }
29523
29974
  encodeFrame(timestampMicros) {
29524
- const frame2 = new VideoFrame(this.canvas, {
29975
+ if (!this.encoder || this.encoder.state !== "configured") {
29976
+ this.recording = false;
29977
+ return;
29978
+ }
29979
+ const frame2 = new VideoFrame(this.frameSourceCanvas(), {
29525
29980
  timestamp: timestampMicros
29526
29981
  });
29527
29982
  const keyFrame = this.frameCount % (this.fps * 2) === 0;
@@ -29529,20 +29984,32 @@ class HistoryRecorder {
29529
29984
  frame2.close();
29530
29985
  this.frameCount++;
29531
29986
  }
29987
+ reset() {
29988
+ this.encoder = null;
29989
+ this.muxer = null;
29990
+ this.target = null;
29991
+ this.captureCanvas = null;
29992
+ this.captureContext = null;
29993
+ this.frameCount = 0;
29994
+ this.startTime = 0;
29995
+ }
29532
29996
  /** Stop recording and return the MP4 blob. */
29533
29997
  async stop() {
29534
29998
  if (!this.encoder || !this.muxer || !this.target) {
29535
29999
  throw new Error("Not recording");
29536
30000
  }
30001
+ const encoder2 = this.encoder;
30002
+ const muxer = this.muxer;
30003
+ const target = this.target;
29537
30004
  this.recording = false;
29538
- await this.encoder.flush();
29539
- this.encoder.close();
29540
- this.muxer.finalize();
29541
- const buffer = this.target.buffer;
29542
- this.encoder = null;
29543
- this.muxer = null;
29544
- this.target = null;
29545
- return new Blob([buffer], { type: "video/mp4" });
30005
+ try {
30006
+ await encoder2.flush();
30007
+ if (encoder2.state !== "closed") encoder2.close();
30008
+ muxer.finalize();
30009
+ return new Blob([target.buffer], { type: "video/mp4" });
30010
+ } finally {
30011
+ this.reset();
30012
+ }
29546
30013
  }
29547
30014
  /** Abort recording without producing output. */
29548
30015
  abort() {
@@ -29554,10 +30021,7 @@ class HistoryRecorder {
29554
30021
  } catch {
29555
30022
  }
29556
30023
  }
29557
- this.encoder = null;
29558
- this.muxer = null;
29559
- this.target = null;
29560
- this.frameCount = 0;
30024
+ this.reset();
29561
30025
  }
29562
30026
  }
29563
30027
  let registeredStart$2 = null;
@@ -29613,6 +30077,12 @@ function HistoryRecorderBridge() {
29613
30077
  requestAnimationFrame(() => {
29614
30078
  useForgeStore.getState().setHistoryPlaying(true);
29615
30079
  });
30080
+ }).catch((err) => {
30081
+ recorderRef.current = null;
30082
+ state2.setHistoryRecording(false);
30083
+ state2.setHistoryPlaying(false);
30084
+ showToast("Recording failed", "error");
30085
+ console.error("History recording failed:", err);
29616
30086
  });
29617
30087
  }, [gl]);
29618
30088
  const stop = reactExports.useCallback(async () => {
@@ -29664,7 +30134,7 @@ function generateReportInWorker(options) {
29664
30134
  return new Promise((resolve2, reject) => {
29665
30135
  const worker = new Worker(new URL(
29666
30136
  /* @vite-ignore */
29667
- "/assets/reportWorker-CdBz5bNg.js",
30137
+ "/assets/reportWorker-DO6hcQbh.js",
29668
30138
  import.meta.url
29669
30139
  ), { type: "module" });
29670
30140
  const cleanup = () => {
@@ -30347,9 +30817,13 @@ function buildSketchExportArtifacts(format, stem, runResult) {
30347
30817
  });
30348
30818
  }
30349
30819
  let orbitVideoExporter = null;
30820
+ let viewportImageExporter = null;
30350
30821
  function registerOrbitVideoExporter(exporter) {
30351
30822
  orbitVideoExporter = exporter;
30352
30823
  }
30824
+ function registerViewportImageExporter(exporter) {
30825
+ viewportImageExporter = exporter;
30826
+ }
30353
30827
  function rerunActiveScriptForQuality(quality) {
30354
30828
  const { files, activeFile, paramOverrides } = useForgeStore.getState();
30355
30829
  const assemblyState = resolveAssemblyStateForExport();
@@ -30475,6 +30949,23 @@ async function exportOrbitVideoFromStore(preferredStem, options) {
30475
30949
  const ext = blob.type === "video/mp4" ? "mp4" : "gif";
30476
30950
  triggerDownload(blob, `${stem}.orbit.${ext}`);
30477
30951
  }
30952
+ async function captureViewportImageBlobFromStore(options) {
30953
+ if (!viewportImageExporter) {
30954
+ throw new Error("Viewport is not ready for image capture. Try again in a moment.");
30955
+ }
30956
+ const { activeFile, theme } = useForgeStore.getState();
30957
+ return viewportImageExporter({
30958
+ title: deriveExportStem(activeFile),
30959
+ themeName: theme,
30960
+ ...options ?? {}
30961
+ });
30962
+ }
30963
+ async function exportViewportImageFromStore(preferredStem, options) {
30964
+ const { activeFile } = useForgeStore.getState();
30965
+ const stem = sanitizeExportStem(deriveExportStem(activeFile));
30966
+ const blob = await captureViewportImageBlobFromStore(options);
30967
+ triggerDownload(blob, `${stem}.png`);
30968
+ }
30478
30969
  function exportSketchFromStore(format, preferredStem) {
30479
30970
  const { result, activeFile } = useForgeStore.getState();
30480
30971
  const stem = sanitizeExportStem(preferredStem ?? deriveExportStem(activeFile));
@@ -30735,6 +31226,11 @@ function ViewportRecordingBridge({ controlsRef }) {
30735
31226
  frameIntervalRef.current = 1e3 / TARGET_FPS;
30736
31227
  void recorder.start().then(() => {
30737
31228
  useForgeStore.getState().setAnimationRecording(true);
31229
+ }).catch((err) => {
31230
+ recorderRef.current = null;
31231
+ useForgeStore.getState().setAnimationRecording(false);
31232
+ showToast("Recording failed", "error");
31233
+ console.error("Viewport recording failed:", err);
30738
31234
  });
30739
31235
  }, [gl, snapshotOrbitTarget]);
30740
31236
  const stop = reactExports.useCallback(async () => {
@@ -31570,6 +32066,14 @@ function TrajectoryRecorderBridge({ controlsRef }) {
31570
32066
  state2.setTrajectoryProgress(0);
31571
32067
  void recorder.start().then(() => {
31572
32068
  state2.setTrajectoryRecording(true);
32069
+ }).catch((err) => {
32070
+ recorderRef.current = null;
32071
+ trajectoryRef.current = null;
32072
+ lastFrameRef.current = 0;
32073
+ manualStartRef.current = 0;
32074
+ state2.setTrajectoryRecording(false);
32075
+ showToast("Recording failed", "error");
32076
+ console.error("Trajectory recording failed:", err);
31573
32077
  });
31574
32078
  }, [gl]);
31575
32079
  const stop = reactExports.useCallback(async () => {
@@ -32526,6 +33030,164 @@ function OrbitExporterBridge({ controlsRef }) {
32526
33030
  }, [exportOrbitVideo]);
32527
33031
  return null;
32528
33032
  }
33033
+ function clampDimension(value, fallback) {
33034
+ const numeric = Number.isFinite(value) ? value : fallback;
33035
+ return Math.max(96, Math.min(4096, Math.round(numeric ?? 96)));
33036
+ }
33037
+ function computeFrameLayout(width, height, includeFrame) {
33038
+ if (!includeFrame) return { x: 0, y: 0, width, height, headerHeight: 0, pad: 0 };
33039
+ const pad = Math.max(14, Math.round(Math.min(width, height) * 0.035));
33040
+ const headerHeight = Math.max(50, Math.min(96, Math.round(height * 0.105)));
33041
+ return {
33042
+ x: pad,
33043
+ y: headerHeight,
33044
+ width: Math.max(1, width - pad * 2),
33045
+ height: Math.max(1, height - headerHeight - pad),
33046
+ headerHeight,
33047
+ pad
33048
+ };
33049
+ }
33050
+ function cssVar(name, fallback) {
33051
+ if (typeof document === "undefined") return fallback;
33052
+ const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
33053
+ return value || fallback;
33054
+ }
33055
+ function canvasBlob(canvas) {
33056
+ return new Promise((resolve2, reject) => {
33057
+ canvas.toBlob((blob) => blob ? resolve2(blob) : reject(new Error("Could not encode PNG capture.")), "image/png");
33058
+ });
33059
+ }
33060
+ function drawCorner(ctx, x, y, dx, dy, size) {
33061
+ ctx.beginPath();
33062
+ ctx.moveTo(x, y + dy * size);
33063
+ ctx.lineTo(x, y);
33064
+ ctx.lineTo(x + dx * size, y);
33065
+ ctx.stroke();
33066
+ }
33067
+ function composeFrame(source, outputWidth, outputHeight, layout, options) {
33068
+ var _a3;
33069
+ const canvas = document.createElement("canvas");
33070
+ canvas.width = outputWidth;
33071
+ canvas.height = outputHeight;
33072
+ const ctx = canvas.getContext("2d");
33073
+ if (!ctx) throw new Error("Could not create PNG capture context.");
33074
+ const bg = cssVar("--fc-bg", "#02070a");
33075
+ const surface = cssVar("--fc-bgSurface", "#0a2028");
33076
+ const border = cssVar("--fc-border", "#165363");
33077
+ const accent = cssVar("--fc-accent", "#36e8ff");
33078
+ const text = cssVar("--fc-text", "#d9fbff");
33079
+ const muted = cssVar("--fc-textMuted", "#8ebdc6");
33080
+ const dim = cssVar("--fc-textDim", "#4e7078");
33081
+ const title = ((_a3 = options.title) == null ? void 0 : _a3.trim()) || "ForgeCAD model";
33082
+ const themeLabel = options.themeName ? `${options.themeName} theme` : "current theme";
33083
+ ctx.fillStyle = bg;
33084
+ ctx.fillRect(0, 0, outputWidth, outputHeight);
33085
+ ctx.fillStyle = surface;
33086
+ ctx.fillRect(0, 0, outputWidth, layout.headerHeight);
33087
+ ctx.strokeStyle = border;
33088
+ ctx.lineWidth = Math.max(1, Math.round(outputWidth / 900));
33089
+ ctx.strokeRect(layout.x - 1, layout.y - 1, layout.width + 2, layout.height + 2);
33090
+ ctx.drawImage(source, layout.x, layout.y, layout.width, layout.height);
33091
+ ctx.strokeStyle = accent;
33092
+ ctx.globalAlpha = 0.85;
33093
+ ctx.beginPath();
33094
+ ctx.moveTo(layout.pad, layout.headerHeight - 1);
33095
+ ctx.lineTo(outputWidth - layout.pad, layout.headerHeight - 1);
33096
+ ctx.stroke();
33097
+ const corner = Math.max(18, Math.min(42, Math.round(Math.min(outputWidth, outputHeight) * 0.035)));
33098
+ drawCorner(ctx, layout.pad, layout.y, 1, 1, corner);
33099
+ drawCorner(ctx, outputWidth - layout.pad, layout.y, -1, 1, corner);
33100
+ drawCorner(ctx, layout.pad, outputHeight - layout.pad, 1, -1, corner);
33101
+ drawCorner(ctx, outputWidth - layout.pad, outputHeight - layout.pad, -1, -1, corner);
33102
+ ctx.globalAlpha = 1;
33103
+ const mono = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
33104
+ ctx.fillStyle = accent;
33105
+ ctx.font = `700 ${Math.max(11, Math.round(outputHeight * 0.012))}px ${mono}`;
33106
+ ctx.fillText("FORGECAD CAPTURE", layout.pad, Math.round(layout.headerHeight * 0.35));
33107
+ ctx.fillStyle = text;
33108
+ ctx.font = `700 ${Math.max(16, Math.round(outputHeight * 0.021))}px Inter, system-ui, sans-serif`;
33109
+ ctx.fillText(title.slice(0, 64), layout.pad, Math.round(layout.headerHeight * 0.72));
33110
+ ctx.fillStyle = muted;
33111
+ ctx.font = `500 ${Math.max(11, Math.round(outputHeight * 0.012))}px ${mono}`;
33112
+ ctx.textAlign = "right";
33113
+ ctx.fillText(`${outputWidth}x${outputHeight}`, outputWidth - layout.pad, Math.round(layout.headerHeight * 0.35));
33114
+ ctx.fillStyle = dim;
33115
+ ctx.fillText(themeLabel, outputWidth - layout.pad, Math.round(layout.headerHeight * 0.72));
33116
+ ctx.textAlign = "left";
33117
+ return canvas;
33118
+ }
33119
+ function copyRenderedCanvas(source, width, height) {
33120
+ const canvas = document.createElement("canvas");
33121
+ canvas.width = width;
33122
+ canvas.height = height;
33123
+ const ctx = canvas.getContext("2d");
33124
+ if (!ctx) throw new Error("Could not create PNG capture context.");
33125
+ ctx.drawImage(source, 0, 0, width, height);
33126
+ return canvas;
33127
+ }
33128
+ function withCameraAspect(camera, width, height, render) {
33129
+ if (camera instanceof PerspectiveCamera$1) {
33130
+ const previousAspect = camera.aspect;
33131
+ camera.aspect = width / height;
33132
+ camera.updateProjectionMatrix();
33133
+ try {
33134
+ render();
33135
+ } finally {
33136
+ camera.aspect = previousAspect;
33137
+ camera.updateProjectionMatrix();
33138
+ }
33139
+ return;
33140
+ }
33141
+ if (camera instanceof OrthographicCamera$1) {
33142
+ const previous = { left: camera.left, right: camera.right, top: camera.top, bottom: camera.bottom };
33143
+ const midX = (camera.left + camera.right) / 2;
33144
+ const halfHeight = (camera.top - camera.bottom) / 2;
33145
+ const halfWidth = halfHeight * (width / height);
33146
+ camera.left = midX - halfWidth;
33147
+ camera.right = midX + halfWidth;
33148
+ camera.updateProjectionMatrix();
33149
+ try {
33150
+ render();
33151
+ } finally {
33152
+ Object.assign(camera, previous);
33153
+ camera.updateProjectionMatrix();
33154
+ }
33155
+ return;
33156
+ }
33157
+ render();
33158
+ }
33159
+ function ViewportImageCaptureBridge() {
33160
+ const { camera, gl, scene } = useThree();
33161
+ const capture = reactExports.useCallback(
33162
+ async (options) => {
33163
+ const currentSize = gl.getSize(new Vector2());
33164
+ const outputWidth = clampDimension(options == null ? void 0 : options.width, currentSize.x);
33165
+ const outputHeight = clampDimension(options == null ? void 0 : options.height, currentSize.y);
33166
+ const layout = computeFrameLayout(outputWidth, outputHeight, (options == null ? void 0 : options.includeFrame) ?? true);
33167
+ const previousSize = currentSize.clone();
33168
+ const previousPixelRatio = gl.getPixelRatio();
33169
+ try {
33170
+ await waitForAnimationFrame();
33171
+ gl.setPixelRatio(1);
33172
+ gl.setSize(layout.width, layout.height, false);
33173
+ withCameraAspect(camera, layout.width, layout.height, () => gl.render(scene, camera));
33174
+ const rendered = copyRenderedCanvas(gl.domElement, layout.width, layout.height);
33175
+ const finalCanvas = (options == null ? void 0 : options.includeFrame) === false ? rendered : composeFrame(rendered, outputWidth, outputHeight, layout, options ?? {});
33176
+ return await canvasBlob(finalCanvas);
33177
+ } finally {
33178
+ gl.setPixelRatio(previousPixelRatio);
33179
+ gl.setSize(previousSize.x, previousSize.y, false);
33180
+ gl.render(scene, camera);
33181
+ }
33182
+ },
33183
+ [camera, gl, scene]
33184
+ );
33185
+ reactExports.useEffect(() => {
33186
+ registerViewportImageExporter(capture);
33187
+ return () => registerViewportImageExporter(null);
33188
+ }, [capture]);
33189
+ return null;
33190
+ }
32529
33191
  const DEBUG_HL_DEFAULT_COLOR = "#ff00ff";
32530
33192
  const DEBUG_HL_DEFAULT_POINT_SIZE = 3;
32531
33193
  const DEBUG_HL_DEFAULT_PLANE_SIZE = 50;
@@ -33521,7 +34183,7 @@ function SectionCapPreview({
33521
34183
  class ScanProxyWorkerClient {
33522
34184
  constructor(workerFactory = () => new Worker(new URL(
33523
34185
  /* @vite-ignore */
33524
- "/assets/scanProxyWorker-B-9VbLIs.js",
34186
+ "/assets/scanProxyWorker-2GtDLk-R.js",
33525
34187
  import.meta.url
33526
34188
  ), { type: "module" })) {
33527
34189
  __publicField(this, "worker", null);
@@ -33897,35 +34559,6 @@ function MatrixGlyphVolume({
33897
34559
  )
33898
34560
  ] });
33899
34561
  }
33900
- const SCAN_COLD_RGB = normalizedRgb(SCAN_CONTEXT_EDGE_COLOR);
33901
- const SCAN_MID_RGB = normalizedRgb(SCAN_RISK_MID_COLOR);
33902
- const SCAN_HOT_RGB = normalizedRgb(SCAN_RISK_HOT_COLOR);
33903
- function normalizedRgb([r2, g2, b2]) {
33904
- return [r2 / 255, g2 / 255, b2 / 255];
33905
- }
33906
- function clamp01$1(value) {
33907
- return Math.max(0, Math.min(1, value));
33908
- }
33909
- function mix(a2, b2, t2) {
33910
- return a2 + (b2 - a2) * clamp01$1(t2);
33911
- }
33912
- function scanRiskFromColor(r2, g2, b2) {
33913
- return clamp01$1(r2 * 1.08 + g2 * 0.48 - b2 * 0.62 - 0.38);
33914
- }
33915
- function scanColorBuffer(source) {
33916
- const out = new Float32Array(source.length);
33917
- for (let index = 0; index < source.length; index += 3) {
33918
- const risk = scanRiskFromColor(source[index] ?? 0, source[index + 1] ?? 0, source[index + 2] ?? 0);
33919
- const lowBand = risk < 0.55;
33920
- const t2 = lowBand ? risk / 0.55 : (risk - 0.55) / 0.45;
33921
- const from = lowBand ? SCAN_COLD_RGB : SCAN_MID_RGB;
33922
- const to = lowBand ? SCAN_MID_RGB : SCAN_HOT_RGB;
33923
- out[index] = mix(from[0], to[0], t2);
33924
- out[index + 1] = mix(from[1], to[1], t2);
33925
- out[index + 2] = mix(from[2], to[2], t2);
33926
- }
33927
- return out;
33928
- }
33929
34562
  function SurfaceFieldMaterial({
33930
34563
  field,
33931
34564
  color: color2,
@@ -33985,64 +34618,174 @@ function ZebraInspectionMaterial({ clippingPlanes }) {
33985
34618
  }
33986
34619
  const EMPTY_CLIPPING_PLANES$1 = [];
33987
34620
  const EMPTY_SECTION_PLANES$1 = [];
34621
+ function scanCellKey(ix, iy, iz) {
34622
+ return `${ix}:${iy}:${iz}`;
34623
+ }
34624
+ function buildDirectScanCellColors(pointCloud, grid, matrix) {
34625
+ const accumulators = /* @__PURE__ */ new Map();
34626
+ const point = new Vector3();
34627
+ const [originX, originY, originZ] = grid.origin;
34628
+ const sampleCount = Math.floor(pointCloud.positions.length / 3);
34629
+ for (let sample = 0; sample < sampleCount; sample += 1) {
34630
+ const offset = sample * 3;
34631
+ point.set(pointCloud.positions[offset], pointCloud.positions[offset + 1], pointCloud.positions[offset + 2]).applyMatrix4(matrix);
34632
+ const ix = Math.floor((point.x - originX) / grid.cellSize);
34633
+ const iy = Math.floor((point.y - originY) / grid.cellSize);
34634
+ const iz = Math.floor((point.z - originZ) / grid.cellSize);
34635
+ const key = scanCellKey(ix, iy, iz);
34636
+ const accumulator = accumulators.get(key) ?? { ix, iy, iz, r: 0, g: 0, b: 0, count: 0 };
34637
+ accumulator.r += pointCloud.colors[offset] ?? 0.35;
34638
+ accumulator.g += pointCloud.colors[offset + 1] ?? 0.35;
34639
+ accumulator.b += pointCloud.colors[offset + 2] ?? 0.35;
34640
+ accumulator.count += 1;
34641
+ accumulators.set(key, accumulator);
34642
+ }
34643
+ const directColors = /* @__PURE__ */ new Map();
34644
+ for (const [key, accumulator] of accumulators) {
34645
+ directColors.set(key, {
34646
+ ix: accumulator.ix,
34647
+ iy: accumulator.iy,
34648
+ iz: accumulator.iz,
34649
+ color: [accumulator.r / accumulator.count, accumulator.g / accumulator.count, accumulator.b / accumulator.count]
34650
+ });
34651
+ }
34652
+ return directColors;
34653
+ }
34654
+ function nearestScanCellColor(ix, iy, iz, coloredCells) {
34655
+ let best = coloredCells[0];
34656
+ let bestDistance = Number.POSITIVE_INFINITY;
34657
+ for (const candidate of coloredCells) {
34658
+ const distance = (candidate.ix - ix) ** 2 + (candidate.iy - iy) ** 2 + (candidate.iz - iz) ** 2;
34659
+ if (distance >= bestDistance) continue;
34660
+ best = candidate;
34661
+ bestDistance = distance;
34662
+ }
34663
+ return (best == null ? void 0 : best.color) ?? [0.35, 0.35, 0.35];
34664
+ }
34665
+ function createScanAnalysisColorGeometry(geometry, pointCloud, grid, matrix) {
34666
+ var _a3;
34667
+ if (!geometry) return null;
34668
+ const position = geometry.getAttribute("position");
34669
+ const scanCell = geometry.getAttribute("scanCell");
34670
+ if (!position || !scanCell || position.count !== scanCell.count) return null;
34671
+ const directColors = buildDirectScanCellColors(pointCloud, grid, matrix);
34672
+ const coloredCells = [...directColors.values()];
34673
+ if (coloredCells.length === 0) return null;
34674
+ const resolvedColors = /* @__PURE__ */ new Map();
34675
+ const colors = new Float32Array(position.count * 3);
34676
+ for (let vertex = 0; vertex < position.count; vertex += 1) {
34677
+ const ix = Math.round(scanCell.getX(vertex));
34678
+ const iy = Math.round(scanCell.getY(vertex));
34679
+ const iz = Math.round(scanCell.getZ(vertex));
34680
+ const key = scanCellKey(ix, iy, iz);
34681
+ let color2 = resolvedColors.get(key);
34682
+ if (!color2) {
34683
+ color2 = ((_a3 = directColors.get(key)) == null ? void 0 : _a3.color) ?? nearestScanCellColor(ix, iy, iz, coloredCells);
34684
+ resolvedColors.set(key, color2);
34685
+ }
34686
+ const offset = vertex * 3;
34687
+ colors[offset] = color2[0];
34688
+ colors[offset + 1] = color2[1];
34689
+ colors[offset + 2] = color2[2];
34690
+ }
34691
+ const coloredGeometry = geometry.clone();
34692
+ coloredGeometry.setAttribute("color", new BufferAttribute(colors, 3));
34693
+ return coloredGeometry;
34694
+ }
33988
34695
  function ScanProxyLayer({
33989
34696
  geometry,
33990
34697
  color: color2,
33991
34698
  fillOpacity,
33992
34699
  wireOpacity,
33993
34700
  clippingPlanes,
33994
- additiveWire
34701
+ volumeFill = false,
34702
+ showWire = true,
34703
+ analysisGeometry
33995
34704
  }) {
33996
- if (!geometry) return null;
34705
+ const fillGeometry = analysisGeometry ?? geometry;
34706
+ if (!geometry || !fillGeometry) return null;
33997
34707
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
33998
- /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34708
+ /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry: fillGeometry, raycast: () => null, children: volumeFill ? /* @__PURE__ */ jsxRuntimeExports.jsx(
33999
34709
  "meshBasicMaterial",
34000
34710
  {
34001
34711
  color: color2,
34002
34712
  transparent: true,
34003
- opacity: fillOpacity,
34713
+ opacity: Math.min(0.1, fillOpacity * 0.2),
34004
34714
  side: DoubleSide,
34005
34715
  depthWrite: false,
34716
+ blending: AdditiveBlending,
34717
+ toneMapped: false,
34718
+ clippingPlanes
34719
+ }
34720
+ ) : analysisGeometry ? /* @__PURE__ */ jsxRuntimeExports.jsx(
34721
+ "meshBasicMaterial",
34722
+ {
34723
+ vertexColors: true,
34724
+ opacity: fillOpacity,
34725
+ side: DoubleSide,
34726
+ alphaHash: true,
34727
+ alphaToCoverage: true,
34728
+ depthWrite: true,
34729
+ toneMapped: false,
34730
+ clippingPlanes
34731
+ }
34732
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
34733
+ "meshBasicMaterial",
34734
+ {
34735
+ color: color2,
34736
+ opacity: fillOpacity,
34737
+ side: DoubleSide,
34738
+ alphaHash: true,
34739
+ alphaToCoverage: true,
34740
+ depthWrite: true,
34006
34741
  toneMapped: false,
34007
34742
  clippingPlanes
34008
34743
  }
34009
34744
  ) }),
34010
- /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34745
+ showWire && /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34011
34746
  "meshBasicMaterial",
34012
34747
  {
34013
34748
  color: color2,
34014
- transparent: true,
34015
34749
  opacity: wireOpacity,
34016
34750
  wireframe: true,
34017
34751
  side: DoubleSide,
34018
- depthWrite: false,
34019
- blending: additiveWire ? AdditiveBlending : NormalBlending,
34752
+ alphaHash: true,
34753
+ alphaToCoverage: true,
34754
+ depthWrite: true,
34020
34755
  toneMapped: false,
34021
34756
  clippingPlanes
34022
34757
  }
34023
34758
  ) })
34024
34759
  ] });
34025
34760
  }
34026
- function objectColorScanLayerStyles(color2) {
34027
- return SCAN_PROXY_LAYER_STYLES.map((layer) => ({ ...layer, color: color2 }));
34028
- }
34029
34761
  function ScanProxyVolume({
34030
34762
  proxy,
34031
34763
  clippingPlanes,
34032
- objectColor
34764
+ objectColor,
34765
+ volumeFill = false,
34766
+ showWire = true,
34767
+ analysisGeometries
34033
34768
  }) {
34034
- const layers = objectColor ? objectColorScanLayerStyles(objectColor) : SCAN_PROXY_LAYER_STYLES;
34035
- const additiveWire = objectColor === void 0;
34036
- return /* @__PURE__ */ jsxRuntimeExports.jsx("group", { userData: { scanProxyTelemetry: proxy.telemetry }, children: layers.map((layer) => /* @__PURE__ */ jsxRuntimeExports.jsx(
34037
- ScanProxyLayer,
34038
- {
34039
- geometry: proxy.geometries[layer.material],
34040
- clippingPlanes,
34041
- additiveWire,
34042
- ...layer
34043
- },
34044
- layer.material
34045
- )) });
34769
+ const layers = reactExports.useMemo(() => objectColor ? scanMaterialLayerStyles(objectColor) : SCAN_PROXY_LAYER_STYLES, [objectColor]);
34770
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("group", { userData: { scanProxyTelemetry: proxy.telemetry }, children: layers.map((layer) => {
34771
+ const analysisGeometry = (analysisGeometries == null ? void 0 : analysisGeometries[layer.material]) ?? null;
34772
+ const fillOpacity = analysisGeometry ? Math.max(layer.fillOpacity, 0.78) : layer.fillOpacity;
34773
+ const wireOpacity = analysisGeometry ? Math.max(layer.wireOpacity, 0.34) : layer.wireOpacity;
34774
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
34775
+ ScanProxyLayer,
34776
+ {
34777
+ geometry: proxy.geometries[layer.material],
34778
+ analysisGeometry,
34779
+ clippingPlanes,
34780
+ volumeFill,
34781
+ showWire,
34782
+ ...layer,
34783
+ fillOpacity,
34784
+ wireOpacity
34785
+ },
34786
+ layer.material
34787
+ );
34788
+ }) });
34046
34789
  }
34047
34790
  function ForgeObject({
34048
34791
  obj,
@@ -34100,11 +34843,10 @@ function ForgeObject({
34100
34843
  const inspectPointGeo = reactExports.useMemo(() => {
34101
34844
  if (!inspectPointCloud) return null;
34102
34845
  const geometry = new BufferGeometry();
34103
- const colors = isScalarScan ? scanColorBuffer(inspectPointCloud.colors) : inspectPointCloud.colors;
34104
34846
  geometry.setAttribute("position", new BufferAttribute(inspectPointCloud.positions, 3));
34105
- geometry.setAttribute("color", new BufferAttribute(colors, 3));
34847
+ geometry.setAttribute("color", new BufferAttribute(inspectPointCloud.colors, 3));
34106
34848
  return geometry;
34107
- }, [inspectPointCloud, isScalarScan]);
34849
+ }, [inspectPointCloud]);
34108
34850
  const inspectMeshColorGeo = reactExports.useMemo(() => {
34109
34851
  if (!solidGeo || !inspectMeshColors) return null;
34110
34852
  const position = solidGeo.getAttribute("position");
@@ -34123,6 +34865,11 @@ function ForgeObject({
34123
34865
  );
34124
34866
  const scanProxyState = useScanProxyWorkerGeometry(solidGeo, wantsScanProxy, scanProxyGrid, matrix);
34125
34867
  const scanProxy = scanProxyState.proxy;
34868
+ const scanAnalysisGeometries = reactExports.useMemo(() => {
34869
+ if (!isScalarScan || !scanProxy || !inspectPointCloud) return null;
34870
+ const shell = createScanAnalysisColorGeometry(scanProxy.geometries.shell, inspectPointCloud, scanProxy.grid, matrix);
34871
+ return shell ? { shell } : null;
34872
+ }, [inspectPointCloud, isScalarScan, matrix, scanProxy]);
34126
34873
  reactExports.useEffect(() => {
34127
34874
  return () => {
34128
34875
  solidGeo == null ? void 0 : solidGeo.dispose();
@@ -34144,16 +34891,13 @@ function ForgeObject({
34144
34891
  inspectHeatmapField == null ? void 0 : inspectHeatmapField.texture.dispose();
34145
34892
  };
34146
34893
  }, [inspectHeatmapField]);
34894
+ reactExports.useEffect(() => {
34895
+ return () => {
34896
+ Object.values(scanAnalysisGeometries ?? {}).forEach((geometry) => geometry == null ? void 0 : geometry.dispose());
34897
+ };
34898
+ }, [scanAnalysisGeometries]);
34147
34899
  if (!solidGeo || !settings.visible) return null;
34148
34900
  const effectiveRenderMode = isInteracting && renderMode === "overlay" ? "solid" : renderMode;
34149
- const isInspecting = inspectChannel !== "none";
34150
- const showInspectHeatmap = Boolean(
34151
- isScalarInspect && !isScalarScan && inspectHeatmapField && (inspectDisplayMode === "heatmap" || inspectDisplayMode === "both")
34152
- );
34153
- const showScalarContextMesh = Boolean(isScalarInspect && (isScalarScan || !showInspectHeatmap));
34154
- const showInspectPoints = Boolean(
34155
- isScalarInspect && inspectPointGeo && (inspectDisplayMode === "points" || inspectDisplayMode === "both" || isScalarScan)
34156
- );
34157
34901
  const renderStylePreset = getRenderStylePreset(renderStyle);
34158
34902
  const materialDefaults = renderStylePreset.material;
34159
34903
  const surfaceField = renderStylePreset.surfaceField;
@@ -34166,6 +34910,7 @@ function ForgeObject({
34166
34910
  const materialOpacity = Math.min(meshOpacity, styleOpacity);
34167
34911
  const transmission = authoredMaterialTransmission ?? transparentDefaults.transmission;
34168
34912
  const isTransparent = materialOpacity < 1 || transmission > 0;
34913
+ const isInspecting = inspectChannel !== "none";
34169
34914
  const showSolid = isInspecting || effectiveRenderMode !== "wireframe";
34170
34915
  const showEdges = !isInspecting && effectiveRenderMode === "overlay" && renderStyle !== "scan" && renderStyle !== "matrix";
34171
34916
  const showWire = !isInspecting && effectiveRenderMode === "wireframe";
@@ -34177,7 +34922,16 @@ function ForgeObject({
34177
34922
  const showFloatingObject = inspectChannel !== "floating" || hasInspectMeshColors || inspectColor !== "#000000";
34178
34923
  const showScanRenderStyle = showSolid && inspectChannel === "none" && renderStyle === "scan";
34179
34924
  const showMatrixRenderStyle = showSolid && inspectChannel === "none" && renderStyle === "matrix";
34180
- const showScalarScanProxy = showSolid && isScalarScan && scanProxy !== null;
34925
+ const showScalarScanProxy = showSolid && isScalarScan && scanProxy !== null && (scanAnalysisGeometries == null ? void 0 : scanAnalysisGeometries.shell) != null;
34926
+ const showInspectHeatmap = Boolean(
34927
+ isScalarInspect && inspectHeatmapField && (!isScalarScan && (inspectDisplayMode === "heatmap" || inspectDisplayMode === "both") || isScalarScan && !showScalarScanProxy)
34928
+ );
34929
+ const showScalarContextMesh = Boolean(
34930
+ isScalarInspect && (!isScalarScan && !showInspectHeatmap || isScalarScan && !showScalarScanProxy && !showInspectHeatmap)
34931
+ );
34932
+ const showInspectPoints = Boolean(
34933
+ isScalarInspect && inspectPointGeo && (inspectDisplayMode === "points" || inspectDisplayMode === "both")
34934
+ );
34181
34935
  const showScanFallbackMesh = showScanRenderStyle && scanProxy === null;
34182
34936
  const showMatrixFallbackMesh = showMatrixRenderStyle && scanProxy === null;
34183
34937
  const showHiddenPickMesh = showScanRenderStyle || showMatrixRenderStyle || showScalarScanProxy;
@@ -34256,7 +35010,7 @@ function ForgeObject({
34256
35010
  clippingPlanes: effectiveClippingPlanes
34257
35011
  }
34258
35012
  ) }),
34259
- showScalarScanProxy && /* @__PURE__ */ jsxRuntimeExports.jsx(ScanProxyVolume, { proxy: scanProxy, clippingPlanes: effectiveClippingPlanes }),
35013
+ showScalarScanProxy && scanProxy && scanAnalysisGeometries && /* @__PURE__ */ jsxRuntimeExports.jsx(ScanProxyVolume, { proxy: scanProxy, clippingPlanes: effectiveClippingPlanes, analysisGeometries: scanAnalysisGeometries }),
34260
35014
  showSolid && showScalarContextMesh && !showScalarScanProxy && /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry: solidGeo, userData: { forgeMesh: true }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34261
35015
  "meshBasicMaterial",
34262
35016
  {
@@ -34295,7 +35049,8 @@ function ForgeObject({
34295
35049
  side: DoubleSide,
34296
35050
  toneMapped: false,
34297
35051
  clippingPlanes: effectiveClippingPlanes
34298
- }
35052
+ },
35053
+ inspectHeatmapField.texture.uuid
34299
35054
  ) }),
34300
35055
  showInspectPoints && inspectPointGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("points", { geometry: inspectPointGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34301
35056
  "pointsMaterial",
@@ -34340,32 +35095,28 @@ function ForgeObject({
34340
35095
  ) }),
34341
35096
  edgesGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: edgesGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx("lineBasicMaterial", { color: "#d7e1eb", transparent: true, opacity: 0.36, depthWrite: false, clippingPlanes: effectiveClippingPlanes }) })
34342
35097
  ] }),
34343
- showScanRenderStyle && (scanProxy ? /* @__PURE__ */ jsxRuntimeExports.jsx(ScanProxyVolume, { proxy: scanProxy, clippingPlanes: effectiveClippingPlanes, objectColor: settings.color }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
34344
- showScanFallbackMesh && /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry: solidGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34345
- "meshBasicMaterial",
34346
- {
34347
- color: settings.color,
34348
- transparent: true,
34349
- opacity: 0.14,
34350
- side: DoubleSide,
34351
- depthWrite: false,
34352
- blending: NormalBlending,
34353
- toneMapped: false,
34354
- clippingPlanes: effectiveClippingPlanes
34355
- }
34356
- ) }),
34357
- edgesGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: edgesGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
34358
- "lineBasicMaterial",
34359
- {
34360
- color: settings.color,
34361
- transparent: true,
34362
- opacity: 0.62,
34363
- depthWrite: false,
34364
- blending: NormalBlending,
34365
- clippingPlanes: effectiveClippingPlanes
34366
- }
34367
- ) })
34368
- ] })),
35098
+ showScanRenderStyle && (scanProxy ? /* @__PURE__ */ jsxRuntimeExports.jsx(
35099
+ ScanProxyVolume,
35100
+ {
35101
+ proxy: scanProxy,
35102
+ clippingPlanes: effectiveClippingPlanes,
35103
+ objectColor: settings.color,
35104
+ volumeFill: true,
35105
+ showWire: false
35106
+ }
35107
+ ) : showScanFallbackMesh && /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry: solidGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
35108
+ "meshBasicMaterial",
35109
+ {
35110
+ color: scanMaterialShellColor(settings.color),
35111
+ transparent: true,
35112
+ opacity: 0.08,
35113
+ side: DoubleSide,
35114
+ depthWrite: false,
35115
+ blending: AdditiveBlending,
35116
+ toneMapped: false,
35117
+ clippingPlanes: effectiveClippingPlanes
35118
+ }
35119
+ ) })),
34369
35120
  showMatrixRenderStyle && (scanProxy ? /* @__PURE__ */ jsxRuntimeExports.jsx(
34370
35121
  MatrixGlyphVolume,
34371
35122
  {
@@ -34782,9 +35533,12 @@ function colorToHex(color2) {
34782
35533
  return rgbToHex(color2);
34783
35534
  }
34784
35535
  function fmt(value) {
34785
- return Number.isInteger(value) ? String(value) : value.toFixed(1);
35536
+ if (!Number.isFinite(value)) return "0";
35537
+ if (Number.isInteger(value)) return String(value);
35538
+ const fixed = Math.abs(value) < 10 ? value.toFixed(2) : value.toFixed(1);
35539
+ return fixed.replace(/\.?0+$/, "");
34786
35540
  }
34787
- function inspectionLegendDefinitionFor(channel, displayMode) {
35541
+ function inspectionLegendDefinitionFor(channel, displayMode, legendOptions = {}) {
34788
35542
  switch (channel) {
34789
35543
  case "mask":
34790
35544
  return {
@@ -34810,8 +35564,11 @@ function inspectionLegendDefinitionFor(channel, displayMode) {
34810
35564
  case "rig":
34811
35565
  return {
34812
35566
  title: "Rig Skeleton",
34813
- summary: "Geometry is ghosted while joint axes, rotation arcs, and kinematic links stay bright.",
35567
+ summary: "Geometry is ghosted while joint axes, link pins, and rigid edges stay bright.",
34814
35568
  swatches: [
35569
+ { label: "Link pin", detail: "kinematic node", color: rgbToHex(RIG_KINEMATIC_LINK_COLOR) },
35570
+ { label: "Fixed link", detail: "anchored node", color: rgbToHex(RIG_FIXED_LINK_COLOR) },
35571
+ { label: "Rigid edge", detail: "distance member", color: rgbToHex(RIG_KINEMATIC_EDGE_COLOR) },
34815
35572
  { label: "Joint link", detail: "parent-child chain", color: rgbToHex(RIG_LINK_COLOR) },
34816
35573
  { label: "Part link", detail: "joint to moving body", color: rgbToHex(RIG_PART_LINK_COLOR) },
34817
35574
  { label: "Hidden joint", detail: "fixed follower", color: rgbToHex(RIG_HIDDEN_LINK_COLOR) },
@@ -34869,33 +35626,25 @@ function inspectionLegendDefinitionFor(channel, displayMode) {
34869
35626
  ]
34870
35627
  };
34871
35628
  case "thickness": {
34872
- const options = DEFAULT_THICKNESS_INSPECTION_OPTIONS;
35629
+ const colorRange = legendOptions.thicknessColorRange ?? DEFAULT_THICKNESS_COLOR_RANGE;
35630
+ const midpoint = colorRange.min + (colorRange.max - colorRange.min) / 2;
35631
+ const ramp = {
35632
+ colors: THICKNESS_GRADIENT_COLORS.map(colorToHex),
35633
+ leftLabel: `${fmt(colorRange.min)} mm`,
35634
+ centerLabel: `${fmt(midpoint)} mm`,
35635
+ rightLabel: `${fmt(colorRange.max)} mm`
35636
+ };
34873
35637
  if (displayMode === "scan") {
34874
35638
  return {
34875
35639
  title: "Thickness Scan",
34876
- summary: "Cyan is the context shell. Warm samples mark thin or risky material from the same thickness analysis.",
34877
- swatches: [
34878
- { label: "Shell", detail: "context", color: colorToHex(SCAN_CONTEXT_COLOR) },
34879
- { label: "Trace", detail: "surface edge", color: colorToHex(SCAN_CONTEXT_EDGE_COLOR) },
34880
- { label: "Warn", detail: `<= ${fmt(options.warnThickness)} mm`, color: colorToHex(SCAN_RISK_MID_COLOR) },
34881
- { label: "Critical", detail: `<= ${fmt(options.minThickness)} mm`, color: colorToHex(SCAN_RISK_HOT_COLOR) }
34882
- ]
35640
+ summary: "Each scan box uses the same continuous wall-thickness color ramp as the heatmap.",
35641
+ ramp
34883
35642
  };
34884
35643
  }
34885
35644
  return {
34886
35645
  title: "Wall Thickness",
34887
- summary: "Red/orange is thin material. Green/blue has more printable or machinable thickness.",
34888
- ramp: {
34889
- colors: [
34890
- colorToHex(THICKNESS_COLORS.critical),
34891
- colorToHex(THICKNESS_COLORS.warning),
34892
- colorToHex(THICKNESS_COLORS.ok),
34893
- colorToHex(THICKNESS_COLORS.thick)
34894
- ],
34895
- leftLabel: `<= ${fmt(options.minThickness)} mm`,
34896
- centerLabel: `${fmt(options.warnThickness)} mm warn`,
34897
- rightLabel: `>= ${fmt(options.maxThickness)} mm`
34898
- }
35646
+ summary: "Color maps continuously from thinner to thicker material across the selected range.",
35647
+ ramp
34899
35648
  };
34900
35649
  }
34901
35650
  case "roughness": {
@@ -34903,12 +35652,20 @@ function inspectionLegendDefinitionFor(channel, displayMode) {
34903
35652
  if (displayMode === "scan") {
34904
35653
  return {
34905
35654
  title: "Roughness Scan",
34906
- summary: "Cyan is the context shell. Warm samples mark sharper or harsher mesh transitions from the same roughness analysis.",
35655
+ summary: "Each scan box is colored from the same roughness analysis field as the heatmap.",
34907
35656
  swatches: [
34908
- { label: "Shell", detail: "context", color: colorToHex(SCAN_CONTEXT_COLOR) },
34909
- { label: "Trace", detail: "surface edge", color: colorToHex(SCAN_CONTEXT_EDGE_COLOR) },
34910
- { label: "Sharp", detail: `>= ${fmt(options.sharpAngleDeg)} deg`, color: colorToHex(SCAN_RISK_MID_COLOR) },
34911
- { label: "Harsh", detail: `>= ${fmt(options.harshAngleDeg)} deg`, color: colorToHex(SCAN_RISK_HOT_COLOR) }
35657
+ { label: "Smooth", detail: `< ${fmt(options.smoothAngleDeg)} deg`, color: colorToHex(ROUGHNESS_COLORS.smooth) },
35658
+ {
35659
+ label: "Moderate",
35660
+ detail: `${fmt(options.smoothAngleDeg)}-${fmt(options.sharpAngleDeg)} deg`,
35661
+ color: colorToHex(ROUGHNESS_COLORS.moderate)
35662
+ },
35663
+ {
35664
+ label: "Sharp",
35665
+ detail: `${fmt(options.sharpAngleDeg)}-${fmt(options.harshAngleDeg)} deg`,
35666
+ color: colorToHex(ROUGHNESS_COLORS.sharp)
35667
+ },
35668
+ { label: "Harsh", detail: `>= ${fmt(options.harshAngleDeg)} deg`, color: colorToHex(ROUGHNESS_COLORS.harsh) }
34912
35669
  ]
34913
35670
  };
34914
35671
  }
@@ -34968,6 +35725,78 @@ const swatchGridStyle = {
34968
35725
  gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
34969
35726
  gap: 6
34970
35727
  };
35728
+ const rangeControlsStyle = {
35729
+ display: "grid",
35730
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
35731
+ gap: 7,
35732
+ marginTop: 7
35733
+ };
35734
+ const rangeLabelStyle = {
35735
+ display: "grid",
35736
+ gap: 3,
35737
+ minWidth: 0,
35738
+ color: "var(--fc-textDim)",
35739
+ fontSize: 10
35740
+ };
35741
+ const rangeInputStyle = {
35742
+ width: "100%",
35743
+ minWidth: 0,
35744
+ boxSizing: "border-box",
35745
+ height: 24,
35746
+ borderRadius: 6,
35747
+ border: "1px solid var(--fc-border)",
35748
+ background: "var(--fc-bg)",
35749
+ color: "var(--fc-text)",
35750
+ font: "inherit",
35751
+ fontSize: 11,
35752
+ padding: "2px 6px",
35753
+ outline: "none"
35754
+ };
35755
+ const dualSliderStyle = {
35756
+ position: "relative",
35757
+ height: 28,
35758
+ marginTop: 9,
35759
+ touchAction: "none"
35760
+ };
35761
+ const sliderTrackStyle = {
35762
+ position: "absolute",
35763
+ left: 0,
35764
+ right: 0,
35765
+ top: 12,
35766
+ height: 5,
35767
+ borderRadius: 999,
35768
+ background: "color-mix(in srgb, var(--fc-border) 76%, transparent)"
35769
+ };
35770
+ const sliderActiveStyle = {
35771
+ position: "absolute",
35772
+ top: 12,
35773
+ height: 5,
35774
+ borderRadius: 999,
35775
+ background: "linear-gradient(90deg, #ff1c1c, #ffde00, #3cdc5a, #4691ff)"
35776
+ };
35777
+ const sliderThumbStyle = {
35778
+ position: "absolute",
35779
+ top: 5,
35780
+ width: 18,
35781
+ height: 18,
35782
+ borderRadius: "50%",
35783
+ border: "2px solid var(--fc-bgPanel)",
35784
+ background: "var(--fc-text)",
35785
+ boxShadow: "0 1px 5px rgba(0, 0, 0, 0.3)",
35786
+ padding: 0,
35787
+ cursor: "grab",
35788
+ touchAction: "none"
35789
+ };
35790
+ const sliderScaleStyle = {
35791
+ display: "grid",
35792
+ gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
35793
+ gap: 6,
35794
+ marginTop: 1,
35795
+ color: "var(--fc-textDim)",
35796
+ fontSize: 9
35797
+ };
35798
+ const SLIDER_STEP = 0.01;
35799
+ const MIN_RANGE_SPAN = 1e-3;
34971
35800
  function Ramp({ compact, ramp }) {
34972
35801
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { minWidth: 0 }, children: [
34973
35802
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -35024,15 +35853,272 @@ function Swatch({ item }) {
35024
35853
  ] })
35025
35854
  ] });
35026
35855
  }
35856
+ function parseRangeInput(value, fallback) {
35857
+ const parsed = Number(value);
35858
+ return Number.isFinite(parsed) ? parsed : fallback;
35859
+ }
35860
+ function rangeDraftValue(value) {
35861
+ return Number.isFinite(value) ? String(value) : "";
35862
+ }
35863
+ function rangeKey(range) {
35864
+ return `${range.min}:${range.max}`;
35865
+ }
35866
+ function clamp(value, min, max2) {
35867
+ return Math.max(min, Math.min(max2, value));
35868
+ }
35869
+ function snapSliderValue(value) {
35870
+ return Number((Math.round(value / SLIDER_STEP) * SLIDER_STEP).toFixed(2));
35871
+ }
35872
+ function formatSliderTick(value) {
35873
+ if (!Number.isFinite(value)) return "0";
35874
+ return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, "");
35875
+ }
35876
+ function sliderMaxFor(range) {
35877
+ const target = Math.max(6, range.max * 1.25, range.min + 1);
35878
+ if (target <= 10) return Math.ceil(target);
35879
+ if (target <= 50) return Math.ceil(target / 5) * 5;
35880
+ if (target <= 100) return Math.ceil(target / 10) * 10;
35881
+ return Math.ceil(target / 50) * 50;
35882
+ }
35883
+ function DualRangeSlider({
35884
+ range,
35885
+ sliderMax,
35886
+ onDraftChange,
35887
+ onCommit
35888
+ }) {
35889
+ const trackRef = reactExports.useRef(null);
35890
+ const minPercent = clamp(range.min, 0, sliderMax) / sliderMax * 100;
35891
+ const maxPercent = clamp(range.max, 0, sliderMax) / sliderMax * 100;
35892
+ const valueFromPointer = (event) => {
35893
+ var _a3;
35894
+ const rect = (_a3 = trackRef.current) == null ? void 0 : _a3.getBoundingClientRect();
35895
+ if (!rect || rect.width <= 0) return null;
35896
+ const ratio = clamp((event.clientX - rect.left) / rect.width, 0, 1);
35897
+ return snapSliderValue(ratio * sliderMax);
35898
+ };
35899
+ const updateThumb = (thumb, value) => {
35900
+ if (thumb === "min") {
35901
+ onDraftChange({ min: clamp(value, 0, range.max - MIN_RANGE_SPAN), max: range.max });
35902
+ } else {
35903
+ onDraftChange({ min: range.min, max: Math.max(range.min + MIN_RANGE_SPAN, clamp(value, MIN_RANGE_SPAN, sliderMax)) });
35904
+ }
35905
+ };
35906
+ const handlePointerMove = (thumb, event) => {
35907
+ if ((event.buttons & 1) !== 1) return;
35908
+ const value = valueFromPointer(event);
35909
+ if (value == null) return;
35910
+ updateThumb(thumb, value);
35911
+ };
35912
+ const handlePointerDown = (thumb, event) => {
35913
+ event.currentTarget.setPointerCapture(event.pointerId);
35914
+ const value = valueFromPointer(event);
35915
+ if (value != null) updateThumb(thumb, value);
35916
+ };
35917
+ const handleKeyDown = (thumb, event) => {
35918
+ let next = null;
35919
+ if (event.key === "ArrowLeft" || event.key === "ArrowDown") next = range[thumb] - SLIDER_STEP;
35920
+ if (event.key === "ArrowRight" || event.key === "ArrowUp") next = range[thumb] + SLIDER_STEP;
35921
+ if (event.key === "Home") next = thumb === "min" ? 0 : range.min + MIN_RANGE_SPAN;
35922
+ if (event.key === "End") next = thumb === "min" ? range.max - MIN_RANGE_SPAN : sliderMax;
35923
+ if (event.key === "Enter") {
35924
+ event.preventDefault();
35925
+ event.currentTarget.blur();
35926
+ return;
35927
+ }
35928
+ if (next == null) return;
35929
+ event.preventDefault();
35930
+ updateThumb(thumb, snapSliderValue(next));
35931
+ };
35932
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
35933
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: trackRef, style: dualSliderStyle, children: [
35934
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: sliderTrackStyle }),
35935
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35936
+ "div",
35937
+ {
35938
+ style: {
35939
+ ...sliderActiveStyle,
35940
+ left: `${minPercent}%`,
35941
+ width: `${Math.max(0, maxPercent - minPercent)}%`
35942
+ }
35943
+ }
35944
+ ),
35945
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35946
+ "button",
35947
+ {
35948
+ type: "button",
35949
+ role: "slider",
35950
+ "aria-label": "Minimum thickness color",
35951
+ "aria-valuemin": 0,
35952
+ "aria-valuemax": range.max,
35953
+ "aria-valuenow": range.min,
35954
+ onPointerDown: (event) => handlePointerDown("min", event),
35955
+ onPointerMove: (event) => handlePointerMove("min", event),
35956
+ onPointerUp: onCommit,
35957
+ onLostPointerCapture: onCommit,
35958
+ onBlur: onCommit,
35959
+ onKeyDown: (event) => handleKeyDown("min", event),
35960
+ style: {
35961
+ ...sliderThumbStyle,
35962
+ left: `${minPercent}%`,
35963
+ transform: "translateX(-50%)",
35964
+ zIndex: range.min >= range.max - SLIDER_STEP ? 3 : 2
35965
+ }
35966
+ }
35967
+ ),
35968
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35969
+ "button",
35970
+ {
35971
+ type: "button",
35972
+ role: "slider",
35973
+ "aria-label": "Maximum thickness color",
35974
+ "aria-valuemin": range.min,
35975
+ "aria-valuemax": sliderMax,
35976
+ "aria-valuenow": range.max,
35977
+ onPointerDown: (event) => handlePointerDown("max", event),
35978
+ onPointerMove: (event) => handlePointerMove("max", event),
35979
+ onPointerUp: onCommit,
35980
+ onLostPointerCapture: onCommit,
35981
+ onBlur: onCommit,
35982
+ onKeyDown: (event) => handleKeyDown("max", event),
35983
+ style: {
35984
+ ...sliderThumbStyle,
35985
+ left: `${maxPercent}%`,
35986
+ transform: "translateX(-50%)",
35987
+ zIndex: 2,
35988
+ background: "var(--fc-accent, #4f8cff)"
35989
+ }
35990
+ }
35991
+ )
35992
+ ] }),
35993
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: sliderScaleStyle, children: [
35994
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "0" }),
35995
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { textAlign: "center" }, children: formatSliderTick(sliderMax / 2) }),
35996
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { textAlign: "right" }, children: formatSliderTick(sliderMax) })
35997
+ ] })
35998
+ ] });
35999
+ }
36000
+ function ThicknessRangeControls({
36001
+ compact,
36002
+ range,
36003
+ onChange
36004
+ }) {
36005
+ const [minDraft, setMinDraft] = reactExports.useState(() => rangeDraftValue(range.min));
36006
+ const [maxDraft, setMaxDraft] = reactExports.useState(() => rangeDraftValue(range.max));
36007
+ const [sliderDraft, setSliderDraft] = reactExports.useState(range);
36008
+ const [sliderMax, setSliderMax] = reactExports.useState(() => sliderMaxFor(range));
36009
+ const sliderDraftRef = reactExports.useRef(range);
36010
+ const committedSliderKeyRef = reactExports.useRef(rangeKey(range));
36011
+ const span = Math.max(1, range.max - range.min);
36012
+ reactExports.useEffect(() => {
36013
+ setMinDraft(rangeDraftValue(range.min));
36014
+ setMaxDraft(rangeDraftValue(range.max));
36015
+ setSliderDraft(range);
36016
+ sliderDraftRef.current = range;
36017
+ committedSliderKeyRef.current = rangeKey(range);
36018
+ }, [range.max, range.min]);
36019
+ const updateSliderDraft = (next) => {
36020
+ sliderDraftRef.current = next;
36021
+ setSliderDraft(next);
36022
+ setMinDraft(rangeDraftValue(next.min));
36023
+ setMaxDraft(rangeDraftValue(next.max));
36024
+ };
36025
+ const commitSliderDraft = () => {
36026
+ const next = sliderDraftRef.current;
36027
+ const nextKey = rangeKey(next);
36028
+ if (nextKey === committedSliderKeyRef.current) return;
36029
+ committedSliderKeyRef.current = nextKey;
36030
+ onChange(next);
36031
+ };
36032
+ const commitMin = () => {
36033
+ const value = minDraft.trim();
36034
+ if (value === "") {
36035
+ setMinDraft(rangeDraftValue(range.min));
36036
+ return;
36037
+ }
36038
+ const min = Math.max(0, parseRangeInput(value, range.min));
36039
+ const max2 = range.max <= min ? min + span : range.max;
36040
+ const next = { min, max: max2 };
36041
+ setSliderMax(sliderMaxFor(next));
36042
+ onChange(next);
36043
+ };
36044
+ const commitMax = () => {
36045
+ const value = maxDraft.trim();
36046
+ if (value === "") {
36047
+ setMaxDraft(rangeDraftValue(range.max));
36048
+ return;
36049
+ }
36050
+ const max2 = Math.max(1e-3, parseRangeInput(value, range.max));
36051
+ const min = range.min >= max2 ? Math.max(0, max2 - span) : range.min;
36052
+ const next = { min, max: max2 };
36053
+ setSliderMax(sliderMaxFor(next));
36054
+ onChange(next);
36055
+ };
36056
+ const handleKeyDown = (event, reset) => {
36057
+ if (event.key === "Enter") {
36058
+ event.preventDefault();
36059
+ event.currentTarget.blur();
36060
+ } else if (event.key === "Escape") {
36061
+ event.preventDefault();
36062
+ reset();
36063
+ event.currentTarget.blur();
36064
+ }
36065
+ };
36066
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
36067
+ "div",
36068
+ {
36069
+ style: {
36070
+ ...rangeControlsStyle,
36071
+ gridTemplateColumns: compact ? "minmax(0, 1fr)" : rangeControlsStyle.gridTemplateColumns
36072
+ },
36073
+ children: [
36074
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(DualRangeSlider, { range: sliderDraft, sliderMax, onDraftChange: updateSliderDraft, onCommit: commitSliderDraft }) }),
36075
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { style: rangeLabelStyle, children: [
36076
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Min" }),
36077
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
36078
+ "input",
36079
+ {
36080
+ type: "number",
36081
+ min: 0,
36082
+ step: 0.1,
36083
+ value: minDraft,
36084
+ onChange: (event) => setMinDraft(event.currentTarget.value),
36085
+ onBlur: commitMin,
36086
+ onKeyDown: (event) => handleKeyDown(event, () => setMinDraft(rangeDraftValue(range.min))),
36087
+ style: rangeInputStyle
36088
+ }
36089
+ )
36090
+ ] }),
36091
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { style: rangeLabelStyle, children: [
36092
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Max" }),
36093
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
36094
+ "input",
36095
+ {
36096
+ type: "number",
36097
+ min: 1e-3,
36098
+ step: 0.1,
36099
+ value: maxDraft,
36100
+ onChange: (event) => setMaxDraft(event.currentTarget.value),
36101
+ onBlur: commitMax,
36102
+ onKeyDown: (event) => handleKeyDown(event, () => setMaxDraft(rangeDraftValue(range.max))),
36103
+ style: rangeInputStyle
36104
+ }
36105
+ )
36106
+ ] })
36107
+ ]
36108
+ }
36109
+ );
36110
+ }
35027
36111
  function InspectionLegend({
35028
36112
  channel,
35029
36113
  displayMode,
35030
36114
  warnings,
35031
- details
36115
+ details,
36116
+ thicknessColorRange,
36117
+ onThicknessColorRangeChange
35032
36118
  }) {
35033
36119
  const panelRef = reactExports.useRef(null);
35034
36120
  const [compact, setCompact] = reactExports.useState(false);
35035
- const definition = inspectionLegendDefinitionFor(channel, displayMode);
36121
+ const definition = inspectionLegendDefinitionFor(channel, displayMode, { thicknessColorRange });
35036
36122
  reactExports.useEffect(() => {
35037
36123
  var _a3;
35038
36124
  const parent = (_a3 = panelRef.current) == null ? void 0 : _a3.parentElement;
@@ -35045,7 +36131,12 @@ function InspectionLegend({
35045
36131
  }, [channel]);
35046
36132
  if (!definition) return null;
35047
36133
  const warning = warnings[0];
35048
- const effectivePanelStyle = compact ? { ...panelStyle, padding: "7px 8px" } : panelStyle;
36134
+ const showThicknessControls = channel === "thickness" && thicknessColorRange !== void 0 && onThicknessColorRangeChange !== void 0;
36135
+ const effectivePanelStyle = {
36136
+ ...panelStyle,
36137
+ ...compact ? { padding: "7px 8px" } : {},
36138
+ ...showThicknessControls ? { pointerEvents: "auto" } : {}
36139
+ };
35049
36140
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: panelRef, className: "fc-viewport-floating-panel fc-inspection-legend", style: effectivePanelStyle, children: [
35050
36141
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: titleStyle, children: [
35051
36142
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: definition.title }),
@@ -35053,6 +36144,7 @@ function InspectionLegend({
35053
36144
  ] }),
35054
36145
  !compact && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: summaryStyle, children: definition.summary }),
35055
36146
  definition.ramp && /* @__PURE__ */ jsxRuntimeExports.jsx(Ramp, { compact, ramp: definition.ramp }),
36147
+ showThicknessControls && /* @__PURE__ */ jsxRuntimeExports.jsx(ThicknessRangeControls, { compact, range: thicknessColorRange, onChange: onThicknessColorRangeChange }),
35056
36148
  details && /* @__PURE__ */ jsxRuntimeExports.jsx(
35057
36149
  "div",
35058
36150
  {
@@ -36672,6 +37764,12 @@ function computeSceneObjectBounds(obj, objectMatrices) {
36672
37764
  }
36673
37765
  return isFiniteBox3(out) ? out : null;
36674
37766
  }
37767
+ if (obj.curve3d) {
37768
+ const b2 = obj.curve3d.bounds;
37769
+ const out = new Box3();
37770
+ if (!expandBoundsByTransformedAabb(out, b2.min, b2.max, matrix)) return null;
37771
+ return isFiniteBox3(out) ? out : null;
37772
+ }
36675
37773
  if (obj.toolpath) {
36676
37774
  const b2 = obj.toolpath.bounds;
36677
37775
  const out = new Box3();
@@ -40322,8 +41420,8 @@ const computeExplodeTreeOffsets = (root, explodeAmount, explodeConfig) => {
40322
41420
  };
40323
41421
  const EMPTY_RIG_INSPECTION_OVERLAY_STATE = {
40324
41422
  joints: [],
40325
- links: [],
40326
- points: [],
41423
+ edges: [],
41424
+ kinematicLinks: [],
40327
41425
  bounds: null
40328
41426
  };
40329
41427
  function isFiniteVector(value) {
@@ -40388,18 +41486,20 @@ function buildRigInspectionOverlayState(args) {
40388
41486
  joints.forEach((joint) => {
40389
41487
  byChild.set(joint.joint.child, joint);
40390
41488
  });
40391
- const links = [];
40392
- const points = [];
41489
+ const edges = [];
41490
+ const kinematicLinks = [];
40393
41491
  if (args.assemblyKinematics) {
40394
- const positions = new Map(args.assemblyKinematics.links.map((link) => [link.name, new Vector3(...link.position)]));
41492
+ const derivedNames = new Set(args.assemblyKinematics.derivedLinks.map((link) => link.name));
41493
+ const controlledNames = new Set(args.assemblyKinematics.angles.filter((angle) => angle.control).map((angle) => angle.c));
40395
41494
  const connectedLinkNames = /* @__PURE__ */ new Set();
41495
+ const positions = new Map(args.assemblyKinematics.links.map((link) => [link.name, new Vector3(...link.position)]));
40396
41496
  for (const edge of args.assemblyKinematics.edges) {
40397
41497
  const start = positions.get(edge.a);
40398
41498
  const end = positions.get(edge.b);
40399
41499
  if (!start || !end || start.distanceToSquared(end) <= 1e-8) continue;
40400
41500
  connectedLinkNames.add(edge.a);
40401
41501
  connectedLinkNames.add(edge.b);
40402
- links.push({
41502
+ edges.push({
40403
41503
  id: `kinematic:${edge.name}`,
40404
41504
  kind: "kinematic",
40405
41505
  start,
@@ -40407,22 +41507,36 @@ function buildRigInspectionOverlayState(args) {
40407
41507
  hidden: false
40408
41508
  });
40409
41509
  }
41510
+ for (const edge of args.assemblyKinematics.frameEdges ?? []) {
41511
+ const start = new Vector3(...edge.start);
41512
+ const end = new Vector3(...edge.end);
41513
+ if (!isFiniteVector(start) || !isFiniteVector(end) || start.distanceToSquared(end) <= 1e-8) continue;
41514
+ edges.push({
41515
+ id: `frame-edge:${edge.name}`,
41516
+ kind: "kinematic",
41517
+ start,
41518
+ end,
41519
+ hidden: false
41520
+ });
41521
+ }
40410
41522
  for (const link of args.assemblyKinematics.links) {
40411
41523
  const position = positions.get(link.name);
40412
41524
  if (!position || !isFiniteVector(position)) continue;
40413
- points.push({
41525
+ kinematicLinks.push({
40414
41526
  id: `kinematic-link:${link.name}`,
40415
41527
  name: link.name,
41528
+ kind: derivedNames.has(link.name) ? "derived" : link.fixed ? "fixed" : link.gaugeFixed ? "gauge" : "moving",
40416
41529
  position,
40417
- fixed: link.fixed === true,
40418
- connected: connectedLinkNames.has(link.name)
41530
+ controlled: controlledNames.has(link.name),
41531
+ connected: connectedLinkNames.has(link.name),
41532
+ hidden: false
40419
41533
  });
40420
41534
  }
40421
41535
  }
40422
41536
  for (const joint of joints) {
40423
41537
  const parentJoint = joint.joint.parent ? byChild.get(joint.joint.parent) : null;
40424
41538
  if (parentJoint && parentJoint.pivotWorld.distanceToSquared(joint.pivotWorld) > 1e-8) {
40425
- links.push({
41539
+ edges.push({
40426
41540
  id: `${joint.joint.name}:hierarchy`,
40427
41541
  kind: "hierarchy",
40428
41542
  start: parentJoint.pivotWorld,
@@ -40432,7 +41546,7 @@ function buildRigInspectionOverlayState(args) {
40432
41546
  }
40433
41547
  const center = childCenter(args, joint.joint.child);
40434
41548
  if (center && center.distanceToSquared(joint.pivotWorld) > 1e-8) {
40435
- links.push({
41549
+ edges.push({
40436
41550
  id: `${joint.joint.name}:part`,
40437
41551
  kind: "part",
40438
41552
  start: joint.pivotWorld,
@@ -40443,12 +41557,12 @@ function buildRigInspectionOverlayState(args) {
40443
41557
  }
40444
41558
  const bounds = new Box3();
40445
41559
  let hasBounds = false;
40446
- for (const point of points) {
40447
- if (expandFinitePoint(bounds, point.position)) hasBounds = true;
41560
+ for (const link of kinematicLinks) {
41561
+ if (expandFinitePoint(bounds, link.position)) hasBounds = true;
40448
41562
  }
40449
- for (const link of links) {
40450
- if (expandFinitePoint(bounds, link.start)) hasBounds = true;
40451
- if (expandFinitePoint(bounds, link.end)) hasBounds = true;
41563
+ for (const edge of edges) {
41564
+ if (expandFinitePoint(bounds, edge.start)) hasBounds = true;
41565
+ if (expandFinitePoint(bounds, edge.end)) hasBounds = true;
40452
41566
  }
40453
41567
  for (const joint of joints) {
40454
41568
  if (expandFinitePoint(bounds, joint.pivotWorld)) hasBounds = true;
@@ -40459,7 +41573,7 @@ function buildRigInspectionOverlayState(args) {
40459
41573
  if (hasBounds) {
40460
41574
  bounds.expandByScalar(Math.max(2, args.axisLength * 0.12));
40461
41575
  }
40462
- return { joints, links, points, bounds: hasBounds ? bounds : null };
41576
+ return { joints, edges, kinematicLinks, bounds: hasBounds ? bounds : null };
40463
41577
  }
40464
41578
  const EMPTY_PLANE_RECORD = Object.freeze({});
40465
41579
  const EMPTY_CLIPPING_RECORD = Object.freeze({});
@@ -40641,6 +41755,8 @@ function useViewportState() {
40641
41755
  const inspectDisplayMode = useForgeStore((s) => s.inspectDisplayMode);
40642
41756
  const scanGranularity = useForgeStore((s) => s.scanGranularity);
40643
41757
  const inspectPointSampleCount = useForgeStore((s) => s.inspectPointSampleCount);
41758
+ const thicknessColorRange = useForgeStore((s) => s.thicknessColorRange);
41759
+ const setThicknessColorRange = useForgeStore((s) => s.setThicknessColorRange);
40644
41760
  const comparisonInspectMode = useForgeStore((s) => s.comparisonInspectMode);
40645
41761
  const comparisonCandidateOpacity = useForgeStore((s) => s.comparisonCandidateOpacity);
40646
41762
  const comparisonReferenceOpacity = useForgeStore((s) => s.comparisonReferenceOpacity);
@@ -41032,6 +42148,8 @@ function useViewportState() {
41032
42148
  inspectChannel,
41033
42149
  inspectDisplayMode,
41034
42150
  inspectPointSampleCount,
42151
+ thicknessColorRange,
42152
+ setThicknessColorRange,
41035
42153
  comparisonInspectMode,
41036
42154
  comparisonCandidateOpacity,
41037
42155
  comparisonReferenceOpacity,
@@ -42088,12 +43206,12 @@ const HoverTooltipLayer = reactExports.forwardRef(function HoverTooltipLayer2({
42088
43206
  top: 0,
42089
43207
  display: "none",
42090
43208
  zIndex: 15,
42091
- background: "var(--fc-floating-panel-bg, #111111d9)",
43209
+ background: "var(--fc-floating-panel-bg, var(--fc-bgPanel, #111111d9))",
42092
43210
  color: "var(--fc-text, #f2f2f2)",
42093
43211
  padding: "3px 7px",
42094
43212
  borderRadius: "var(--fc-floating-panel-radius, 4px)",
42095
- border: "1px solid var(--fc-floating-panel-border, #2a2a2a)",
42096
- boxShadow: "var(--fc-floating-panel-shadow, none)",
43213
+ border: "1px solid var(--fc-floating-panel-border, var(--fc-border, #2a2a2a))",
43214
+ boxShadow: "var(--fc-floating-panel-shadow, 0 8px 20px rgba(0, 0, 0, 0.18))",
42097
43215
  fontFamily: "var(--fc-hud-font, inherit)",
42098
43216
  fontSize: 11,
42099
43217
  fontWeight: 600,
@@ -42424,10 +43542,12 @@ function RenderLabelsOverlay({ labels }) {
42424
43542
  if (labels.length === 0) return null;
42425
43543
  return /* @__PURE__ */ jsxRuntimeExports.jsx("group", { renderOrder: 12, children: labels.map((label) => /* @__PURE__ */ jsxRuntimeExports.jsx(RenderLabelItem, { label }, label.id)) });
42426
43544
  }
42427
- function RigLink({ link, radius }) {
42428
- const segment = reactExports.useMemo(() => resolveSegmentMeshTransform(link.start, link.end), [link.end, link.start]);
43545
+ function RigEdge({ edge, radius }) {
43546
+ const segment = reactExports.useMemo(() => resolveSegmentMeshTransform(edge.start, edge.end), [edge.end, edge.start]);
42429
43547
  if (!segment) return null;
42430
- const color2 = rgbToHex(link.hidden ? RIG_HIDDEN_LINK_COLOR : link.kind === "part" ? RIG_PART_LINK_COLOR : RIG_LINK_COLOR);
43548
+ const color2 = rgbToHex(
43549
+ edge.hidden ? RIG_HIDDEN_LINK_COLOR : edge.kind === "part" ? RIG_PART_LINK_COLOR : edge.kind === "kinematic" ? RIG_KINEMATIC_EDGE_COLOR : RIG_LINK_COLOR
43550
+ );
42431
43551
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
42432
43552
  "mesh",
42433
43553
  {
@@ -42437,16 +43557,35 @@ function RigLink({ link, radius }) {
42437
43557
  userData: { measureHelper: true },
42438
43558
  children: [
42439
43559
  /* @__PURE__ */ jsxRuntimeExports.jsx("cylinderGeometry", { args: [radius, radius, segment.length, 16] }),
42440
- /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: color2, depthTest: false, transparent: true, opacity: link.hidden ? 0.72 : 0.96, toneMapped: false })
43560
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: color2, depthTest: false, transparent: true, opacity: edge.hidden ? 0.72 : 0.96, toneMapped: false })
42441
43561
  ]
42442
43562
  }
42443
43563
  );
42444
43564
  }
42445
- function RigLinkPoint({ point, radius }) {
42446
- const color2 = rgbToHex(point.connected ? RIG_LINK_COLOR : RIG_PART_LINK_COLOR);
42447
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { position: [point.position.x, point.position.y, point.position.z], renderOrder: 94, userData: { measureHelper: true }, children: [
42448
- /* @__PURE__ */ jsxRuntimeExports.jsx("sphereGeometry", { args: [radius, 20, 12] }),
42449
- /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: color2, depthTest: false, transparent: true, opacity: point.fixed ? 1 : 0.9, toneMapped: false })
43565
+ function colorForKinematicLink(link) {
43566
+ if (link.kind === "fixed" || link.kind === "gauge") return rgbToHex(RIG_FIXED_LINK_COLOR);
43567
+ if (link.kind === "derived") return rgbToHex(RIG_DERIVED_LINK_COLOR);
43568
+ return rgbToHex(link.connected ? RIG_KINEMATIC_LINK_COLOR : RIG_PART_LINK_COLOR);
43569
+ }
43570
+ function RigKinematicLinkPin({ link, radius }) {
43571
+ const color2 = colorForKinematicLink(link);
43572
+ const position = [link.position.x, link.position.y, link.position.z];
43573
+ const markerRadius = link.kind === "derived" || !link.connected ? radius * 0.72 : radius;
43574
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("group", { position, renderOrder: 94, userData: { measureHelper: true }, children: [
43575
+ link.kind === "fixed" || link.kind === "gauge" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { renderOrder: 94, userData: { measureHelper: true }, children: [
43576
+ /* @__PURE__ */ jsxRuntimeExports.jsx("boxGeometry", { args: [radius * 1.75, radius * 1.75, radius * 1.05] }),
43577
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: color2, depthTest: false, transparent: true, opacity: 0.98, toneMapped: false })
43578
+ ] }) : link.kind === "derived" || !link.connected ? /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { renderOrder: 94, userData: { measureHelper: true }, children: [
43579
+ /* @__PURE__ */ jsxRuntimeExports.jsx("torusGeometry", { args: [markerRadius * 0.82, markerRadius * 0.18, 8, 24] }),
43580
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: color2, depthTest: false, transparent: true, opacity: 0.98, toneMapped: false })
43581
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { renderOrder: 94, userData: { measureHelper: true }, children: [
43582
+ /* @__PURE__ */ jsxRuntimeExports.jsx("sphereGeometry", { args: [markerRadius, 18, 18] }),
43583
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: color2, depthTest: false, transparent: true, opacity: 0.98, toneMapped: false })
43584
+ ] }),
43585
+ link.controlled && /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { renderOrder: 95, userData: { measureHelper: true }, children: [
43586
+ /* @__PURE__ */ jsxRuntimeExports.jsx("torusGeometry", { args: [radius * 1.32, radius * 0.12, 8, 28] }),
43587
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: rgbToHex(RIG_CONTROL_LINK_COLOR), depthTest: false, transparent: true, opacity: 0.98, toneMapped: false })
43588
+ ] })
42450
43589
  ] });
42451
43590
  }
42452
43591
  function RigInspectionOverlay({ state: state2, config }) {
@@ -42462,16 +43601,21 @@ function RigInspectionOverlay({ state: state2, config }) {
42462
43601
  [config, hiddenColor]
42463
43602
  );
42464
43603
  const maxAxisLength = reactExports.useMemo(() => state2.joints.reduce((max2, joint) => Math.max(max2, joint.axisLength), 0), [state2.joints]);
42465
- const linkRadius = MathUtils.clamp(
42466
- maxAxisLength * config.axisLineRadiusScale * 1.5,
42467
- config.axisLineRadiusMin * 1.5,
42468
- config.axisLineRadiusMax * 2.25
43604
+ const scaleBasis = Math.max(maxAxisLength, config.axisLengthMin);
43605
+ const edgeRadius = MathUtils.clamp(
43606
+ scaleBasis * config.axisLineRadiusScale * 1.15,
43607
+ config.axisLineRadiusMin * 1.15,
43608
+ config.axisLineRadiusMax * 1.75
43609
+ );
43610
+ const linkPinRadius = MathUtils.clamp(
43611
+ scaleBasis * config.axisDotRadiusScale * 3.2,
43612
+ config.axisDotRadiusMin * 2.2,
43613
+ config.axisLineRadiusMax * 3.2
42469
43614
  );
42470
- const pointRadius = Math.max(linkRadius * 2.15, config.axisLineRadiusMin * 2.4);
42471
- if (state2.joints.length === 0 && state2.links.length === 0 && state2.points.length === 0) return null;
43615
+ if (state2.joints.length === 0 && state2.edges.length === 0 && state2.kinematicLinks.length === 0) return null;
42472
43616
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("group", { children: [
42473
- state2.links.map((link) => /* @__PURE__ */ jsxRuntimeExports.jsx(RigLink, { link, radius: linkRadius }, link.id)),
42474
- state2.points.map((point) => /* @__PURE__ */ jsxRuntimeExports.jsx(RigLinkPoint, { point, radius: pointRadius }, point.id)),
43617
+ state2.edges.map((edge) => /* @__PURE__ */ jsxRuntimeExports.jsx(RigEdge, { edge, radius: edgeRadius }, edge.id)),
43618
+ state2.kinematicLinks.map((link) => /* @__PURE__ */ jsxRuntimeExports.jsx(RigKinematicLinkPin, { link, radius: linkPinRadius }, link.id)),
42475
43619
  state2.joints.map((joint) => /* @__PURE__ */ jsxRuntimeExports.jsx(HoveredJointOverlay, { state: joint, config: joint.hidden ? hiddenConfig : config }, joint.joint.name))
42476
43620
  ] });
42477
43621
  }
@@ -42493,7 +43637,7 @@ function directCadKindForPath(path) {
42493
43637
  return null;
42494
43638
  }
42495
43639
  function directCadReferenceCode(path, kind) {
42496
- const fn = kind === "step" ? "importStep" : "importMesh";
43640
+ const fn = kind === "step" ? "Import.step" : "Import.mesh";
42497
43641
  const name = basename(path);
42498
43642
  return [`const imported = ${fn}(${JSON.stringify(path)});`, `return [{ name: ${JSON.stringify(name)}, shape: imported }];`].join("\n");
42499
43643
  }
@@ -42647,12 +43791,12 @@ function useGeometryComparison(args) {
42647
43791
  class InspectWorkerClient {
42648
43792
  constructor(workerFactory = () => new Worker(new URL(
42649
43793
  /* @vite-ignore */
42650
- "/assets/inspectWorker-COyp8XXA.js",
43794
+ "/assets/inspectWorker-CZsCFtQT.js",
42651
43795
  import.meta.url
42652
43796
  ), { type: "module" })) {
42653
43797
  __publicField(this, "worker", null);
42654
43798
  __publicField(this, "reqId", 0);
42655
- __publicField(this, "pending", null);
43799
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
42656
43800
  this.workerFactory = workerFactory;
42657
43801
  }
42658
43802
  getWorker() {
@@ -42660,9 +43804,9 @@ class InspectWorkerClient {
42660
43804
  this.worker = this.workerFactory();
42661
43805
  this.worker.onmessage = (event) => {
42662
43806
  const data = event.data;
42663
- const pending = this.pending;
43807
+ const pending = this.pending.get(data.payload.reqId);
42664
43808
  if (!pending) return;
42665
- this.pending = null;
43809
+ this.pending.delete(data.payload.reqId);
42666
43810
  if (data.type === "inspect-error") {
42667
43811
  pending.reject(new Error(data.payload.message));
42668
43812
  } else {
@@ -42670,22 +43814,28 @@ class InspectWorkerClient {
42670
43814
  }
42671
43815
  };
42672
43816
  this.worker.onerror = (event) => {
42673
- var _a3, _b2;
43817
+ var _a3;
42674
43818
  const error = new Error(event.message || "Inspect worker failed unexpectedly.");
42675
- (_a3 = this.pending) == null ? void 0 : _a3.reject(error);
42676
- this.pending = null;
42677
- (_b2 = this.worker) == null ? void 0 : _b2.terminate();
43819
+ this.pending.forEach((pending) => pending.reject(error));
43820
+ this.pending.clear();
43821
+ (_a3 = this.worker) == null ? void 0 : _a3.terminate();
42678
43822
  this.worker = null;
42679
43823
  };
42680
43824
  return this.worker;
42681
43825
  }
42682
43826
  replaceActiveWorker() {
42683
- var _a3, _b2;
42684
- (_a3 = this.pending) == null ? void 0 : _a3.reject(new Error("cancelled"));
42685
- this.pending = null;
42686
- (_b2 = this.worker) == null ? void 0 : _b2.terminate();
43827
+ var _a3;
43828
+ this.pending.forEach((pending) => pending.reject(new Error("cancelled")));
43829
+ this.pending.clear();
43830
+ (_a3 = this.worker) == null ? void 0 : _a3.terminate();
42687
43831
  this.worker = null;
42688
43832
  }
43833
+ postRequest(request, transfers = []) {
43834
+ return new Promise((resolve2, reject) => {
43835
+ this.pending.set(request.payload.reqId, { resolve: resolve2, reject });
43836
+ this.getWorker().postMessage(request, transfers);
43837
+ });
43838
+ }
42689
43839
  analyze(payload) {
42690
43840
  if (this.pending) this.replaceActiveWorker();
42691
43841
  const reqId = ++this.reqId;
@@ -42705,10 +43855,18 @@ class InspectWorkerClient {
42705
43855
  object.collisionShape.mergeToVert.buffer
42706
43856
  ] : []
42707
43857
  ]);
42708
- return new Promise((resolve2, reject) => {
42709
- this.pending = { resolve: resolve2, reject };
42710
- this.getWorker().postMessage(request, transfers);
42711
- });
43858
+ return this.postRequest(request, transfers);
43859
+ }
43860
+ colorizeThickness(payload) {
43861
+ const reqId = ++this.reqId;
43862
+ const request = {
43863
+ type: "colorize-thickness",
43864
+ payload: {
43865
+ reqId,
43866
+ ...payload
43867
+ }
43868
+ };
43869
+ return this.postRequest(request);
42712
43870
  }
42713
43871
  dispose() {
42714
43872
  this.replaceActiveWorker();
@@ -43039,6 +44197,7 @@ function analyzePayloadFor(channel, objects, inspectPointSampleCount, groundZ, w
43039
44197
  }
43040
44198
  function resultToState(channel, result) {
43041
44199
  return {
44200
+ analysisId: result.analysisId,
43042
44201
  status: "ready",
43043
44202
  channel,
43044
44203
  objectColors: result.objectColors,
@@ -43061,7 +44220,8 @@ function resultToState(channel, result) {
43061
44220
  {
43062
44221
  sampleCount: object.sampleCount,
43063
44222
  positions: object.positions,
43064
- colors: object.colors
44223
+ colors: object.colors,
44224
+ values: object.values
43065
44225
  }
43066
44226
  ])
43067
44227
  ),
@@ -43069,8 +44229,40 @@ function resultToState(channel, result) {
43069
44229
  error: null
43070
44230
  };
43071
44231
  }
44232
+ function applyThicknessColorizeResult(state2, result) {
44233
+ if (state2.channel !== "thickness" || state2.status !== "ready") return state2;
44234
+ if (state2.analysisId !== result.analysisId) return state2;
44235
+ const colorsByObjectId = new Map(result.pointObjects.map((object) => [object.objectId, object.colors]));
44236
+ const pointClouds = Object.fromEntries(
44237
+ Object.entries(state2.pointClouds).map(([objectId, pointCloud]) => [
44238
+ objectId,
44239
+ {
44240
+ ...pointCloud,
44241
+ colors: colorsByObjectId.get(objectId) ?? pointCloud.colors
44242
+ }
44243
+ ])
44244
+ );
44245
+ return {
44246
+ ...state2,
44247
+ pointClouds,
44248
+ heatmapFields: Object.fromEntries(
44249
+ result.heatmapFieldObjects.map((object) => [
44250
+ object.objectId,
44251
+ {
44252
+ data: object.data,
44253
+ boundsMin: object.boundsMin,
44254
+ boundsSize: object.boundsSize,
44255
+ gridSize: object.gridSize
44256
+ }
44257
+ ])
44258
+ )
44259
+ };
44260
+ }
44261
+ function thicknessColorRangeKey(range) {
44262
+ return `${range.min}:${range.max}`;
44263
+ }
43072
44264
  function useInspectWorkerAnalysis(args) {
43073
- const [state2, setState] = reactExports.useState({
44265
+ const [rawState, setRawState] = reactExports.useState({
43074
44266
  status: "idle",
43075
44267
  channel: "none",
43076
44268
  objectColors: {},
@@ -43081,10 +44273,11 @@ function useInspectWorkerAnalysis(args) {
43081
44273
  warnings: [],
43082
44274
  error: null
43083
44275
  });
44276
+ const [colorizedState, setColorizedState] = reactExports.useState(null);
43084
44277
  reactExports.useEffect(() => {
43085
44278
  if (!WORKER_CHANNELS.has(args.inspectChannel)) {
43086
44279
  inspectWorkerClient.dispose();
43087
- setState({
44280
+ setRawState({
43088
44281
  status: "idle",
43089
44282
  channel: args.inspectChannel,
43090
44283
  objectColors: {},
@@ -43099,7 +44292,7 @@ function useInspectWorkerAnalysis(args) {
43099
44292
  }
43100
44293
  let cancelled = false;
43101
44294
  const channel = args.inspectChannel;
43102
- setState({
44295
+ setRawState({
43103
44296
  status: "loading",
43104
44297
  channel: args.inspectChannel,
43105
44298
  objectColors: {},
@@ -43133,12 +44326,13 @@ function useInspectWorkerAnalysis(args) {
43133
44326
  )
43134
44327
  );
43135
44328
  if (cancelled) return;
43136
- setState(resultToState(args.inspectChannel, result));
44329
+ setRawState(resultToState(args.inspectChannel, result));
44330
+ setColorizedState(null);
43137
44331
  } catch (error) {
43138
44332
  if (cancelled || error instanceof InspectBuildCancelledError || error instanceof Error && error.message === "cancelled") {
43139
44333
  return;
43140
44334
  }
43141
- setState({
44335
+ setRawState({
43142
44336
  status: "error",
43143
44337
  channel: args.inspectChannel,
43144
44338
  objectColors: {},
@@ -43164,7 +44358,179 @@ function useInspectWorkerAnalysis(args) {
43164
44358
  args.objectMatrices,
43165
44359
  args.groundZ
43166
44360
  ]);
43167
- return state2;
44361
+ reactExports.useEffect(() => {
44362
+ if (rawState.status !== "ready" || rawState.channel !== "thickness" || rawState.analysisId === void 0) {
44363
+ setColorizedState(null);
44364
+ return;
44365
+ }
44366
+ let cancelled = false;
44367
+ const analysisId = rawState.analysisId;
44368
+ const rangeKey22 = thicknessColorRangeKey(args.thicknessColorRange);
44369
+ void inspectWorkerClient.colorizeThickness({
44370
+ analysisId,
44371
+ colorMinThickness: args.thicknessColorRange.min,
44372
+ colorMaxThickness: args.thicknessColorRange.max
44373
+ }).then((result) => {
44374
+ if (cancelled) return;
44375
+ setColorizedState({
44376
+ analysisId,
44377
+ rangeKey: rangeKey22,
44378
+ state: applyThicknessColorizeResult(rawState, result)
44379
+ });
44380
+ }).catch((error) => {
44381
+ if (cancelled || error instanceof Error && error.message === "cancelled") return;
44382
+ setColorizedState(null);
44383
+ });
44384
+ return () => {
44385
+ cancelled = true;
44386
+ };
44387
+ }, [args.thicknessColorRange, rawState]);
44388
+ const rangeKey2 = thicknessColorRangeKey(args.thicknessColorRange);
44389
+ if (rawState.status === "ready" && rawState.channel === "thickness" && rawState.analysisId !== void 0 && (colorizedState == null ? void 0 : colorizedState.analysisId) === rawState.analysisId && colorizedState.rangeKey === rangeKey2) {
44390
+ return colorizedState.state;
44391
+ }
44392
+ return rawState;
44393
+ }
44394
+ function voxelCellKey(cell) {
44395
+ return `${cell.x},${cell.y},${cell.z}`;
44396
+ }
44397
+ function resolveVoxelIntentCellSize(cellSize) {
44398
+ return Math.max(1, Number.isFinite(cellSize) ? cellSize : 10);
44399
+ }
44400
+ function voxelCellFromPoint(point, cellSize) {
44401
+ return {
44402
+ x: Math.floor(point.x / cellSize),
44403
+ y: Math.floor(point.y / cellSize),
44404
+ z: Math.floor(point.z / cellSize)
44405
+ };
44406
+ }
44407
+ function dominantAxis(normal) {
44408
+ const ax = Math.abs(normal.x);
44409
+ const ay = Math.abs(normal.y);
44410
+ const az = Math.abs(normal.z);
44411
+ if (ax >= ay && ax >= az) return "x";
44412
+ if (ay >= ax && ay >= az) return "y";
44413
+ return "z";
44414
+ }
44415
+ function anchoredCellIndex(value, cellSize, direction) {
44416
+ const scaled = value / cellSize;
44417
+ const base = Math.floor(scaled);
44418
+ const onBoundary = Math.abs(scaled - Math.round(scaled)) < 1e-7;
44419
+ return onBoundary && direction < 0 ? base - 1 : base;
44420
+ }
44421
+ function voxelCellFromSurfaceAnchor(point, normal, cellSize, side) {
44422
+ const cell = voxelCellFromPoint(point, cellSize);
44423
+ const axis = dominantAxis(normal);
44424
+ const normalValue = normal[axis];
44425
+ const outsideDirection = normalValue < 0 ? -1 : 1;
44426
+ const direction = side === "outside" ? outsideDirection : -outsideDirection;
44427
+ cell[axis] = anchoredCellIndex(point[axis], cellSize, direction);
44428
+ return cell;
44429
+ }
44430
+ function parseVoxelCellKey(key) {
44431
+ const [x, y, z] = key.split(",").map(Number);
44432
+ return { x, y, z };
44433
+ }
44434
+ function addVoxelCells(a2, b2) {
44435
+ return { x: a2.x + b2.x, y: a2.y + b2.y, z: a2.z + b2.z };
44436
+ }
44437
+ function voxelCellCenter(cell, cellSize) {
44438
+ return [(cell.x + 0.5) * cellSize, (cell.y + 0.5) * cellSize, (cell.z + 0.5) * cellSize];
44439
+ }
44440
+ const VOXEL_INTENT_TOOL_COLORS = {
44441
+ red: "#ef4444",
44442
+ green: "#22c55e",
44443
+ blue: "#3b82f6",
44444
+ yellow: "#facc15",
44445
+ white: "#f8fafc",
44446
+ erase: "#a855f7"
44447
+ };
44448
+ const VOXEL_INTENT_TOOL_LABELS = {
44449
+ red: { label: "Red", detail: "mark a red thing to explain in chat" },
44450
+ green: { label: "Green", detail: "mark a green thing to explain in chat" },
44451
+ blue: { label: "Blue", detail: "mark a blue thing to explain in chat" },
44452
+ yellow: { label: "Yellow", detail: "mark a yellow thing to explain in chat" },
44453
+ white: { label: "White", detail: "mark a neutral thing to explain in chat" },
44454
+ erase: { label: "Eraser", detail: "delete the first marker block clicked" }
44455
+ };
44456
+ const VOXEL_INTENT_TOOL_ORDER = ["red", "green", "blue", "yellow", "white", "erase"];
44457
+ const VOXEL_INTENT_PLACEMENT_LABELS = {
44458
+ surface: { label: "Surface", detail: "place outside the clicked face" },
44459
+ inside: { label: "Inside", detail: "place just under the first clicked surface" },
44460
+ through: { label: "Through", detail: "place between the first and second surface hits" },
44461
+ grid: { label: "Grid", detail: "place from the click point in the global grid" }
44462
+ };
44463
+ const VOXEL_INTENT_PLACEMENT_ORDER = ["surface", "inside", "through", "grid"];
44464
+ function directionFromFace(event) {
44465
+ var _a3;
44466
+ const normal = (_a3 = event.face) == null ? void 0 : _a3.normal;
44467
+ if (!normal) return { x: 0, y: 0, z: 1 };
44468
+ const values = [
44469
+ { axis: "x", value: normal.x },
44470
+ { axis: "y", value: normal.y },
44471
+ { axis: "z", value: normal.z }
44472
+ ];
44473
+ values.sort((a2, b2) => Math.abs(b2.value) - Math.abs(a2.value));
44474
+ const strongest = values[0];
44475
+ return {
44476
+ x: strongest.axis === "x" ? Math.sign(strongest.value) || 1 : 0,
44477
+ y: strongest.axis === "y" ? Math.sign(strongest.value) || 1 : 0,
44478
+ z: strongest.axis === "z" ? Math.sign(strongest.value) || 1 : 0
44479
+ };
44480
+ }
44481
+ function VoxelIntentCellMesh({
44482
+ cell,
44483
+ color: color2,
44484
+ cellSize,
44485
+ opacity,
44486
+ onClick
44487
+ }) {
44488
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { position: voxelCellCenter(cell, cellSize), onClick, renderOrder: 60, userData: { voxelIntentCell: cell }, children: [
44489
+ /* @__PURE__ */ jsxRuntimeExports.jsx("boxGeometry", { args: [cellSize * 0.96, cellSize * 0.96, cellSize * 0.96] }),
44490
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshStandardMaterial", { color: color2, roughness: 0.62, metalness: 0.04, transparent: true, opacity, depthWrite: opacity > 0.55 })
44491
+ ] });
44492
+ }
44493
+ function VoxelIntentOverlay() {
44494
+ const tool = useForgeStore((s) => s.voxelIntentTool);
44495
+ const placement = useForgeStore((s) => s.voxelIntentPlacement);
44496
+ const cellSize = useForgeStore((s) => s.voxelIntentCellSize);
44497
+ const blocks = useForgeStore((s) => s.voxelIntentBlocks);
44498
+ const cellSizePreviewVisible = useForgeStore((s) => s.voxelIntentCellSizePreviewVisible);
44499
+ const setBlock = useForgeStore((s) => s.setVoxelIntentBlock);
44500
+ const eraseBlock = useForgeStore((s) => s.eraseVoxelIntentBlock);
44501
+ const resolvedCellSize = resolveVoxelIntentCellSize(cellSize);
44502
+ const blockEntries = reactExports.useMemo(() => Object.entries(blocks).map(([key, block]) => ({ cell: parseVoxelCellKey(key), block })), [blocks]);
44503
+ const occupied = (cell) => voxelCellKey(cell) in blocks;
44504
+ const placeAdjacentTo = (blockCell, event) => {
44505
+ event.stopPropagation();
44506
+ if (tool === "erase") {
44507
+ const clickedCell = event.object.userData.voxelIntentCell ?? blockCell;
44508
+ eraseBlock(clickedCell);
44509
+ return;
44510
+ }
44511
+ const faceCandidate = addVoxelCells(blockCell, directionFromFace(event));
44512
+ if (!occupied(faceCandidate)) {
44513
+ setBlock(faceCandidate, { groupId: tool, placement });
44514
+ return;
44515
+ }
44516
+ };
44517
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("group", { children: [
44518
+ cellSizePreviewVisible && /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { position: voxelCellCenter({ x: 0, y: 0, z: 0 }, resolvedCellSize), renderOrder: 55, raycast: () => null, children: [
44519
+ /* @__PURE__ */ jsxRuntimeExports.jsx("boxGeometry", { args: [resolvedCellSize, resolvedCellSize, resolvedCellSize] }),
44520
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshStandardMaterial", { color: "#22d3ee", emissive: "#0891b2", emissiveIntensity: 0.42, roughness: 0.28, metalness: 0.02 })
44521
+ ] }),
44522
+ blockEntries.map(({ cell, block }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
44523
+ VoxelIntentCellMesh,
44524
+ {
44525
+ cell,
44526
+ color: VOXEL_INTENT_TOOL_COLORS[block.groupId] ?? VOXEL_INTENT_TOOL_COLORS.white,
44527
+ cellSize: resolvedCellSize,
44528
+ opacity: 0.92,
44529
+ onClick: (event) => placeAdjacentTo(cell, event)
44530
+ },
44531
+ `block:${voxelCellKey(cell)}`
44532
+ ))
44533
+ ] });
43168
44534
  }
43169
44535
  function buildManualSceneConfig(base, manualScene, renderStylePreset) {
43170
44536
  var _a3, _b2, _c;
@@ -43480,6 +44846,15 @@ function Viewport() {
43480
44846
  const buildLedgerEvents = useForgeStore((s) => s.buildLedgerEvents);
43481
44847
  const measureSelections = useForgeStore((s) => s.measureSelections);
43482
44848
  const meshPreviewFile = useForgeStore((s) => s.meshPreviewFile);
44849
+ const voxelIntentMode = useForgeStore((s) => s.voxelIntentMode);
44850
+ const voxelIntentTool = useForgeStore((s) => s.voxelIntentTool);
44851
+ const voxelIntentPlacement = useForgeStore((s) => s.voxelIntentPlacement);
44852
+ const voxelIntentCellSizeValue = useForgeStore((s) => s.voxelIntentCellSize);
44853
+ const voxelIntentGhostOpacity = useForgeStore((s) => s.voxelIntentGhostOpacity);
44854
+ const voxelIntentBlocks = useForgeStore((s) => s.voxelIntentBlocks);
44855
+ const setVoxelIntentBlock = useForgeStore((s) => s.setVoxelIntentBlock);
44856
+ const eraseVoxelIntentBlock = useForgeStore((s) => s.eraseVoxelIntentBlock);
44857
+ const setVoxelIntentContextFile = useForgeStore((s) => s.setVoxelIntentContextFile);
43483
44858
  const isSvgActive = !!activeFile && activeFile.toLowerCase().endsWith(".svg");
43484
44859
  const state2 = useViewportState();
43485
44860
  const {
@@ -43494,6 +44869,8 @@ function Viewport() {
43494
44869
  inspectChannel,
43495
44870
  inspectDisplayMode,
43496
44871
  inspectPointSampleCount,
44872
+ thicknessColorRange,
44873
+ setThicknessColorRange,
43497
44874
  comparisonInspectMode,
43498
44875
  comparisonCandidateOpacity,
43499
44876
  projectionMode,
@@ -43566,6 +44943,7 @@ function Viewport() {
43566
44943
  previewFile,
43567
44944
  knownFileNames
43568
44945
  } = state2;
44946
+ const voxelContextFile = previewFile ?? ((activeFile == null ? void 0 : activeFile.toLowerCase().endsWith(".forge.js")) ? activeFile : null);
43569
44947
  const historyMode = useForgeStore((s) => s.historyMode);
43570
44948
  const historyObjectIds = useForgeStore((s) => s.historyObjectIds);
43571
44949
  useHistoryPlaybackLoop();
@@ -43606,7 +44984,7 @@ function Viewport() {
43606
44984
  if (activeFile && !isModelFile(activeFile) && !isSvgActive && !meshPreviewFile) {
43607
44985
  return {
43608
44986
  title: "Viewport disabled",
43609
- body: `${activeFile} is a helper file, not a .forge.js or .sketch.js model. Open a model file to render geometry.`
44987
+ body: `${activeFile} is a text file, not a .forge.js or .sketch.js model. Open a model file to render geometry.`
43610
44988
  };
43611
44989
  }
43612
44990
  if (!(result == null ? void 0 : result.error) && (result == null ? void 0 : result.assemblyKinematics) && objects.length === 0 && !rigInspectActive) {
@@ -43617,7 +44995,10 @@ function Viewport() {
43617
44995
  }
43618
44996
  return null;
43619
44997
  }, [activeFile, isSvgActive, meshPreviewFile, objects.length, result, rigInspectActive]);
43620
- const canvasDpr = isViewportInteracting || hasVisibleSdfObjects ? renderStylePreset.canvasDpr.live : [1, renderStylePreset.canvasDpr.idleMax];
44998
+ const canvasDpr = reactExports.useMemo(() => {
44999
+ if (hasVisibleSdfObjects) return renderStylePreset.canvasDpr.live;
45000
+ return [1, renderStylePreset.canvasDpr.idleMax];
45001
+ }, [hasVisibleSdfObjects, renderStylePreset.canvasDpr.idleMax, renderStylePreset.canvasDpr.live]);
43621
45002
  const effectiveSdfObjectSettings = reactExports.useMemo(() => {
43622
45003
  const next = {};
43623
45004
  for (const obj of objects) {
@@ -43632,6 +45013,7 @@ function Viewport() {
43632
45013
  const inspectAnalysis = useInspectWorkerAnalysis({
43633
45014
  inspectChannel: comparisonInspectActive ? "none" : inspectChannel,
43634
45015
  inspectPointSampleCount,
45016
+ thicknessColorRange,
43635
45017
  sceneVersion,
43636
45018
  objects,
43637
45019
  objectSettings,
@@ -43656,7 +45038,7 @@ function Viewport() {
43656
45038
  const handlers = useViewportHandlers({
43657
45039
  containerRef,
43658
45040
  contextMenuRef,
43659
- measureMode,
45041
+ measureMode: measureMode || voxelIntentMode,
43660
45042
  isViewportInteracting,
43661
45043
  objectPickSyncEnabled,
43662
45044
  hoveredObjectId,
@@ -43701,6 +45083,100 @@ function Viewport() {
43701
45083
  handlePerformanceInfoChange,
43702
45084
  handleViewPersistenceResolved: handleViewPersistenceResolvedFromHandlers
43703
45085
  } = handlers;
45086
+ reactExports.useEffect(() => {
45087
+ setVoxelIntentContextFile(voxelContextFile);
45088
+ }, [setVoxelIntentContextFile, voxelContextFile]);
45089
+ const voxelCellKey2 = (cell) => `${cell.x},${cell.y},${cell.z}`;
45090
+ const addVoxelCells2 = (a2, b2) => ({ x: a2.x + b2.x, y: a2.y + b2.y, z: a2.z + b2.z });
45091
+ const voxelNormalFromBoxHit = (point, bounds) => {
45092
+ const candidates = [
45093
+ { normal: { x: -1, y: 0, z: 0 }, distance: Math.abs(point.x - bounds.min.x) },
45094
+ { normal: { x: 1, y: 0, z: 0 }, distance: Math.abs(point.x - bounds.max.x) },
45095
+ { normal: { x: 0, y: -1, z: 0 }, distance: Math.abs(point.y - bounds.min.y) },
45096
+ { normal: { x: 0, y: 1, z: 0 }, distance: Math.abs(point.y - bounds.max.y) },
45097
+ { normal: { x: 0, y: 0, z: -1 }, distance: Math.abs(point.z - bounds.min.z) },
45098
+ { normal: { x: 0, y: 0, z: 1 }, distance: Math.abs(point.z - bounds.max.z) }
45099
+ ];
45100
+ candidates.sort((a2, b2) => a2.distance - b2.distance);
45101
+ return candidates[0].normal;
45102
+ };
45103
+ const voxelBlockHitsOnRay = (ray, cellSize) => {
45104
+ const hits = [];
45105
+ const halfExtent = cellSize * 0.55;
45106
+ const hitPoint = new Vector3();
45107
+ for (const key of Object.keys(voxelIntentBlocks)) {
45108
+ const [x, y, z] = key.split(",").map(Number);
45109
+ const center = new Vector3((x + 0.5) * cellSize, (y + 0.5) * cellSize, (z + 0.5) * cellSize);
45110
+ const bounds = new Box3(
45111
+ new Vector3(center.x - halfExtent, center.y - halfExtent, center.z - halfExtent),
45112
+ new Vector3(center.x + halfExtent, center.y + halfExtent, center.z + halfExtent)
45113
+ );
45114
+ const intersection = ray.intersectBox(bounds, hitPoint);
45115
+ if (!intersection) continue;
45116
+ const alongRay = intersection.clone().sub(ray.origin).dot(ray.direction);
45117
+ hits.push({ cell: { x, y, z }, normal: voxelNormalFromBoxHit(intersection, bounds), alongRay });
45118
+ }
45119
+ hits.sort((a2, b2) => a2.alongRay - b2.alongRay);
45120
+ return hits;
45121
+ };
45122
+ const rayExitPointForObjectClick = (event, cellSize) => {
45123
+ var _a4;
45124
+ const hitObject = event.object;
45125
+ hitObject.updateWorldMatrix(true, false);
45126
+ const raycaster = new Raycaster(event.ray.origin.clone(), event.ray.direction.clone(), 0, 1e5);
45127
+ const hits = raycaster.intersectObject(hitObject, false).sort((a2, b2) => a2.distance - b2.distance);
45128
+ const minGap = Math.max(1e-3, cellSize * 0.08);
45129
+ const distinctHits = [];
45130
+ for (const hit of hits) {
45131
+ if (hit.distance < event.distance - minGap) continue;
45132
+ const previousHit = distinctHits[distinctHits.length - 1];
45133
+ if (!previousHit || Math.abs(hit.distance - previousHit.distance) > minGap) distinctHits.push(hit);
45134
+ }
45135
+ return ((_a4 = distinctHits[1]) == null ? void 0 : _a4.point) ?? null;
45136
+ };
45137
+ const handleVoxelObjectSurfaceClick = (event, matrix) => {
45138
+ var _a4;
45139
+ event.stopPropagation();
45140
+ if ((event.delta ?? 0) > 4) return;
45141
+ const cellSize = resolveVoxelIntentCellSize(voxelIntentCellSizeValue);
45142
+ const markerHits = voxelBlockHitsOnRay(event.ray, cellSize);
45143
+ if (voxelIntentTool === "erase") {
45144
+ const markerHit2 = markerHits[0];
45145
+ if (markerHit2) eraseVoxelIntentBlock(markerHit2.cell);
45146
+ return;
45147
+ }
45148
+ const markerHit = markerHits[0];
45149
+ if (markerHit) {
45150
+ const candidate = addVoxelCells2(markerHit.cell, markerHit.normal);
45151
+ if (!(voxelCellKey2(candidate) in voxelIntentBlocks)) {
45152
+ setVoxelIntentBlock(candidate, { groupId: voxelIntentTool, placement: voxelIntentPlacement });
45153
+ }
45154
+ return;
45155
+ }
45156
+ const worldNormal = ((_a4 = event.face) == null ? void 0 : _a4.normal) ? event.face.normal.clone().applyNormalMatrix(new Matrix3().getNormalMatrix(matrix)).normalize() : new Vector3(0, 0, 1);
45157
+ if (worldNormal.lengthSq() === 0) worldNormal.set(0, 0, 1);
45158
+ if (voxelIntentPlacement === "through") {
45159
+ const exitPoint = rayExitPointForObjectClick(event, cellSize);
45160
+ const cutPoint = exitPoint ? event.point.clone().lerp(exitPoint, 0.5) : event.point.clone().sub(worldNormal.clone().multiplyScalar(cellSize * 0.5));
45161
+ setVoxelIntentBlock(voxelCellFromPoint(cutPoint, cellSize), { groupId: voxelIntentTool, placement: voxelIntentPlacement });
45162
+ return;
45163
+ }
45164
+ if (voxelIntentPlacement === "inside") {
45165
+ setVoxelIntentBlock(voxelCellFromSurfaceAnchor(event.point, worldNormal, cellSize, "inside"), {
45166
+ groupId: voxelIntentTool,
45167
+ placement: voxelIntentPlacement
45168
+ });
45169
+ return;
45170
+ }
45171
+ if (voxelIntentPlacement === "grid") {
45172
+ setVoxelIntentBlock(voxelCellFromPoint(event.point, cellSize), { groupId: voxelIntentTool, placement: voxelIntentPlacement });
45173
+ return;
45174
+ }
45175
+ setVoxelIntentBlock(voxelCellFromSurfaceAnchor(event.point, worldNormal, cellSize, "outside"), {
45176
+ groupId: voxelIntentTool,
45177
+ placement: voxelIntentPlacement
45178
+ });
45179
+ };
43704
45180
  const overlayEntries = [];
43705
45181
  if (showPerformanceInfo) {
43706
45182
  overlayEntries.push({
@@ -43755,6 +45231,8 @@ function Viewport() {
43755
45231
  channel: inspectChannel,
43756
45232
  displayMode: inspectDisplayMode,
43757
45233
  warnings: inspectWarnings,
45234
+ thicknessColorRange,
45235
+ onThicknessColorRangeChange: setThicknessColorRange,
43758
45236
  details: comparisonInspectActive ? /* @__PURE__ */ jsxRuntimeExports.jsx(ComparisonLegendDetails, { metrics: comparisonAnalysis.metrics, mode: comparisonInspectMode }) : void 0
43759
45237
  }
43760
45238
  )
@@ -43834,7 +45312,7 @@ function Viewport() {
43834
45312
  {
43835
45313
  style: {
43836
45314
  background: renderStyle === "classic" ? t2.viewportBg : renderStylePreset.background,
43837
- cursor: measureMode ? "crosshair" : "default"
45315
+ cursor: measureMode || voxelIntentMode ? "crosshair" : "default"
43838
45316
  },
43839
45317
  dpr: canvasDpr,
43840
45318
  gl: {
@@ -43921,14 +45399,16 @@ function Viewport() {
43921
45399
  const settings = objectSettings[obj.id] ?? { visible: true, opacity: 1, color: "#5b9bd5" };
43922
45400
  const isDimmedByFocus = focusedObjectIdSet.size > 0 && !focusedObjectIdSet.has(obj.id);
43923
45401
  const isDimmedByGhost = constructionGhost !== null && obj.id !== constructionGhost.objectId;
45402
+ const isDimmedByVoxel = voxelIntentMode && !!obj.shape;
43924
45403
  const isHiddenByHistory = historyMode && historyObjectIds.includes(obj.id);
43925
45404
  if (isHiddenByHistory) return null;
43926
45405
  const effectiveSettings = isDimmedByFocus || isDimmedByGhost ? { ...settings, opacity: Math.min(settings.opacity, FOCUS_MODE_DIM_OPACITY) } : settings;
45406
+ const voxelSettings = isDimmedByVoxel ? { ...effectiveSettings, opacity: Math.min(effectiveSettings.opacity, voxelIntentGhostOpacity) } : effectiveSettings;
43927
45407
  const comparisonSettings = comparisonInspectActive && obj.shape ? {
43928
- ...effectiveSettings,
43929
- opacity: Math.min(effectiveSettings.opacity, comparisonCandidateContextOpacity(comparisonCandidateOpacity)),
45408
+ ...voxelSettings,
45409
+ opacity: Math.min(voxelSettings.opacity, comparisonCandidateContextOpacity(comparisonCandidateOpacity)),
43930
45410
  color: "#b9c3cc"
43931
- } : effectiveSettings;
45411
+ } : voxelSettings;
43932
45412
  const isHovered = hoveredObjectId === obj.id;
43933
45413
  const matrix = objectMatrices[obj.id] ?? new Matrix4();
43934
45414
  const objectClippingPlanes = objectClippingPlanesById[obj.id] ?? EMPTY_CLIPPING_PLANES;
@@ -43959,7 +45439,7 @@ function Viewport() {
43959
45439
  onPointerEnter: (event) => updateHoverLabel(obj, event),
43960
45440
  onPointerMove: (event) => updateHoverLabel(obj, event),
43961
45441
  onPointerLeave: (event) => clearHoverLabel(obj, event),
43962
- onClick: (event) => handleObjectClick(obj, event),
45442
+ onClick: (event) => voxelIntentMode ? handleVoxelObjectSurfaceClick(event, matrix) : handleObjectClick(obj, event),
43963
45443
  onDoubleClick: (event) => handleObjectDoubleClick(obj, event),
43964
45444
  onContextMenu: (event) => handleObjectContextMenu(obj, event)
43965
45445
  },
@@ -44032,6 +45512,7 @@ function Viewport() {
44032
45512
  }
44033
45513
  return null;
44034
45514
  }),
45515
+ voxelIntentMode && /* @__PURE__ */ jsxRuntimeExports.jsx(VoxelIntentOverlay, {}),
44035
45516
  inspectChannel === "collisions" && /* @__PURE__ */ jsxRuntimeExports.jsx(CollisionInspectionOverlay, { geometries: inspectAnalysis.collisionGeometries }),
44036
45517
  rigInspectActive && /* @__PURE__ */ jsxRuntimeExports.jsx(RigInspectionOverlay, { state: rigInspectionOverlay, config: jointOverlayConfig }),
44037
45518
  comparisonInspectActive && comparisonAnalysis.referenceResult && /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -44086,7 +45567,7 @@ function Viewport() {
44086
45567
  }
44087
45568
  ),
44088
45569
  /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomSampler, { onZoomChange: setZoomMmPerPx }),
44089
- gridEnabled && !isSketchOnly && /* @__PURE__ */ jsxRuntimeExports.jsx(
45570
+ gridEnabled && !isSketchOnly && !voxelIntentMode && /* @__PURE__ */ jsxRuntimeExports.jsx(
44090
45571
  Grid,
44091
45572
  {
44092
45573
  args: [500, 500],
@@ -44111,7 +45592,7 @@ function Viewport() {
44111
45592
  onPointerEnter: (obj, event) => updateHoverLabel(obj, event),
44112
45593
  onPointerMove: (obj, event) => updateHoverLabel(obj, event),
44113
45594
  onPointerLeave: (obj, event) => clearHoverLabel(obj, event),
44114
- onClick: (obj, event) => handleObjectClick(obj, event),
45595
+ onClick: (obj, event) => voxelIntentMode ? handleVoxelObjectSurfaceClick(event, objectMatrices[obj.id] ?? new Matrix4()) : handleObjectClick(obj, event),
44115
45596
  onDoubleClick: (obj, event) => handleObjectDoubleClick(obj, event),
44116
45597
  onContextMenu: (obj, event) => handleObjectContextMenu(obj, event)
44117
45598
  }
@@ -44146,7 +45627,7 @@ function Viewport() {
44146
45627
  minPolarAngle: 0,
44147
45628
  maxPolarAngle: Math.PI,
44148
45629
  enableRotate: !isSketchOnly,
44149
- mouseButtons: drawModeActive ? MOUSE_BUTTONS_DRAW : isSketchOnly ? MOUSE_BUTTONS_SKETCH : MOUSE_BUTTONS_3D,
45630
+ mouseButtons: voxelIntentMode ? MOUSE_BUTTONS_VOXEL : drawModeActive ? MOUSE_BUTTONS_DRAW : isSketchOnly ? MOUSE_BUTTONS_SKETCH : MOUSE_BUTTONS_3D,
44150
45631
  touches: isSketchOnly ? TOUCH_GESTURES_SKETCH : TOUCH_GESTURES_3D
44151
45632
  }
44152
45633
  ),
@@ -44154,6 +45635,7 @@ function Viewport() {
44154
45635
  /* @__PURE__ */ jsxRuntimeExports.jsx(ViewManager, { isSketchOnly, controlsRef }),
44155
45636
  /* @__PURE__ */ jsxRuntimeExports.jsx(ViewPersistence, { controlsRef, isSketchOnly, onResolved: handleViewPersistenceResolved }),
44156
45637
  /* @__PURE__ */ jsxRuntimeExports.jsx(OrbitExporterBridge, { controlsRef }),
45638
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ViewportImageCaptureBridge, {}),
44157
45639
  /* @__PURE__ */ jsxRuntimeExports.jsx(ViewportRecordingBridge, { controlsRef }),
44158
45640
  historyMode && /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryRecorderBridge, {}),
44159
45641
  /* @__PURE__ */ jsxRuntimeExports.jsx(TrajectoryRecorderBridge, { controlsRef })
@@ -44255,7 +45737,7 @@ function Viewport() {
44255
45737
  /* @__PURE__ */ jsxRuntimeExports.jsx(ViewportOverlayHost, { entries: overlayEntries }),
44256
45738
  viewportDisabledMessage && /* @__PURE__ */ jsxRuntimeExports.jsx(ViewportDisabledOverlay, { title: viewportDisabledMessage.title, body: viewportDisabledMessage.body }),
44257
45739
  drawFlagEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx(DrawToolbar, {}),
44258
- /* @__PURE__ */ jsxRuntimeExports.jsx(HoverTooltipLayer, { ref: hoverTooltipRef, enabled: objectPickSyncEnabled && !measureMode }),
45740
+ /* @__PURE__ */ jsxRuntimeExports.jsx(HoverTooltipLayer, { ref: hoverTooltipRef, enabled: objectPickSyncEnabled && !measureMode && !voxelIntentMode }),
44259
45741
  viewportPortalHost && objectContextMenu && reactDomExports.createPortal(
44260
45742
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
44261
45743
  "div",
@@ -44729,7 +46211,7 @@ function Viewport() {
44729
46211
  }
44730
46212
  );
44731
46213
  }
44732
- const EditorApp$1 = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-Beb-IZ0y.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
46214
+ const EditorApp$1 = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-QXsAISLR.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
44733
46215
  const PENDING_SHARE_COPY_KEY = "fc-pending-share-copy";
44734
46216
  function storePendingShareCopy(shareId) {
44735
46217
  sessionStorage.setItem(PENDING_SHARE_COPY_KEY, shareId);
@@ -44995,17 +46477,17 @@ function SeoMetadata() {
44995
46477
  }, [location.pathname]);
44996
46478
  return null;
44997
46479
  }
44998
- reactExports.lazy(() => __vitePreload(() => import("./LandingPageProofDriven-Cr6fXMDj.js"), true ? __vite__mapDeps([1]) : void 0).then((m2) => ({ default: m2.LandingPageProofDriven })));
44999
- const DocsPage = reactExports.lazy(() => __vitePreload(() => import("./DocsPage-B954L3YN.js"), true ? [] : void 0).then((m2) => ({ default: m2.DocsPage })));
45000
- reactExports.lazy(() => __vitePreload(() => import("./BlogPage-kF0fkdJT.js"), true ? [] : void 0).then((m2) => ({ default: m2.BlogPage })));
45001
- reactExports.lazy(() => __vitePreload(() => import("./BenchmarkPage-DfPMY_-d.js"), true ? __vite__mapDeps([1,2]) : void 0).then((m2) => ({ default: m2.BenchmarkPage })));
45002
- reactExports.lazy(() => __vitePreload(() => import("./AdminPage-CDyGUinA.js"), true ? [] : void 0).then((m2) => ({ default: m2.AdminPage })));
46480
+ reactExports.lazy(() => __vitePreload(() => import("./LandingPageProofDriven-yhhOodbf.js"), true ? __vite__mapDeps([1]) : void 0).then((m2) => ({ default: m2.LandingPageProofDriven })));
46481
+ const DocsPage = reactExports.lazy(() => __vitePreload(() => import("./DocsPage-B5LePEuj.js"), true ? [] : void 0).then((m2) => ({ default: m2.DocsPage })));
46482
+ reactExports.lazy(() => __vitePreload(() => import("./BlogPage-DodHpvmf.js"), true ? [] : void 0).then((m2) => ({ default: m2.BlogPage })));
46483
+ reactExports.lazy(() => __vitePreload(() => import("./BenchmarkPage-a9_f-1US.js"), true ? __vite__mapDeps([1,2]) : void 0).then((m2) => ({ default: m2.BenchmarkPage })));
46484
+ reactExports.lazy(() => __vitePreload(() => import("./AdminPage-DwYHz72L.js"), true ? [] : void 0).then((m2) => ({ default: m2.AdminPage })));
45003
46485
  reactExports.lazy(() => __vitePreload(() => Promise.resolve().then(() => PublishedModelPage$1), true ? void 0 : void 0).then((m2) => ({ default: m2.PublishedModelPage })));
45004
- reactExports.lazy(() => __vitePreload(() => import("./SettingsPage-Bz0of4KQ.js"), true ? [] : void 0).then((m2) => ({ default: m2.SettingsPage })));
45005
- reactExports.lazy(() => __vitePreload(() => import("./PricingPage-zWXkvlwl.js"), true ? __vite__mapDeps([1,3]) : void 0).then((m2) => ({ default: m2.PricingPage })));
45006
- reactExports.lazy(() => __vitePreload(() => import("./LegalPage-Dzklqmmg.js"), true ? __vite__mapDeps([1,4]) : void 0).then((m2) => ({ default: m2.LegalPage })));
45007
- const EditorApp = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-Beb-IZ0y.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
45008
- const EmbedViewer = reactExports.lazy(() => __vitePreload(() => import("./EmbedViewer-C77B-TrF.js"), true ? [] : void 0).then((m2) => ({ default: m2.EmbedViewer })));
46486
+ reactExports.lazy(() => __vitePreload(() => import("./SettingsPage-BJZcM97j.js"), true ? [] : void 0).then((m2) => ({ default: m2.SettingsPage })));
46487
+ reactExports.lazy(() => __vitePreload(() => import("./PricingPage-E3Rma7aV.js"), true ? __vite__mapDeps([1,3]) : void 0).then((m2) => ({ default: m2.PricingPage })));
46488
+ reactExports.lazy(() => __vitePreload(() => import("./LegalPage-5RbKRGYK.js"), true ? __vite__mapDeps([1,4]) : void 0).then((m2) => ({ default: m2.LegalPage })));
46489
+ const EditorApp = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-QXsAISLR.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
46490
+ const EmbedViewer = reactExports.lazy(() => __vitePreload(() => import("./EmbedViewer-DdEHGUMU.js"), true ? [] : void 0).then((m2) => ({ default: m2.EmbedViewer })));
45009
46491
  const embedMode = isEmbedMode() && !window.location.pathname.startsWith("/m/");
45010
46492
  const EDITABLE_CRASH_FILE = /\.(?:forge\.js|[cm]?[jt]sx?|json|md|txt|svg|dxf)$/i;
45011
46493
  function firstMeaningfulLine(text) {
@@ -45225,61 +46707,73 @@ function App() {
45225
46707
  applyTheme(localStorage.getItem("fc-theme") || "dark");
45226
46708
  clientExports.createRoot(document.getElementById("root")).render(/* @__PURE__ */ jsxRuntimeExports.jsx(App, {}));
45227
46709
  export {
45228
- ViewController as $,
46710
+ INSPECT_POINT_SAMPLE_COUNT_MIN as $,
45229
46711
  AuthApiError as A,
45230
46712
  BrandMark as B,
45231
- hasExternalFiles as C,
45232
- isImportableProjectExactFile as D,
45233
- resolvePreviewFile as E,
46713
+ captureViewportImageBlobFromStore as C,
46714
+ exportExactFromStore as D,
46715
+ storageQuotaUpgradeMessage as E,
45234
46716
  FLAG_DEFINITIONS as F,
45235
- countParamSnapshotDiff as G,
45236
- buildProjectShareUrl as H,
45237
- buildEmbedSnippet as I,
45238
- publishProjectShare as J,
45239
- unpublishProjectShare as K,
45240
- formatComputeBackendLabel as L,
45241
- themes as M,
45242
- computeBackendFromParts as N,
45243
- formatArea as O,
45244
- sliderToAnimationSpeed as P,
45245
- animationSpeedToSlider as Q,
45246
- formatAnimationSpeed as R,
45247
- resolveJointRange as S,
45248
- useJointsConfig as T,
45249
- useJointAnimationValues as U,
45250
- INSPECT_POINT_SAMPLE_COUNT_MAX as V,
45251
- INSPECT_POINT_SAMPLE_COUNT_MIN as W,
45252
- expandBoundsByTransformedAabb as X,
45253
- Canvas as Y,
45254
- PerspectiveCamera as Z,
45255
- ControlsInteractionBridge as _,
46717
+ isImportableProjectMeshFile as G,
46718
+ isImportableProjectBinaryFile as H,
46719
+ hasExternalFiles as I,
46720
+ isImportableProjectExactFile as J,
46721
+ resolvePreviewFile as K,
46722
+ countParamSnapshotDiff as L,
46723
+ buildProjectShareUrl as M,
46724
+ buildEmbedSnippet as N,
46725
+ publishProjectShare as O,
46726
+ unpublishProjectShare as P,
46727
+ formatComputeBackendLabel as Q,
46728
+ themes as R,
46729
+ computeBackendFromParts as S,
46730
+ formatArea as T,
46731
+ sliderToAnimationSpeed as U,
46732
+ animationSpeedToSlider as V,
46733
+ formatAnimationSpeed as W,
46734
+ resolveJointRange as X,
46735
+ useJointsConfig as Y,
46736
+ useJointAnimationValues as Z,
46737
+ INSPECT_POINT_SAMPLE_COUNT_MAX as _,
45256
46738
  applyTheme as a,
45257
- SceneConfigurator as a0,
45258
- LocalEnvironment as a1,
45259
- ForgeObject as a2,
45260
- RenderLabelsOverlay as a3,
45261
- Grid as a4,
45262
- OrbitControls2 as a5,
45263
- TOUCH_GESTURES_3D as a6,
45264
- MOUSE_BUTTONS_3D as a7,
45265
- ModelJourneyBar as a8,
45266
- useJointAnimationLoop as a9,
45267
- computeJointNodeMatrices as aa,
45268
- computeObjectJointMatrices as ab,
45269
- readLastActiveFileForUser as ac,
45270
- ToastContainer as ad,
45271
- isMobile as ae,
45272
- useFeatureFlag as af,
45273
- decodeSharedHash as ag,
45274
- decodeSharedBundle as ah,
45275
- getExternalUrl as ai,
45276
- getGistId as aj,
45277
- Viewport as ak,
45278
- shouldBlockBrowserShortcut as al,
45279
- useDrawStore as am,
45280
- storePendingShareCopy as an,
45281
- buildShareUrl as ao,
45282
- share as ap,
46739
+ VOXEL_INTENT_TOOL_ORDER as a0,
46740
+ VOXEL_INTENT_TOOL_LABELS as a1,
46741
+ VOXEL_INTENT_TOOL_COLORS as a2,
46742
+ VOXEL_INTENT_PLACEMENT_ORDER as a3,
46743
+ VOXEL_INTENT_PLACEMENT_LABELS as a4,
46744
+ highlightLanguageForProjectFile as a5,
46745
+ hasProjectTextFileExtension as a6,
46746
+ expandBoundsByTransformedAabb as a7,
46747
+ Canvas as a8,
46748
+ PerspectiveCamera as a9,
46749
+ buildShareUrl as aA,
46750
+ share as aB,
46751
+ ControlsInteractionBridge as aa,
46752
+ ViewController as ab,
46753
+ SceneConfigurator as ac,
46754
+ LocalEnvironment as ad,
46755
+ ForgeObject as ae,
46756
+ RenderLabelsOverlay as af,
46757
+ Grid as ag,
46758
+ OrbitControls2 as ah,
46759
+ TOUCH_GESTURES_3D as ai,
46760
+ MOUSE_BUTTONS_3D as aj,
46761
+ ModelJourneyBar as ak,
46762
+ useJointAnimationLoop as al,
46763
+ computeJointNodeMatrices as am,
46764
+ computeObjectJointMatrices as an,
46765
+ readLastActiveFileForUser as ao,
46766
+ ToastContainer as ap,
46767
+ isMobile as aq,
46768
+ useFeatureFlag as ar,
46769
+ decodeSharedHash as as,
46770
+ decodeSharedBundle as at,
46771
+ getExternalUrl as au,
46772
+ getGistId as av,
46773
+ Viewport as aw,
46774
+ shouldBlockBrowserShortcut as ax,
46775
+ useDrawStore as ay,
46776
+ storePendingShareCopy as az,
45283
46777
  authFetch as b,
45284
46778
  authApi as c,
45285
46779
  showToast as d,
@@ -45287,22 +46781,22 @@ export {
45287
46781
  formatStorageBytes as f,
45288
46782
  useForgeStore as g,
45289
46783
  fileSystem as h,
45290
- copyTextToClipboard as i,
45291
- useFeatureFlagStore as j,
45292
- fetchGistModel as k,
45293
- fetchUrlModel as l,
45294
- exportMeshFromStore as m,
45295
- exportReportFromStore as n,
45296
- exportOrbitVideoFromStore as o,
45297
- exportSketchFromStore as p,
45298
- buildGistShareUrl as q,
46784
+ isRunnableFile as i,
46785
+ copyTextToClipboard as j,
46786
+ useFeatureFlagStore as k,
46787
+ fetchGistModel as l,
46788
+ monacoLanguageForProjectFile as m,
46789
+ fetchUrlModel as n,
46790
+ exportMeshFromStore as o,
46791
+ exportReportFromStore as p,
46792
+ exportViewportImageFromStore as q,
45299
46793
  readProjectFilesFromDataTransfer as r,
45300
46794
  storageUsagePercent as s,
45301
46795
  triggerDownload as t,
45302
46796
  useAuthStore as u,
45303
- exportExactFromStore as v,
45304
- deriveExportStem as w,
45305
- storageQuotaUpgradeMessage as x,
45306
- isImportableProjectMeshFile as y,
45307
- isImportableProjectBinaryFile as z
46797
+ exportOrbitVideoFromStore as v,
46798
+ exportSketchFromStore as w,
46799
+ buildGistShareUrl as x,
46800
+ deriveExportStem as y,
46801
+ sanitizeExportStem as z
45308
46802
  };