forgecad 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/assets/{AdminPage-B3L3W1Uo.js → AdminPage-B1nIvqLS.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-YZJbw5nd.js} +2 -2
  3. package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-DIWRApKS.js} +1 -1
  4. package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-ClL6X1hR.js} +8 -22
  5. package/dist/assets/EditorApp-CYBDvSyT.js +17067 -0
  6. package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-Dmfu_LIw.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-XYTiYxfM.js} +2 -2
  8. package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-D5Z3CscF.js} +2 -2
  9. package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-BP4lIGio.js} +2 -2
  10. package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-D3bcPBsC.js} +1 -1
  11. package/dist/assets/{app-BsRYSfxY.js → app-BKjogwIZ.js} +3288 -512
  12. package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-6a9-ilom.js} +80498 -74979
  13. package/dist/assets/cli/{render-XXol_ET7.js → render-CMNudGb0.js} +1264 -113
  14. package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-BuZgc606.js} +8369 -6839
  15. package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-DQ82ueGu.js} +45438 -39996
  16. package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
  17. package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
  18. package/dist/assets/{inspectWorker-ymhBV4Ll.js → inspectWorker-Cuby2qfT.js} +4899 -1303
  19. package/dist/assets/{jointPose-B0blBj9A.js → jointPose-CFql5I-u.js} +1 -1
  20. package/dist/assets/{landing-proof-driven-Cpf-MIbI.css → landing-proof-driven-_u4v_xQb.css} +2 -2
  21. package/dist/assets/{manifold-CYlIm-M6.js → manifold-02pmr7O7.js} +2 -2
  22. package/dist/assets/{manifold-B_7QXpGB.js → manifold-C6KU0oII.js} +1 -1
  23. package/dist/assets/{manifold-CNShmpEJ.js → manifold-P1yF3GKn.js} +1 -1
  24. package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-kg065BVL.js} +76583 -65731
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/AI/usage.md +6 -8
  28. package/dist/docs-raw/CLI.md +14 -12
  29. package/dist/docs-raw/component-model.md +28 -9
  30. package/dist/docs-raw/generated/assembly.md +76 -3
  31. package/dist/docs-raw/generated/concepts.md +43 -7
  32. package/dist/docs-raw/generated/core.md +399 -73
  33. package/dist/docs-raw/generated/curves.md +357 -6
  34. package/dist/docs-raw/generated/runtime-names.md +12 -12
  35. package/dist/docs-raw/generated/sketch.md +16 -3
  36. package/dist/docs-raw/guides/inspection-bundles.md +5 -3
  37. package/dist/docs-raw/guides/structural-fea.md +235 -0
  38. package/dist/docs-raw/skills/forgecad-build-model.md +70 -147
  39. package/dist/docs-raw/skills/forgecad-image-prompt.md +1 -1
  40. package/dist/docs-raw/skills/forgecad-project-sync.md +3 -3
  41. package/dist/docs-raw/skills/forgecad-reconstruct-cad-file.md +2 -2
  42. package/dist/docs-raw/skills/forgecad-reconstruct-from-images.md +4 -5
  43. package/dist/docs-raw/skills/forgecad.md +4 -1
  44. package/dist/docs-raw/skills/index.md +1 -5
  45. package/dist/docs-raw/welcome.md +3 -4
  46. package/dist/index.html +1 -1
  47. package/dist/llms.txt +1 -2
  48. package/dist/sitemap.xml +15 -15
  49. package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-UJWUEIDC.js} +1 -1
  50. package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-O2EPDJSY.js} +1 -1
  51. package/dist-cli/{chunk-UHBRMYA6.js → chunk-MNDROM7T.js} +78926 -73392
  52. package/dist-cli/forgecad.js +6306 -1061
  53. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  54. package/dist-skill/CONTEXT.md +1257 -110
  55. package/dist-skill/SKILL.md +4 -1
  56. package/dist-skill/docs/API/core/concepts.md +31 -4
  57. package/dist-skill/docs/CLI.md +14 -12
  58. package/dist-skill/docs/generated/assembly.md +73 -3
  59. package/dist-skill/docs/generated/core.md +395 -74
  60. package/dist-skill/docs/generated/curves.md +356 -6
  61. package/dist-skill/docs/generated/runtime-names.md +12 -12
  62. package/dist-skill/docs/generated/sketch.md +16 -3
  63. package/dist-skill/docs/guides/inspection-bundles.md +5 -3
  64. package/dist-skill/docs/guides/manual-parameters.md +130 -0
  65. package/dist-skill/docs/guides/structural-fea.md +235 -0
  66. package/dist-skill/library/README.md +0 -4
  67. package/dist-skill/library/forgecad-build-model/SKILL.md +57 -150
  68. package/dist-skill/library/forgecad-build-model/references/inspection-feedback.md +58 -0
  69. package/dist-skill/library/forgecad-build-model/references/module-contracts.md +53 -0
  70. package/dist-skill/library/forgecad-build-model/references/parameter-controls.md +22 -0
  71. package/dist-skill/library/forgecad-build-model/references/readiness-review.md +43 -0
  72. package/dist-skill/library/forgecad-build-model/references/simulation-feedback.md +49 -0
  73. package/dist-skill/library/forgecad-build-model/references/stage-1-design-intent.md +21 -0
  74. package/dist-skill/library/forgecad-build-model/references/stage-2-architecture-plan.md +23 -0
  75. package/dist-skill/library/forgecad-build-model/references/stage-3-build-slices.md +39 -0
  76. package/dist-skill/library/forgecad-build-model/references/stage-4-feedback-iteration.md +24 -0
  77. package/dist-skill/library/forgecad-build-model/references/stage-5-readiness-package.md +34 -0
  78. package/dist-skill/library/forgecad-image-prompt/SKILL.md +1 -1
  79. package/dist-skill/library/forgecad-project-sync/SKILL.md +3 -3
  80. package/dist-skill/library/forgecad-reconstruct-cad-file/SKILL.md +2 -2
  81. package/dist-skill/library/forgecad-reconstruct-from-images/SKILL.md +4 -5
  82. package/dist-skill/website/skills/forgecad-build-model.md +70 -147
  83. package/dist-skill/website/skills/forgecad-image-prompt.md +1 -1
  84. package/dist-skill/website/skills/forgecad-project-sync.md +3 -3
  85. package/dist-skill/website/skills/forgecad-reconstruct-cad-file.md +2 -2
  86. package/dist-skill/website/skills/forgecad-reconstruct-from-images.md +4 -5
  87. package/dist-skill/website/skills/forgecad.md +4 -1
  88. package/dist-skill/website/skills/index.md +1 -5
  89. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  90. package/examples/api/blend-full-round.forge.js +37 -0
  91. package/examples/api/blend-variable-radius.forge.js +51 -0
  92. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  93. package/examples/api/extrude-up-to-face.forge.js +47 -0
  94. package/examples/api/param-path2d.forge.js +65 -0
  95. package/examples/api/param-placement2d.forge.js +80 -0
  96. package/examples/api/param-spline2d-g-continuity.forge.js +57 -0
  97. package/examples/api/spoon-full-tang-handle.forge.js +188 -0
  98. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  99. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  100. package/examples/api/surface-variable-thickness-panel.forge.js +62 -0
  101. package/examples/mechanical/airplane-propeller.forge.js +81 -28
  102. package/package.json +5 -2
  103. package/dist/assets/EditorApp-BWUGCdD5.js +0 -16610
  104. package/dist/docs-raw/skills/forgecad-design-spec.md +0 -145
  105. package/dist/docs-raw/skills/forgecad-grade-model.md +0 -84
  106. package/dist/docs-raw/skills/forgecad-inspect-model.md +0 -80
  107. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +0 -78
  108. package/dist-skill/library/forgecad-design-spec/SKILL.md +0 -132
  109. package/dist-skill/library/forgecad-design-spec/references/default-profiles.md +0 -99
  110. package/dist-skill/library/forgecad-design-spec/references/master-prompt.md +0 -73
  111. package/dist-skill/library/forgecad-grade-model/SKILL.md +0 -72
  112. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-inspect-model/SKILL.md +0 -68
  114. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +0 -66
  115. package/dist-skill/website/skills/forgecad-design-spec.md +0 -145
  116. package/dist-skill/website/skills/forgecad-grade-model.md +0 -84
  117. package/dist-skill/website/skills/forgecad-inspect-model.md +0 -80
  118. package/dist-skill/website/skills/forgecad-verify-mujoco.md +0 -78
  119. /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +0 -0
  120. /package/dist-skill/library/{forgecad-verify-mujoco → forgecad-build-model}/scripts/mujoco_verify.py +0 -0
  121. /package/dist-skill/library/{forgecad-inspect-model → forgecad-build-model/scripts}/summarize_manifest.py +0 -0
@@ -1,10 +1,10 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/EditorApp-C5f24ZN9.css","assets/landing-proof-driven-Cpf-MIbI.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-C5f24ZN9.css","assets/landing-proof-driven-_u4v_xQb.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, S as SDF_PROGRAM_OP, z as zipSync, s as strToU8, W as WebGLRenderer, R as Raycaster, O as OrthographicCamera$1, P as PerspectiveCamera$1, a as Scene, b as PCFSoftShadowMap, V as VSMShadowMap, c as PCFShadowMap, B as BasicShadowMap, C as ColorManagement, L as LinearSRGBColorSpace, d as SRGBColorSpace, N as NoToneMapping, A as ACESFilmicToneMapping, e as Layers, f as Color, g as RGBAFormat, U as UnsignedByteType, h as Vector3, i as Vector2, j as Clock, T as THREE, D as DoubleSide, k as REVISION, M as Mesh, I as IcosahedronGeometry, l as ShaderMaterial, m as Spherical, Q as Quaternion, n as MOUSE, o as TOUCH, p as Ray, q as Plane, r as DataTextureLoader, H as HalfFloatType, F as FloatType, t as DataUtils, u as LinearFilter, v as RedFormat, w as InstancedBufferGeometry, x as Float32BufferAttribute, y as InstancedInterleavedBuffer, E as InterleavedBufferAttribute, G as WireframeGeometry, J as Box3, K as Sphere, X as UniformsUtils, Y as UniformsLib, Z as Vector4, $ as Line3, a0 as Matrix4, a1 as MathUtils, a2 as Uniform, a3 as WebGLRenderTarget, a4 as DepthTexture, a5 as BackSide, a6 as ClampToEdgeWrapping, a7 as PlaneGeometry, a8 as UVMapping, a9 as DataTexture, aa as Texture, ab as MeshBasicMaterial, ac as IntType, ad as ShortType, ae as ByteType, af as UnsignedIntType, ag as Loader, ah as LoadingManager, ai as LinearMipMapLinearFilter, aj as FileLoader, ak as NoBlending, al as CubeReflectionMapping, am as EquirectangularReflectionMapping, an as CubeTextureLoader, ao as WebGLCubeRenderTarget, ap as ConstraintSketch, aq as setSketchPlacement3D, ar as Sketch, as as PROFILE_BACKEND_MARKER, at as FrozenShape, au as setShapeCompilePlan, av as hasAnyPorts, aw as setShapePortsInternal, ax as markShapePortsUsed, ay as writeViewPreferences, az as setParamOverrides, aA as readViewPreferences, aB as resolveForgeRenderStyle, aC as isConstraintSketch, aD as updateConstraintValue, aE as linearizeMultiObjectSteps, aF as getShapeCompilePlan, aG as resolveCameraControlMode, aH as resolveComparisonOpacity, aI as resolveComparisonInspectMode, aJ as resolveBooleanPref, aK as resolveInspectQuantizeBands, aL as resolveInspectIsolineSpacing, aM as resolveInspectColormap, aN as resolveThicknessColorRange, aO as resolveInspectPointSampleCount, aP as SCAN_PROXY_GRANULARITY_DEFAULT, aQ as DEFAULT_MANUAL_SCENE_SETTINGS, aR as resolveManualSceneSettings, aS as publishSolverWasmRunDebug, aT as resolveForgeQualityPreset, aU as DEFAULT_INSPECT_ISOLINES_ENABLED, aV as SCAN_PROXY_MATRIX_GRANULARITY_MAX, aW as findJointAnimationClip, aX as resolveJointAnimation, aY as resolveJointViewValues, aZ as resolveImportPath, a_ as DEFAULT_INSPECT_CRITICAL_LINE_ENABLED, a$ as resolveScanProxyGranularity, b0 as DEFAULT_COMPARISON_REFERENCE_OPACITY, b1 as DEFAULT_COMPARISON_CANDIDATE_OPACITY, b2 as BufferGeometry, b3 as LineBasicMaterial, b4 as Line$1, b5 as LineDashedMaterial, b6 as CanvasTexture, b7 as Object3D, b8 as FogExp2, b9 as Fog, ba as AmbientLight, bb as HemisphereLight, bc as SpotLight, bd as PointLight, be as DirectionalLight, bf as BufferAttribute, bg as heatPointsForSide, bh as COMPARISON_COLORS, bi as comparisonHeatDepthTest, bj as comparisonHeatEdgeOpacity, bk as comparisonHeatPatchOpacity, bl as shapeToGeometry, bm as buildComparisonHeatPatchGeometry, bn as EdgesGeometry, bo as buildShapeFromCompilePlan, bp as VIEWPORT_CAMERA_STORAGE_KEY, bq as parseViewportCameraState, br as getKernelFaceNameForTriangle, bs as OBJECT_CONTEXT_MENU_MARGIN, bt as buildVisibleHistoryStacks, bu as sketchToSvg, bv as sketchToDxf, bw as runScript, bx as MeshPhysicalMaterial, by as LineSegments, bz as findDesignTraceNodeForConstructionStep, bA as formatDesignTraceAnchor, bB as waitForAnimationFrame, bC as selectBuildLedgerNodes, bD as worldAuthorPlaneToLocal, bE as compileSdfProgramEvaluator3, bF as SDF_LINEAR_OUTPUT_COLOR_GLSL, bG as GLSL3, bH as BoxGeometry, bI as Data3DTexture, bJ as buildSdfRaymarchFragmentShader, bK as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bL as scanProxySourceBytes, bM as disposeScanProxyGeometry, bN as scanProxyGeometryFromPayload, bO as AdditiveBlending, bP as geometryWithVisibleVertexColors, bQ as MeshBVH, bR as makeColorScaleTexture, bS as colorScaleLUT, bT as makeInspectScalarUniforms, bU as updateInspectScalarUniforms, bV as descriptorToThreeTexture, bW as applyProjectedTexture, bX as getRenderStylePreset, bY as SCAN_RENDER_COLORS, bZ as NormalBlending, b_ as acceleratedRaycast, b$ as INSPECT_SCALAR_FRAGMENT_SHADER, c0 as INSPECT_SCALAR_VERTEX_SHADER, c1 as scanMaterialShellColor, c2 as ZEBRA_STRIPE_SOFTNESS, c3 as ZEBRA_STRIPE_SCALE, c4 as ZEBRA_LIGHT_COLOR, c5 as ZEBRA_DARK_COLOR, c6 as ZEBRA_ACCENT_COLOR, c7 as ZEBRA_STRIPE_FRAGMENT_SHADER, c8 as ZEBRA_STRIPE_VERTEX_SHADER, c9 as SCAN_PROXY_LAYER_STYLES, ca as scanMaterialLayerStyles, cb as SURFACE_FIELD_FRAGMENT_SHADER, cc as SURFACE_FIELD_VERTEX_SHADER, cd as WORLD_UP$1, ce as CatmullRomCurve3, cf as TubeGeometry, cg as DEFAULT_THICKNESS_COLOR_RANGE, ch as DEFAULT_COLORMAP, ci as colorScaleHexStops, cj as PERFORMANCE_SAMPLE_INTERVAL_SEC, ck as formatPerformanceCount, cl as NON_TEXT_INPUT_TYPES, cm as MeshStandardMaterial, cn as Shape, co as ShapeGeometry, cp as ShaderLib, cq as CylinderGeometry, cr as createResolvedExplodeConfig, cs as explodeBoundsCenter, ct as explodeMergeBounds, cu as resolveExplodeDirective, cv as computeExplodeMotion, cw as getSketchWorldMatrix, cx as explodeAdd, cy as hasExplodeOverride, cz as resolveExplodeLocalFanDirection, cA as explodeMul, cB as explodeLeafFanStage, cC as normalizeCutPlane, cD as toClippingPlane, cE as isObjectExcludedFromCutPlane, cF as getShapePorts, cG as getShapeUsedPorts, cH as DEFAULT_VIEW_CONFIG, cI as SECTION_EXPLORER_PLANE_NAME, cJ as ZERO_OFFSET, cK as scanProxyGridForBounds, cL as IDENTITY_MATRIX$2, cM as OBJECT_CONTEXT_MENU_WIDTH, cN as OBJECT_CONTEXT_MENU_HEIGHT, cO as triangleSoupFromMeshes, cP as compareTriangleSoups, cQ as buildGeometryComparisonPointCloud, cR as aabbOverlaps, cS as aabbOverlapVolume, cT as DEFAULT_COLLISION_INSPECTION_OPTIONS, cU as resolveScalarSceneSampleBudget, cV as INSPECT_POINT_SAMPLE_COUNT_MIN, cW as FOCUS_MODE_DIM_OPACITY, cX as DEFAULT_THICKNESS_INSPECTION_OPTIONS, cY as comparisonCandidateContextOpacity, cZ as Matrix3, c_ as initBackendForEvaluation } from "./backendInit-6C0DLgH0.js";
7
+ import { _ as __vitePreload, S as SDF_PROGRAM_OP, z as zipSync, s as strToU8, W as WebGLRenderer, R as Raycaster, O as OrthographicCamera$1, P as PerspectiveCamera$1, a as Scene, b as PCFSoftShadowMap, V as VSMShadowMap, c as PCFShadowMap, B as BasicShadowMap, C as ColorManagement, L as LinearSRGBColorSpace, d as SRGBColorSpace, N as NoToneMapping, A as ACESFilmicToneMapping, e as Layers, f as Color, g as RGBAFormat, U as UnsignedByteType, h as Vector3, i as Vector2, j as Clock, T as THREE, D as DoubleSide, k as REVISION, M as Mesh, I as IcosahedronGeometry, l as ShaderMaterial, m as Spherical, Q as Quaternion, n as MOUSE, o as TOUCH, p as Ray, q as Plane, r as DataTextureLoader, H as HalfFloatType, F as FloatType, t as DataUtils, u as LinearFilter, v as RedFormat, w as InstancedBufferGeometry, x as Float32BufferAttribute, y as InstancedInterleavedBuffer, E as InterleavedBufferAttribute, G as WireframeGeometry, J as Box3, K as Sphere, X as UniformsUtils, Y as UniformsLib, Z as Vector4, $ as Line3, a0 as Matrix4, a1 as MathUtils, a2 as Uniform, a3 as WebGLRenderTarget, a4 as DepthTexture, a5 as BackSide, a6 as ClampToEdgeWrapping, a7 as PlaneGeometry, a8 as UVMapping, a9 as DataTexture, aa as Texture, ab as MeshBasicMaterial, ac as IntType, ad as ShortType, ae as ByteType, af as UnsignedIntType, ag as Loader, ah as LoadingManager, ai as LinearMipMapLinearFilter, aj as FileLoader, ak as NoBlending, al as CubeReflectionMapping, am as EquirectangularReflectionMapping, an as CubeTextureLoader, ao as WebGLCubeRenderTarget, ap as ConstraintSketch, aq as setSketchPlacement3D, ar as Sketch, as as PROFILE_BACKEND_MARKER, at as FrozenShape, au as setShapeCompilePlan, av as hasAnyPorts, aw as setShapePortsInternal, ax as markShapePortsUsed, ay as writeViewPreferences, az as setParamOverrides, aA as readViewPreferences, aB as resolveForgeRenderStyle, aC as isConstraintSketch, aD as updateConstraintValue, aE as linearizeMultiObjectSteps, aF as getShapeCompilePlan, aG as resolveCameraControlMode, aH as resolveComparisonOpacity, aI as resolveComparisonInspectMode, aJ as resolveBooleanPref, aK as resolveInspectQuantizeBands, aL as resolveInspectIsolineSpacing, aM as resolveInspectColormap, aN as resolveThicknessColorRange, aO as resolveInspectPointSampleCount, aP as SCAN_PROXY_GRANULARITY_DEFAULT, aQ as DEFAULT_MANUAL_SCENE_SETTINGS, aR as resolveManualSceneSettings, aS as publishSolverWasmRunDebug, aT as resolveForgeQualityPreset, aU as DEFAULT_INSPECT_ISOLINES_ENABLED, aV as SCAN_PROXY_MATRIX_GRANULARITY_MAX, aW as findJointAnimationClip, aX as resolveJointAnimation, aY as resolveJointViewValues, aZ as resolveImportPath, a_ as DEFAULT_INSPECT_CRITICAL_LINE_ENABLED, a$ as resolveScanProxyGranularity, b0 as DEFAULT_COMPARISON_REFERENCE_OPACITY, b1 as DEFAULT_COMPARISON_CANDIDATE_OPACITY, b2 as BufferGeometry, b3 as LineBasicMaterial, b4 as Line$1, b5 as LineDashedMaterial, b6 as CanvasTexture, b7 as Object3D, b8 as FogExp2, b9 as Fog, ba as AmbientLight, bb as HemisphereLight, bc as SpotLight, bd as PointLight, be as DirectionalLight, bf as BufferAttribute, bg as heatPointsForSide, bh as COMPARISON_COLORS, bi as comparisonHeatDepthTest, bj as comparisonHeatEdgeOpacity, bk as comparisonHeatPatchOpacity, bl as shapeToGeometry, bm as buildComparisonHeatPatchGeometry, bn as EdgesGeometry, bo as buildShapeFromCompilePlan, bp as VIEWPORT_CAMERA_STORAGE_KEY, bq as parseViewportCameraState, br as getKernelFaceNameForTriangle, bs as OBJECT_CONTEXT_MENU_MARGIN, bt as buildVisibleHistoryStacks, bu as sketchToSvg, bv as sketchToDxf, bw as runScript, bx as MeshPhysicalMaterial, by as LineSegments, bz as findDesignTraceNodeForConstructionStep, bA as formatDesignTraceAnchor, bB as waitForAnimationFrame, bC as selectBuildLedgerNodes, bD as UNIDENTIFIED_FACE_NAME, bE as getBakedEdges, bF as worldAuthorPlaneToLocal, bG as compileSdfProgramEvaluator3, bH as SDF_LINEAR_OUTPUT_COLOR_GLSL, bI as GLSL3, bJ as BoxGeometry, bK as Data3DTexture, bL as buildSdfRaymarchFragmentShader, bM as SDF_RAYMARCH_PROXY_VERTEX_SHADER, bN as scanProxySourceBytes, bO as disposeScanProxyGeometry, bP as scanProxyGeometryFromPayload, bQ as AdditiveBlending, bR as geometryWithVisibleVertexColors, bS as MeshBVH, bT as makeColorScaleTexture, bU as colorScaleLUT, bV as makeScalarValueTexture, bW as makeInspectScalarUniforms, bX as updateInspectScalarUniforms, bY as descriptorToThreeTexture, bZ as applyProjectedTexture, b_ as getRenderStylePreset, b$ as SCAN_RENDER_COLORS, c0 as NormalBlending, c1 as acceleratedRaycast, c2 as INSPECT_SCALAR_FRAGMENT_SHADER, c3 as INSPECT_SCALAR_VERTEX_SHADER, c4 as scanMaterialShellColor, c5 as ZEBRA_STRIPE_SOFTNESS, c6 as ZEBRA_STRIPE_SCALE, c7 as ZEBRA_LIGHT_COLOR, c8 as ZEBRA_DARK_COLOR, c9 as ZEBRA_ACCENT_COLOR, ca as ZEBRA_STRIPE_FRAGMENT_SHADER, cb as ZEBRA_STRIPE_VERTEX_SHADER, cc as SCAN_PROXY_LAYER_STYLES, cd as scanMaterialLayerStyles, ce as SURFACE_FIELD_FRAGMENT_SHADER, cf as SURFACE_FIELD_VERTEX_SHADER, cg as WORLD_UP$1, ch as CatmullRomCurve3, ci as TubeGeometry, cj as DEFAULT_THICKNESS_COLOR_RANGE, ck as DEFAULT_COLORMAP, cl as colorScaleHexStops, cm as PERFORMANCE_SAMPLE_INTERVAL_SEC, cn as formatPerformanceCount, co as NON_TEXT_INPUT_TYPES, cp as MeshStandardMaterial, cq as Shape, cr as ShapeGeometry, cs as ShaderLib, ct as CylinderGeometry, cu as createResolvedExplodeConfig, cv as explodeBoundsCenter, cw as explodeMergeBounds, cx as resolveExplodeDirective, cy as computeExplodeMotion, cz 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_WIDTH, cQ as OBJECT_CONTEXT_MENU_HEIGHT, cR as placement2DItemsOverlap, cS as placement2DFootprintInsideFrame, cT as placement2DPointInsideFrame, cU as placement2DFrameBounds, cV as placement2DClampCenterToFrame, cW as triangleSoupFromMeshes, cX as compareTriangleSoups, cY as buildGeometryComparisonPointCloud, cZ as aabbOverlaps, c_ as aabbOverlapVolume, c$ as DEFAULT_COLLISION_INSPECTION_OPTIONS, d0 as resolveScalarSceneSampleBudget, d1 as INSPECT_POINT_SAMPLE_COUNT_MIN, d2 as FOCUS_MODE_DIM_OPACITY, d3 as DEFAULT_THICKNESS_INSPECTION_OPTIONS, d4 as comparisonCandidateContextOpacity, d5 as Matrix3, d6 as initBackendForEvaluation } from "./backendInit-6a9-ilom.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];
@@ -213,7 +213,7 @@ function trimRunResultCache() {
213
213
  }
214
214
  function persistCache() {
215
215
  try {
216
- const entries = {};
216
+ const entries2 = {};
217
217
  let estimatedPersistBytes = 0;
218
218
  for (const [key, entry] of runResultCache) {
219
219
  estimatedPersistBytes += entry.estimatedBytes;
@@ -224,7 +224,7 @@ function persistCache() {
224
224
  }
225
225
  return;
226
226
  }
227
- entries[key] = {
227
+ entries2[key] = {
228
228
  code: entry.code,
229
229
  files: entry.files,
230
230
  paramOverrides: entry.paramOverrides,
@@ -234,7 +234,7 @@ function persistCache() {
234
234
  serialized: serializedResultToJson(entry.serialized)
235
235
  };
236
236
  }
237
- const json = JSON.stringify({ v: CACHE_VERSION, entries });
237
+ const json = JSON.stringify({ v: CACHE_VERSION, entries: entries2 });
238
238
  if (json.length > MAX_PERSIST_BYTES) {
239
239
  try {
240
240
  sessionStorage.removeItem(sessionStorageKey());
@@ -666,14 +666,14 @@ function BrandMark({
666
666
  }
667
667
  );
668
668
  }
669
- let nextId = 0;
669
+ let nextId$1 = 0;
670
670
  let toasts = [];
671
671
  const listeners$2 = /* @__PURE__ */ new Set();
672
672
  function notify$1() {
673
673
  listeners$2.forEach((l2) => l2());
674
674
  }
675
675
  function showToast(message, variant = "info", durationMs = 3e3) {
676
- const toast = { id: nextId++, message, variant, durationMs };
676
+ const toast = { id: nextId$1++, message, variant, durationMs };
677
677
  toasts = [...toasts, toast];
678
678
  notify$1();
679
679
  setTimeout(() => {
@@ -1551,7 +1551,7 @@ const is = {
1551
1551
  return true;
1552
1552
  }
1553
1553
  };
1554
- function buildGraph(object) {
1554
+ function buildGraph$1(object) {
1555
1555
  const data = {
1556
1556
  nodes: {},
1557
1557
  materials: {},
@@ -2359,7 +2359,7 @@ function loadingFn(extensions, onProgress) {
2359
2359
  }
2360
2360
  if (extensions) extensions(loader);
2361
2361
  return Promise.all(input.map((input2) => new Promise((res, reject) => loader.load(input2, (data) => {
2362
- if (isObject3D(data == null ? void 0 : data.scene)) Object.assign(data, buildGraph(data.scene));
2362
+ if (isObject3D(data == null ? void 0 : data.scene)) Object.assign(data, buildGraph$1(data.scene));
2363
2363
  res(data);
2364
2364
  }, onProgress, (error) => reject(new Error(`Could not load ${input2}: ${error == null ? void 0 : error.message}`))))));
2365
2365
  };
@@ -14866,6 +14866,7 @@ const dark = {
14866
14866
  bg: "#0d1117",
14867
14867
  bgPanel: "#161b22",
14868
14868
  bgSurface: "#1c2128",
14869
+ bgSubtle: "#131920",
14869
14870
  bgHover: "#242b35",
14870
14871
  bgActive: "#2d333b",
14871
14872
  bgInput: "#0a0e14",
@@ -14902,6 +14903,7 @@ const light = {
14902
14903
  bg: "#f0f4f8",
14903
14904
  bgPanel: "#f8fafc",
14904
14905
  bgSurface: "#e8edf2",
14906
+ bgSubtle: "#eef2f6",
14905
14907
  bgHover: "#dfe5ec",
14906
14908
  bgActive: "#d2dae3",
14907
14909
  bgInput: "#f8fafc",
@@ -14938,6 +14940,7 @@ const gruvbox = {
14938
14940
  bg: "#282828",
14939
14941
  bgPanel: "#1d2021",
14940
14942
  bgSurface: "#3c3836",
14943
+ bgSubtle: "#32302f",
14941
14944
  bgHover: "#504945",
14942
14945
  bgActive: "#665c54",
14943
14946
  bgInput: "#1d2021",
@@ -14974,6 +14977,7 @@ const tokyoNight = {
14974
14977
  bg: "#1a1b26",
14975
14978
  bgPanel: "#16161e",
14976
14979
  bgSurface: "#24283b",
14980
+ bgSubtle: "#1f2335",
14977
14981
  bgHover: "#292e42",
14978
14982
  bgActive: "#33467c",
14979
14983
  bgInput: "#16161e",
@@ -15010,6 +15014,7 @@ const kanagawaLotus = {
15010
15014
  bg: "#f2ecbc",
15011
15015
  bgPanel: "#f7f3d7",
15012
15016
  bgSurface: "#e7dba0",
15017
+ bgSubtle: "#eee6b2",
15013
15018
  bgHover: "#d9d08e",
15014
15019
  bgActive: "#c9b97a",
15015
15020
  bgInput: "#f7f3d7",
@@ -15046,6 +15051,7 @@ const shield = {
15046
15051
  bg: "#02070a",
15047
15052
  bgPanel: "#06141a",
15048
15053
  bgSurface: "#0a2028",
15054
+ bgSubtle: "#031016",
15049
15055
  bgHover: "#0d2b35",
15050
15056
  bgActive: "#113d4a",
15051
15057
  bgInput: "#01080c",
@@ -15082,6 +15088,7 @@ const shieldLight = {
15082
15088
  bg: "#cdd5d3",
15083
15089
  bgPanel: "#dbe3e0",
15084
15090
  bgSurface: "#b9c8c5",
15091
+ bgSubtle: "#c4cfcc",
15085
15092
  bgHover: "#a9b8b5",
15086
15093
  bgActive: "#8ea3a0",
15087
15094
  bgInput: "#eef3f1",
@@ -15287,15 +15294,14 @@ const runResultDeserializer = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Obje
15287
15294
  __proto__: null,
15288
15295
  deserializeRunResult
15289
15296
  }, Symbol.toStringTag, { value: "Module" }));
15290
- function compilePlanNeedsNativeShapeLowering(plan) {
15291
- if (plan.kind === "queryOwner") return compilePlanNeedsNativeShapeLowering(plan.base);
15292
- return plan.kind !== "sdf";
15297
+ function compilePlanNeedsServerShapeLowering(_plan) {
15298
+ return true;
15293
15299
  }
15294
- function serializedSceneObjectNeedsNativeShapeLowering(object) {
15295
- return object.compilePlan !== null && object.compilePlan !== void 0 && compilePlanNeedsNativeShapeLowering(object.compilePlan);
15300
+ function serializedSceneObjectNeedsServerShapeLowering(object) {
15301
+ return object.compilePlan !== null && object.compilePlan !== void 0 && compilePlanNeedsServerShapeLowering(object.compilePlan);
15296
15302
  }
15297
- function serializedRunResultNeedsNativeOcctLowering(result) {
15298
- return result.objects.some(serializedSceneObjectNeedsNativeShapeLowering);
15303
+ function serializedRunResultNeedsServerIrLowering(result) {
15304
+ return result.objects.some(serializedSceneObjectNeedsServerShapeLowering);
15299
15305
  }
15300
15306
  function decodeFromWire(data) {
15301
15307
  const view2 = new DataView(data);
@@ -15976,7 +15982,7 @@ const CRASH_COOLDOWN_MS = 2e3;
15976
15982
  class EvalWorkerClient {
15977
15983
  constructor(workerFactory = () => new Worker(new URL(
15978
15984
  /* @vite-ignore */
15979
- "/assets/evalWorker-BssDYW9u.js",
15985
+ "/assets/evalWorker-DQ82ueGu.js",
15980
15986
  import.meta.url
15981
15987
  ), { type: "module" })) {
15982
15988
  __publicField(this, "worker", null);
@@ -17168,8 +17174,8 @@ async function fetchGistModel(gistId) {
17168
17174
  if (!res.ok) throw new Error(`Failed to fetch gist: ${res.status} ${res.statusText}`);
17169
17175
  const data = await res.json();
17170
17176
  const files = data.files;
17171
- const entries = Object.values(files);
17172
- const forgeFile = entries.find((f2) => f2.filename.endsWith(".forge.js")) || entries.find((f2) => f2.filename.endsWith(".sketch.js")) || entries[0];
17177
+ const entries2 = Object.values(files);
17178
+ const forgeFile = entries2.find((f2) => f2.filename.endsWith(".forge.js")) || entries2.find((f2) => f2.filename.endsWith(".sketch.js")) || entries2[0];
17173
17179
  if (!forgeFile) throw new Error("Gist contains no files");
17174
17180
  return { filename: forgeFile.filename, code: forgeFile.content };
17175
17181
  }
@@ -17360,6 +17366,9 @@ function createErrorRunResult(message, quality) {
17360
17366
  params: [],
17361
17367
  stringParams: [],
17362
17368
  listParams: [],
17369
+ path2dParams: [],
17370
+ spline2dParams: [],
17371
+ placement2dParams: [],
17363
17372
  dimensions: [],
17364
17373
  highlights: [],
17365
17374
  debugHighlights3D: [],
@@ -17431,6 +17440,9 @@ function buildRunState(previewFile, runResult, state2) {
17431
17440
  params: runResult.params,
17432
17441
  stringParams: runResult.stringParams,
17433
17442
  listParams: runResult.listParams,
17443
+ path2dParams: runResult.path2dParams ?? [],
17444
+ spline2dParams: runResult.spline2dParams ?? [],
17445
+ placement2dParams: runResult.placement2dParams ?? [],
17434
17446
  jointValues: nextJointValues,
17435
17447
  jointAnimationClip: nextAnimationState.clip,
17436
17448
  jointAnimationProgress: nextAnimationState.progress,
@@ -17452,13 +17464,15 @@ const KERNELS = [
17452
17464
  { id: "occt", name: "occt", location: "local", label: "OCCT" },
17453
17465
  { id: "truck", name: "truck", location: "local", label: "Truck" },
17454
17466
  { id: "sdf", name: "sdf", location: "local", label: "SDF" },
17455
- { id: "server-occt", name: "occt", location: "server", label: "Server OCCT" }
17467
+ { id: "server-occt", name: "occt", location: "server", label: "Server OCCT" },
17468
+ { id: "server-sdf", name: "sdf", location: "server", label: "Server SDF" }
17456
17469
  ];
17457
17470
  function availableKernels(canUseServer) {
17458
17471
  return KERNELS.filter((kernel) => kernel.location === "local" || canUseServer);
17459
17472
  }
17460
17473
  function kernelId(kernel) {
17461
- return kernel.location === "server" ? "server-occt" : kernel.name;
17474
+ if (kernel.location === "local") return kernel.name;
17475
+ return kernel.name === "sdf" ? "server-sdf" : "server-occt";
17462
17476
  }
17463
17477
  function kernelFromId(id) {
17464
17478
  const kernel = KERNELS.find((entry) => entry.id === id);
@@ -17533,6 +17547,9 @@ function errorRunResult(error, timeMs = 0) {
17533
17547
  params: [],
17534
17548
  stringParams: [],
17535
17549
  listParams: [],
17550
+ path2dParams: [],
17551
+ spline2dParams: [],
17552
+ placement2dParams: [],
17536
17553
  dimensions: [],
17537
17554
  highlights: [],
17538
17555
  debugHighlights3D: [],
@@ -17636,6 +17653,7 @@ const VIEW_INSPECT_CHANNELS = /* @__PURE__ */ new Set([
17636
17653
  "distance",
17637
17654
  "collisions",
17638
17655
  "thickness",
17656
+ "throughThickness",
17639
17657
  "roughness"
17640
17658
  ]);
17641
17659
  const INSPECT_DISPLAY_MODES = /* @__PURE__ */ new Set(["heatmap", "points", "both", "scan"]);
@@ -17658,7 +17676,7 @@ function resolveInspectDisplayMode(value) {
17658
17676
  return typeof value === "string" && INSPECT_DISPLAY_MODES.has(value) ? value : "heatmap";
17659
17677
  }
17660
17678
  function isScalarInspectChannel(channel) {
17661
- return channel === "thickness" || channel === "roughness";
17679
+ return channel === "thickness" || channel === "throughThickness" || channel === "roughness";
17662
17680
  }
17663
17681
  function shouldUseScanRenderStyle(channel, displayMode) {
17664
17682
  return isScalarInspectChannel(channel) && displayMode === "scan";
@@ -17714,7 +17732,8 @@ function codeEditorPatchForActiveFile(activeFile, files, meshPreviewFile) {
17714
17732
  }
17715
17733
  function runResultHasViewportContent(result) {
17716
17734
  var _a3, _b2, _c, _d;
17717
- return result.objects.length > 0 || !!result.assemblyKinematics || (((_a3 = result.jointsView) == null ? void 0 : _a3.joints.length) ?? 0) > 0 && ((_b2 = result.jointsView) == null ? void 0 : _b2.enabled) !== false || (((_c = result.renderLabels) == null ? void 0 : _c.length) ?? 0) > 0 || (((_d = result.debugHighlights3D) == null ? void 0 : _d.length) ?? 0) > 0;
17735
+ const hasAnchoredParams = result.params.some((param) => !!param.anchor) || result.stringParams.some((param) => !!param.anchor) || result.listParams.some((param) => !!param.anchor) || (result.path2dParams ?? []).some((param) => !!param.anchor) || (result.spline2dParams ?? []).some((param) => !!param.anchor) || (result.placement2dParams ?? []).some((param) => !!param.anchor);
17736
+ return result.objects.length > 0 || !!result.assemblyKinematics || (((_a3 = result.jointsView) == null ? void 0 : _a3.joints.length) ?? 0) > 0 && ((_b2 = result.jointsView) == null ? void 0 : _b2.enabled) !== false || (((_c = result.renderLabels) == null ? void 0 : _c.length) ?? 0) > 0 || (((_d = result.debugHighlights3D) == null ? void 0 : _d.length) ?? 0) > 0 || hasAnchoredParams;
17718
17737
  }
17719
17738
  function codeEditorPatchForRunResult(result, meshPreviewFile) {
17720
17739
  if (meshPreviewFile || runResultHasViewportContent(result)) return {};
@@ -17748,7 +17767,7 @@ const makeParamSnapshotId = () => {
17748
17767
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
17749
17768
  return `param-snapshot-${Date.now()}-${Math.random().toString(36).slice(2)}`;
17750
17769
  };
17751
- function filterOverridesForCurrentParams(overrides, params, stringParams, listParams) {
17770
+ function filterOverridesForCurrentParams(overrides, params, stringParams, listParams, path2dParams, spline2dParams, placement2dParams) {
17752
17771
  const scalarNames = /* @__PURE__ */ new Set([...params.map((param) => param.name), ...stringParams.map((param) => param.name)]);
17753
17772
  const filtered = {};
17754
17773
  let ignoredCount = 0;
@@ -17758,17 +17777,51 @@ function filterOverridesForCurrentParams(overrides, params, stringParams, listPa
17758
17777
  continue;
17759
17778
  }
17760
17779
  const listDef = listParams.find((candidate) => key === `${candidate.name}.__count__` || key.startsWith(`${candidate.name}[`));
17761
- if (!listDef) {
17762
- ignoredCount += 1;
17780
+ if (listDef) {
17781
+ if (key === `${listDef.name}.__count__`) {
17782
+ filtered[key] = value;
17783
+ continue;
17784
+ }
17785
+ const fieldName = key.slice(key.lastIndexOf(".") + 1);
17786
+ if (listDef.fieldOrder.includes(fieldName)) filtered[key] = value;
17787
+ else ignoredCount += 1;
17763
17788
  continue;
17764
17789
  }
17765
- if (key === `${listDef.name}.__count__`) {
17766
- filtered[key] = value;
17790
+ const pathDef = path2dParams.find((candidate) => key === `${candidate.name}.__count__` || key.startsWith(`${candidate.name}[`));
17791
+ if (pathDef) {
17792
+ if (key === `${pathDef.name}.__count__`) {
17793
+ filtered[key] = value;
17794
+ continue;
17795
+ }
17796
+ const fieldName = key.slice(key.lastIndexOf(".") + 1);
17797
+ if (fieldName === "x" || fieldName === "y") filtered[key] = value;
17798
+ else ignoredCount += 1;
17799
+ continue;
17800
+ }
17801
+ const splineDef = spline2dParams.find((candidate) => key === `${candidate.name}.__count__` || key.startsWith(`${candidate.name}[`));
17802
+ if (splineDef) {
17803
+ if (key === `${splineDef.name}.__count__`) {
17804
+ filtered[key] = value;
17805
+ continue;
17806
+ }
17807
+ const fieldName = key.slice(key.lastIndexOf(".") + 1);
17808
+ if (fieldName === "x" || fieldName === "y" || fieldName === "g") filtered[key] = value;
17809
+ else ignoredCount += 1;
17810
+ continue;
17811
+ }
17812
+ const placementDef = placement2dParams.find((candidate) => key.startsWith(`${candidate.name}.`));
17813
+ if (placementDef) {
17814
+ const suffix = key.slice(placementDef.name.length + 1);
17815
+ const [itemId, fieldName, ...extra] = suffix.split(".");
17816
+ const item = placementDef.items.find((candidate) => candidate.id === itemId);
17817
+ if (item && extra.length === 0 && (fieldName === "x" || fieldName === "y" || fieldName === "angle" || fieldName === "zone")) {
17818
+ filtered[key] = value;
17819
+ } else {
17820
+ ignoredCount += 1;
17821
+ }
17767
17822
  continue;
17768
17823
  }
17769
- const fieldName = key.slice(key.lastIndexOf(".") + 1);
17770
- if (listDef.fieldOrder.includes(fieldName)) filtered[key] = value;
17771
- else ignoredCount += 1;
17824
+ ignoredCount += 1;
17772
17825
  }
17773
17826
  return { overrides: filtered, ignoredCount };
17774
17827
  }
@@ -18185,6 +18238,9 @@ const useForgeStore = create((set, get) => ({
18185
18238
  params: [],
18186
18239
  stringParams: [],
18187
18240
  listParams: [],
18241
+ path2dParams: [],
18242
+ spline2dParams: [],
18243
+ placement2dParams: [],
18188
18244
  runQuality: resolveForgeQualityPreset(initialViewPreferences.runQuality ?? "live"),
18189
18245
  setRunQuality: (quality) => {
18190
18246
  const next = resolveForgeQualityPreset(quality);
@@ -18195,6 +18251,9 @@ const useForgeStore = create((set, get) => ({
18195
18251
  paramOverrides: {},
18196
18252
  paramOverridesByFile: {},
18197
18253
  paramSnapshotsByFile: initialParamSnapshotsByFile,
18254
+ focusedParamName: null,
18255
+ spatialParamSheetName: null,
18256
+ expandedSpatialParamSheetName: null,
18198
18257
  jointValues: {},
18199
18258
  jointAnimationClip: null,
18200
18259
  jointAnimationProgress: 0,
@@ -18233,8 +18292,9 @@ const useForgeStore = create((set, get) => ({
18233
18292
  },
18234
18293
  computeTarget: initialKernel.location,
18235
18294
  setComputeTarget: (target) => {
18295
+ const preferredServerBackend = get().activeBackend === "sdf" ? "sdf" : "occt";
18236
18296
  const kernel = resolveAvailableKernel(
18237
- { name: target === "server" ? "occt" : get().activeBackend, location: target },
18297
+ { name: target === "server" ? preferredServerBackend : get().activeBackend, location: target },
18238
18298
  availableKernels(canUseServerCompute())
18239
18299
  );
18240
18300
  writeViewPreferences({ activeBackend: kernel.name, computeTarget: kernel.location });
@@ -18292,6 +18352,9 @@ const useForgeStore = create((set, get) => ({
18292
18352
  params: [],
18293
18353
  stringParams: [],
18294
18354
  listParams: [],
18355
+ path2dParams: [],
18356
+ spline2dParams: [],
18357
+ placement2dParams: [],
18295
18358
  previewFile: null,
18296
18359
  objectSettings: {},
18297
18360
  buildLedgerEvents: []
@@ -18345,14 +18408,15 @@ const useForgeStore = create((set, get) => ({
18345
18408
  serverComputeAbortController = abortController;
18346
18409
  try {
18347
18410
  const irRequest = await evalWorkerClient.runIrPlan(runPayload);
18348
- if (irRequest.planResult.error || !serializedRunResultNeedsNativeOcctLowering(irRequest.planResult)) {
18411
+ if (irRequest.planResult.error || !serializedRunResultNeedsServerIrLowering(irRequest.planResult)) {
18349
18412
  serialized = irRequest.planResult;
18350
18413
  } else {
18351
18414
  const refreshTimer2 = window.setTimeout(() => {
18352
18415
  void get().refreshServerJobs();
18353
18416
  }, 250);
18354
18417
  try {
18355
- serialized = await computeIr(irRequest, abortController.signal);
18418
+ const serverBackend = get().activeBackend === "sdf" ? "sdf" : "occt";
18419
+ serialized = await computeIr({ ...irRequest, serverBackend }, abortController.signal);
18356
18420
  } finally {
18357
18421
  window.clearTimeout(refreshTimer2);
18358
18422
  void get().refreshServerJobs();
@@ -18493,6 +18557,19 @@ const useForgeStore = create((set, get) => ({
18493
18557
  get().execute();
18494
18558
  }, PARAM_DEBOUNCE_MS);
18495
18559
  },
18560
+ setParams: (values, deleteKeys = []) => {
18561
+ const overrides = { ...get().paramOverrides, ...values };
18562
+ for (const key of deleteKeys) delete overrides[key];
18563
+ const { activeFile: curFile, paramOverridesByFile } = get();
18564
+ const previewKey = curFile ? resolvePreviewFile(curFile, get().files) : null;
18565
+ const nextByFile = previewKey ? { ...paramOverridesByFile, [previewKey]: overrides } : paramOverridesByFile;
18566
+ set({ paramOverrides: overrides, paramOverridesByFile: nextByFile });
18567
+ if (paramExecuteTimer) clearTimeout(paramExecuteTimer);
18568
+ paramExecuteTimer = setTimeout(() => {
18569
+ paramExecuteTimer = null;
18570
+ get().execute();
18571
+ }, PARAM_DEBOUNCE_MS);
18572
+ },
18496
18573
  resetParamOverrides: () => {
18497
18574
  const { activeFile, files, paramOverridesByFile } = get();
18498
18575
  const previewKey = activeFile ? resolvePreviewFile(activeFile, files) : null;
@@ -18502,6 +18579,11 @@ const useForgeStore = create((set, get) => ({
18502
18579
  setParamOverrides({});
18503
18580
  get().execute();
18504
18581
  },
18582
+ focusParam: (name) => set({ focusedParamName: name }),
18583
+ openSpatialParamSheet: (name) => set({ focusedParamName: name, spatialParamSheetName: name, expandedSpatialParamSheetName: null }),
18584
+ expandSpatialParamSheet: (name) => set({ focusedParamName: name, spatialParamSheetName: name, expandedSpatialParamSheetName: name }),
18585
+ closeExpandedSpatialParamSheet: () => set({ expandedSpatialParamSheetName: null }),
18586
+ closeSpatialParamSheet: () => set({ spatialParamSheetName: null, expandedSpatialParamSheetName: null }),
18505
18587
  captureParamSnapshot: () => {
18506
18588
  var _a3;
18507
18589
  const { activeFile, files, paramOverrides, paramSnapshotsByFile } = get();
@@ -18521,11 +18603,30 @@ const useForgeStore = create((set, get) => ({
18521
18603
  },
18522
18604
  applyParamSnapshot: (id) => {
18523
18605
  var _a3;
18524
- const { activeFile, files, paramOverridesByFile, paramSnapshotsByFile, params, stringParams, listParams } = get();
18606
+ const {
18607
+ activeFile,
18608
+ files,
18609
+ paramOverridesByFile,
18610
+ paramSnapshotsByFile,
18611
+ params,
18612
+ stringParams,
18613
+ listParams,
18614
+ path2dParams,
18615
+ spline2dParams,
18616
+ placement2dParams
18617
+ } = get();
18525
18618
  const previewKey = activeFile ? resolvePreviewFile(activeFile, files) : null;
18526
18619
  const snapshot = previewKey ? (_a3 = paramSnapshotsByFile[previewKey]) == null ? void 0 : _a3.find((candidate) => candidate.id === id) : null;
18527
18620
  if (!previewKey || !snapshot) return;
18528
- const { overrides, ignoredCount } = filterOverridesForCurrentParams(snapshot.overrides, params, stringParams, listParams);
18621
+ const { overrides, ignoredCount } = filterOverridesForCurrentParams(
18622
+ snapshot.overrides,
18623
+ params,
18624
+ stringParams,
18625
+ listParams,
18626
+ path2dParams,
18627
+ spline2dParams,
18628
+ placement2dParams
18629
+ );
18529
18630
  const nextByFile = { ...paramOverridesByFile };
18530
18631
  if (Object.keys(overrides).length > 0) nextByFile[previewKey] = overrides;
18531
18632
  else delete nextByFile[previewKey];
@@ -18978,7 +19079,17 @@ const useForgeStore = create((set, get) => ({
18978
19079
  };
18979
19080
  }),
18980
19081
  selectedObjectId: null,
18981
- selectObject: (id) => set({ selectedObjectId: id, constructionGhost: null, selectedConstraintId: null }),
19082
+ selectObject: (id) => set((state2) => ({
19083
+ selectedObjectId: id,
19084
+ constructionGhost: null,
19085
+ selectedConstraintId: null,
19086
+ // Face selection is a sub-selection of an object; drop it when the active
19087
+ // object changes (the click handler re-populates it for the new object).
19088
+ selectedFace: state2.selectedFace && state2.selectedFace.objectId === id ? state2.selectedFace : null,
19089
+ // Edge/vertex sub-selections are likewise scoped to the active object.
19090
+ selectedEdge: state2.selectedEdge && state2.selectedEdge.objectId === id ? state2.selectedEdge : null,
19091
+ selectedVertex: state2.selectedVertex && state2.selectedVertex.objectId === id ? state2.selectedVertex : null
19092
+ })),
18982
19093
  constructionGhost: null,
18983
19094
  setConstructionGhost: (ghost) => set({ constructionGhost: ghost }),
18984
19095
  // ── Construction history replay ──
@@ -19177,6 +19288,30 @@ const useForgeStore = create((set, get) => ({
19177
19288
  setHoveredSurfaceIndex: (index) => set((state2) => state2.hoveredSurfaceIndex === index ? state2 : { hoveredSurfaceIndex: index }),
19178
19289
  selectedSurfaceIndex: null,
19179
19290
  setSelectedSurfaceIndex: (index) => set((state2) => state2.selectedSurfaceIndex === index ? { selectedSurfaceIndex: null } : { selectedSurfaceIndex: index }),
19291
+ selectedFace: null,
19292
+ setSelectedFace: (face) => set((state2) => {
19293
+ const prev = state2.selectedFace;
19294
+ if (face && prev && prev.objectId === face.objectId && prev.carrierName !== null && prev.carrierName === face.carrierName) {
19295
+ return { selectedFace: null };
19296
+ }
19297
+ return { selectedFace: face };
19298
+ }),
19299
+ selectedEdge: null,
19300
+ setSelectedEdge: (edge) => set((state2) => {
19301
+ const prev = state2.selectedEdge;
19302
+ if (edge && prev && prev.objectId === edge.objectId && prev.faceA === edge.faceA && prev.faceB === edge.faceB) {
19303
+ return { selectedEdge: null };
19304
+ }
19305
+ return { selectedEdge: edge };
19306
+ }),
19307
+ selectedVertex: null,
19308
+ setSelectedVertex: (vertex) => set((state2) => {
19309
+ const prev = state2.selectedVertex;
19310
+ if (vertex && prev && prev.objectId === vertex.objectId && prev.point[0] === vertex.point[0] && prev.point[1] === vertex.point[1] && prev.point[2] === vertex.point[2]) {
19311
+ return { selectedVertex: null };
19312
+ }
19313
+ return { selectedVertex: vertex };
19314
+ }),
19180
19315
  selectedSketchEntityId: null,
19181
19316
  setSelectedSketchEntityId: (id) => set((state2) => state2.selectedSketchEntityId === id ? { selectedSketchEntityId: null } : { selectedSketchEntityId: id }),
19182
19317
  hoveredJointName: null,
@@ -19214,6 +19349,13 @@ const useForgeStore = create((set, get) => ({
19214
19349
  });
19215
19350
  },
19216
19351
  clearMeasureSelections: () => set({ measureSelections: [] }),
19352
+ replaceLastMeasureSelection: (entity) => {
19353
+ set((s) => {
19354
+ const sels = s.measureSelections;
19355
+ if (sels.length === 0) return { measureSelections: [entity] };
19356
+ return { measureSelections: [...sels.slice(0, -1), entity] };
19357
+ });
19358
+ },
19217
19359
  measurements: [],
19218
19360
  addMeasurePoint: (pt2) => {
19219
19361
  const measurements = get().measurements;
@@ -19309,6 +19451,12 @@ const useForgeStore = create((set, get) => ({
19309
19451
  writeViewPreferences({ dimensionsVisible: nextDimensionsVisible });
19310
19452
  return { dimensionsVisible: nextDimensionsVisible };
19311
19453
  }),
19454
+ paramAnchorsVisible: initialViewPreferences.paramAnchorsVisible ?? true,
19455
+ toggleParamAnchors: () => set((s) => {
19456
+ const nextParamAnchorsVisible = !s.paramAnchorsVisible;
19457
+ writeViewPreferences({ paramAnchorsVisible: nextParamAnchorsVisible });
19458
+ return { paramAnchorsVisible: nextParamAnchorsVisible };
19459
+ }),
19312
19460
  attachmentsVisible: initialViewPreferences.attachmentsVisible ?? "none",
19313
19461
  setAttachmentsVisible: (mode) => {
19314
19462
  writeViewPreferences({ attachmentsVisible: mode });
@@ -19486,11 +19634,11 @@ const useForgeStore = create((set, get) => ({
19486
19634
  fileSystem.save(normalized, text).catch((e2) => console.error("Save failed:", e2));
19487
19635
  setTimeout(() => get().execute(), 0);
19488
19636
  },
19489
- importTextFiles: async (entries, options = {}) => {
19637
+ importTextFiles: async (entries2, options = {}) => {
19490
19638
  const { files, folders, activeFile } = get();
19491
19639
  const occupied = /* @__PURE__ */ new Set([...Object.keys(files), ...folders]);
19492
19640
  const imported = [];
19493
- for (const entry of entries) {
19641
+ for (const entry of entries2) {
19494
19642
  const targetPath = resolveImportedProjectPath(entry.path, options.targetFolder);
19495
19643
  if (!targetPath) continue;
19496
19644
  const path = uniquifyProjectPath(targetPath, occupied);
@@ -19729,8 +19877,9 @@ onServerAvailabilityChange((available) => {
19729
19877
  useForgeStore.setState({ serverAvailable: available });
19730
19878
  });
19731
19879
  const savedComputeTarget = initialViewPreferences.computeTarget;
19880
+ const savedServerBackend = initialViewPreferences.activeBackend === "sdf" ? "sdf" : "occt";
19732
19881
  if (savedComputeTarget === "server" && canUseServerCompute()) {
19733
- useForgeStore.setState({ activeBackend: "occt", computeTarget: "server" });
19882
+ useForgeStore.setState({ activeBackend: savedServerBackend, computeTarget: "server" });
19734
19883
  startServerPolling();
19735
19884
  }
19736
19885
  {
@@ -22930,6 +23079,51 @@ function DroneCrosshairPicker({
22930
23079
  }, [active, focusHitOrClear, gl.domElement, hideLabel, showHitInfo]);
22931
23080
  return null;
22932
23081
  }
23082
+ const ESCAPE_PRIORITY = {
23083
+ viewport: 20,
23084
+ popover: 50,
23085
+ modal: 100
23086
+ };
23087
+ let nextId = 1;
23088
+ let entries = [];
23089
+ let listening = false;
23090
+ function dispatchEscape(event) {
23091
+ if (event.key !== "Escape" || event.isComposing) return;
23092
+ const ordered = [...entries].sort((a2, b2) => b2.priority - a2.priority || b2.id - a2.id);
23093
+ for (const entry of ordered) {
23094
+ if (!entry.actionRef.current(event)) continue;
23095
+ event.preventDefault();
23096
+ event.stopPropagation();
23097
+ event.stopImmediatePropagation();
23098
+ return;
23099
+ }
23100
+ }
23101
+ function ensureEscapeListener() {
23102
+ if (listening || typeof window === "undefined") return;
23103
+ window.addEventListener("keydown", dispatchEscape, { capture: true });
23104
+ listening = true;
23105
+ }
23106
+ function removeEscapeListenerIfIdle() {
23107
+ if (!listening || entries.length > 0 || typeof window === "undefined") return;
23108
+ window.removeEventListener("keydown", dispatchEscape, { capture: true });
23109
+ listening = false;
23110
+ }
23111
+ function useEscapeAction(action, { active, label, priority = ESCAPE_PRIORITY.viewport }) {
23112
+ const actionRef = reactExports.useRef(action);
23113
+ reactExports.useEffect(() => {
23114
+ actionRef.current = action;
23115
+ }, [action]);
23116
+ reactExports.useEffect(() => {
23117
+ if (!active) return;
23118
+ ensureEscapeListener();
23119
+ const entry = { id: nextId++, label, priority, actionRef };
23120
+ entries = [...entries, entry];
23121
+ return () => {
23122
+ entries = entries.filter((candidate) => candidate !== entry);
23123
+ removeEscapeListenerIfIdle();
23124
+ };
23125
+ }, [active, label, priority]);
23126
+ }
22933
23127
  const DEFAULT_DRONE_CAMERA_STATUS = {
22934
23128
  speed: 0,
22935
23129
  pointerLocked: false,
@@ -23028,6 +23222,13 @@ function DroneCameraController({
23028
23222
  const pointerLockedRef = reactExports.useRef(false);
23029
23223
  const lastStatusRef = reactExports.useRef(DEFAULT_DRONE_CAMERA_STATUS);
23030
23224
  const handledKeyboardEventsRef = reactExports.useRef(/* @__PURE__ */ new WeakSet());
23225
+ const handleEscape = reactExports.useCallback(() => {
23226
+ if (!active) return false;
23227
+ if (onEscape == null ? void 0 : onEscape()) return true;
23228
+ onExit();
23229
+ return true;
23230
+ }, [active, onEscape, onExit]);
23231
+ useEscapeAction(handleEscape, { active, label: "Fly camera", priority: ESCAPE_PRIORITY.viewport + 5 });
23031
23232
  const publishStatus = reactExports.useCallback(
23032
23233
  (patch) => {
23033
23234
  const next = { ...lastStatusRef.current, ...patch };
@@ -23143,11 +23344,6 @@ function DroneCameraController({
23143
23344
  if (handledKeyboardEventsRef.current.has(event)) return;
23144
23345
  handledKeyboardEventsRef.current.add(event);
23145
23346
  if (event.key === "Escape") {
23146
- consumeFlightKey(event);
23147
- if (onEscape == null ? void 0 : onEscape()) {
23148
- return;
23149
- }
23150
- onExit();
23151
23347
  return;
23152
23348
  }
23153
23349
  const code = flightCodeFromEvent(event);
@@ -23251,19 +23447,7 @@ function DroneCameraController({
23251
23447
  publishStatus(DEFAULT_DRONE_CAMERA_STATUS);
23252
23448
  recordDroneDebug({ active: false, keys: [], lastSpeed: 0, pointerLocked: false });
23253
23449
  };
23254
- }, [
23255
- active,
23256
- applyLook,
23257
- camera,
23258
- controlsRef,
23259
- gl.domElement,
23260
- onEscape,
23261
- onExit,
23262
- onInteractionChange,
23263
- publishStatus,
23264
- rotateByMouse,
23265
- stepFlight
23266
- ]);
23450
+ }, [active, applyLook, camera, controlsRef, gl.domElement, onInteractionChange, publishStatus, rotateByMouse, stepFlight]);
23267
23451
  return null;
23268
23452
  }
23269
23453
  const panelStyle$1 = {
@@ -23499,7 +23683,7 @@ function moveOrbitTargetToPoint(camera, controls, target) {
23499
23683
  }
23500
23684
  const MIDDLE_BUTTON = 1;
23501
23685
  const DOUBLE_CLICK_MAX_MS = 500;
23502
- const CLICK_DRAG_TOLERANCE_PX = 5;
23686
+ const CLICK_DRAG_TOLERANCE_PX$1 = 5;
23503
23687
  const DOUBLE_CLICK_DISTANCE_TOLERANCE_PX = 8;
23504
23688
  function distanceSq(a2, b2) {
23505
23689
  return (a2.clientX - b2.clientX) ** 2 + (a2.clientY - b2.clientY) ** 2;
@@ -23535,7 +23719,7 @@ function OrbitTargetPicker({ active, controlsRef }) {
23535
23719
  previousClickRef.current = null;
23536
23720
  if (!active) return;
23537
23721
  const element = gl.domElement;
23538
- const dragToleranceSq = CLICK_DRAG_TOLERANCE_PX ** 2;
23722
+ const dragToleranceSq = CLICK_DRAG_TOLERANCE_PX$1 ** 2;
23539
23723
  const doubleDistanceToleranceSq = DOUBLE_CLICK_DISTANCE_TOLERANCE_PX ** 2;
23540
23724
  const onPointerDown = (event) => {
23541
23725
  if (event.button !== MIDDLE_BUTTON) return;
@@ -23700,7 +23884,7 @@ function OrbitTargetPulseLayer() {
23700
23884
  class ConstructionHistoryWorkerClient {
23701
23885
  constructor(workerFactory = () => new Worker(new URL(
23702
23886
  /* @vite-ignore */
23703
- "/assets/constructionHistoryWorker-cTHWRJEi.js",
23887
+ "/assets/constructionHistoryWorker-BuZgc606.js",
23704
23888
  import.meta.url
23705
23889
  ), { type: "module" })) {
23706
23890
  __publicField(this, "worker", null);
@@ -26212,7 +26396,7 @@ function generateReportInWorker(options) {
26212
26396
  return new Promise((resolve2, reject) => {
26213
26397
  const worker = new Worker(new URL(
26214
26398
  /* @vite-ignore */
26215
- "/assets/reportWorker-Cb5eyM7D.js",
26399
+ "/assets/reportWorker-kg065BVL.js",
26216
26400
  import.meta.url
26217
26401
  ), { type: "module" });
26218
26402
  const cleanup = () => {
@@ -27565,6 +27749,14 @@ function AnimationBar() {
27565
27749
  const [historySpeedInput, setHistorySpeedInput] = reactExports.useState(() => formatHistorySpeedInput(historySpeed));
27566
27750
  const [editingHistorySpeed, setEditingHistorySpeed] = reactExports.useState(false);
27567
27751
  const [copiedTraceId, setCopiedTraceId] = reactExports.useState(null);
27752
+ useEscapeAction(
27753
+ () => {
27754
+ if (!animationMode) return false;
27755
+ if (!recording) exit();
27756
+ return true;
27757
+ },
27758
+ { active: Boolean(animationMode), label: "Animation mode", priority: ESCAPE_PRIORITY.viewport + 4 }
27759
+ );
27568
27760
  const elapsed = useElapsedTime(recording);
27569
27761
  reactExports.useEffect(() => {
27570
27762
  if (!editingHistorySpeed) {
@@ -27574,12 +27766,6 @@ function AnimationBar() {
27574
27766
  const handleKeyDown = reactExports.useCallback(
27575
27767
  (e2) => {
27576
27768
  if (!animationMode) return;
27577
- if (e2.key === "Escape") {
27578
- e2.preventDefault();
27579
- if (recording) return;
27580
- exit();
27581
- return;
27582
- }
27583
27769
  if (animationMode === "construction") {
27584
27770
  switch (e2.key) {
27585
27771
  case " ":
@@ -27597,7 +27783,7 @@ function AnimationBar() {
27597
27783
  }
27598
27784
  }
27599
27785
  },
27600
- [animationMode, recording, historyCurrentStep, toggleHistoryPlayback, setHistoryStep, exit]
27786
+ [animationMode, recording, historyCurrentStep, toggleHistoryPlayback, setHistoryStep]
27601
27787
  );
27602
27788
  reactExports.useEffect(() => {
27603
27789
  window.addEventListener("keydown", handleKeyDown);
@@ -28497,20 +28683,13 @@ function TrajectoryTimeline() {
28497
28683
  const trajectoryDuration = useForgeStore((s) => s.trajectoryDuration);
28498
28684
  const exitTrajectoryMode = useForgeStore((s) => s.exitTrajectoryMode);
28499
28685
  const setTrajectoryPresetParams = useForgeStore((s) => s.setTrajectoryPresetParams);
28500
- const handleKeyDown = reactExports.useCallback(
28501
- (e2) => {
28502
- if (!trajectoryMode) return;
28503
- if (e2.key === "Escape") {
28504
- e2.preventDefault();
28505
- exitTrajectoryMode();
28506
- }
28686
+ useEscapeAction(
28687
+ () => {
28688
+ exitTrajectoryMode();
28689
+ return true;
28507
28690
  },
28508
- [trajectoryMode, exitTrajectoryMode]
28691
+ { active: Boolean(trajectoryMode), label: "Trajectory mode", priority: ESCAPE_PRIORITY.viewport + 4 }
28509
28692
  );
28510
- reactExports.useEffect(() => {
28511
- window.addEventListener("keydown", handleKeyDown);
28512
- return () => window.removeEventListener("keydown", handleKeyDown);
28513
- }, [handleKeyDown]);
28514
28693
  if (!trajectoryMode) return null;
28515
28694
  const isActive = trajectoryRecording || trajectoryPreviewing;
28516
28695
  const presetConfig = TRAJECTORY_PRESETS.find((p2) => p2.id === trajectoryPreset);
@@ -29671,7 +29850,7 @@ const PHASE_CONFIG = {
29671
29850
  const PHASE_ORDER = ["kernel-init", "evaluating", "serializing"];
29672
29851
  function formatEvaluationBackendLabel(activeBackend, computeTarget) {
29673
29852
  const backend = activeBackend === "occt" ? "OCCT" : activeBackend === "manifold" ? "Manifold" : activeBackend === "truck" ? "Truck" : activeBackend === "sdf" ? "SDF" : "kernel";
29674
- return computeTarget === "server" ? "Server OCCT" : `Local ${backend}`;
29853
+ return computeTarget === "server" ? `Server ${backend}` : `Local ${backend}`;
29675
29854
  }
29676
29855
  function EvaluationIndicator({
29677
29856
  phase,
@@ -29980,6 +30159,185 @@ function LedgerMetric({ label, value }) {
29980
30159
  }
29981
30160
  );
29982
30161
  }
30162
+ const QUANT = 1e4;
30163
+ const q = (v) => Math.round(v * QUANT);
30164
+ const vertKey = (pos, i) => `${q(pos.getX(i))},${q(pos.getY(i))},${q(pos.getZ(i))}`;
30165
+ const edgeKey = (a2, b2) => a2 < b2 ? `${a2}|${b2}` : `${b2}|${a2}`;
30166
+ function buildEdgeAdjacency(positions, triCount) {
30167
+ const edgeToTris = /* @__PURE__ */ new Map();
30168
+ for (let t2 = 0; t2 < triCount; t2++) {
30169
+ const base = t2 * 3;
30170
+ const v0 = vertKey(positions, base);
30171
+ const v12 = vertKey(positions, base + 1);
30172
+ const v22 = vertKey(positions, base + 2);
30173
+ for (const ek of [edgeKey(v0, v12), edgeKey(v12, v22), edgeKey(v22, v0)]) {
30174
+ let list = edgeToTris.get(ek);
30175
+ if (!list) {
30176
+ list = [];
30177
+ edgeToTris.set(ek, list);
30178
+ }
30179
+ list.push(t2);
30180
+ }
30181
+ }
30182
+ return edgeToTris;
30183
+ }
30184
+ function floodComponent(positions, edgeToTris, startTriIndex, accept) {
30185
+ const visited = /* @__PURE__ */ new Set();
30186
+ const queue = [startTriIndex];
30187
+ visited.add(startTriIndex);
30188
+ while (queue.length > 0) {
30189
+ const t2 = queue.pop();
30190
+ const base = t2 * 3;
30191
+ const v0 = vertKey(positions, base);
30192
+ const v12 = vertKey(positions, base + 1);
30193
+ const v22 = vertKey(positions, base + 2);
30194
+ for (const ek of [edgeKey(v0, v12), edgeKey(v12, v22), edgeKey(v22, v0)]) {
30195
+ const neighbors = edgeToTris.get(ek);
30196
+ if (!neighbors) continue;
30197
+ for (const n of neighbors) {
30198
+ if (visited.has(n)) continue;
30199
+ if (!accept(n)) continue;
30200
+ visited.add(n);
30201
+ queue.push(n);
30202
+ }
30203
+ }
30204
+ }
30205
+ return Array.from(visited);
30206
+ }
30207
+ function summarizeRegion(positions, normals, indices) {
30208
+ let totalArea = 0;
30209
+ const centroid = new Vector3();
30210
+ const weightedNormal = new Vector3();
30211
+ const tmpA = new Vector3();
30212
+ const tmpB = new Vector3();
30213
+ const tmpC = new Vector3();
30214
+ for (const t2 of indices) {
30215
+ const base = t2 * 3;
30216
+ tmpA.set(positions.getX(base), positions.getY(base), positions.getZ(base));
30217
+ tmpB.set(positions.getX(base + 1), positions.getY(base + 1), positions.getZ(base + 1));
30218
+ tmpC.set(positions.getX(base + 2), positions.getY(base + 2), positions.getZ(base + 2));
30219
+ const ab = tmpB.clone().sub(tmpA);
30220
+ const ac = tmpC.clone().sub(tmpA);
30221
+ const cross = ab.cross(ac);
30222
+ const triArea = cross.length() * 0.5;
30223
+ totalArea += triArea;
30224
+ const triCenter = tmpA.clone().add(tmpB).add(tmpC).multiplyScalar(1 / 3);
30225
+ centroid.add(triCenter.multiplyScalar(triArea));
30226
+ if (cross.lengthSq() > 0) weightedNormal.add(cross.multiplyScalar(0.5));
30227
+ }
30228
+ if (totalArea > 0) centroid.multiplyScalar(1 / totalArea);
30229
+ let normal;
30230
+ if (weightedNormal.lengthSq() > 1e-12) {
30231
+ normal = weightedNormal.normalize();
30232
+ } else if (normals) {
30233
+ const si = indices[0] * 3;
30234
+ normal = new Vector3(normals.getX(si), normals.getY(si), normals.getZ(si)).normalize();
30235
+ } else {
30236
+ normal = new Vector3(0, 0, 1);
30237
+ }
30238
+ return { normal, center: centroid, area: totalArea };
30239
+ }
30240
+ function getFaceRegion(geometry, startTriIndex, options = {}) {
30241
+ const positions = geometry.getAttribute("position");
30242
+ const normals = geometry.getAttribute("normal") ?? null;
30243
+ const triCount = positions.count / 3;
30244
+ const edgeToTris = buildEdgeAdjacency(positions, triCount);
30245
+ const userData = geometry.userData;
30246
+ const faceIds = userData.forgeTriangleFaceIds;
30247
+ const faceIdNames = userData.forgeFaceIdNames;
30248
+ if (faceIds && faceIdNames && startTriIndex < faceIds.length) {
30249
+ const hitFaceId = faceIds[startTriIndex];
30250
+ if (hitFaceId >= 0) {
30251
+ const indices2 = floodComponent(positions, edgeToTris, startTriIndex, (n) => faceIds[n] === hitFaceId);
30252
+ const summary2 = summarizeRegion(positions, normals, indices2);
30253
+ const rawName = faceIdNames[hitFaceId] ?? null;
30254
+ const carrierName = rawName && rawName !== UNIDENTIFIED_FACE_NAME ? rawName : null;
30255
+ return { triangleIndices: indices2, ...summary2, carrierName };
30256
+ }
30257
+ }
30258
+ const normalTolerance = options.normalTolerance ?? 0.9995;
30259
+ const si = startTriIndex * 3;
30260
+ const startNormal = normals ? new Vector3(normals.getX(si), normals.getY(si), normals.getZ(si)) : new Vector3(0, 0, 1);
30261
+ const indices = floodComponent(positions, edgeToTris, startTriIndex, (n) => {
30262
+ if (!normals) return false;
30263
+ const ni = n * 3;
30264
+ const nNormal = new Vector3(normals.getX(ni), normals.getY(ni), normals.getZ(ni));
30265
+ return startNormal.dot(nNormal) >= normalTolerance;
30266
+ });
30267
+ const summary = summarizeRegion(positions, normals, indices);
30268
+ return { triangleIndices: indices, normal: startNormal.clone().normalize(), center: summary.center, area: summary.area, carrierName: null };
30269
+ }
30270
+ function buildFaceHighlightGeometry(sourceGeometry, triangleIndices) {
30271
+ const srcPos = sourceGeometry.getAttribute("position");
30272
+ const count = triangleIndices.length * 9;
30273
+ const positions = new Float32Array(count);
30274
+ for (let i = 0; i < triangleIndices.length; i++) {
30275
+ const base = triangleIndices[i] * 3;
30276
+ const out = i * 9;
30277
+ for (let v = 0; v < 3; v++) {
30278
+ positions[out + v * 3] = srcPos.getX(base + v);
30279
+ positions[out + v * 3 + 1] = srcPos.getY(base + v);
30280
+ positions[out + v * 3 + 2] = srcPos.getZ(base + v);
30281
+ }
30282
+ }
30283
+ const geo = new BufferGeometry();
30284
+ geo.setAttribute("position", new BufferAttribute(positions, 3));
30285
+ return geo;
30286
+ }
30287
+ function closestOnSegment(p2, a2, b2, out) {
30288
+ const abx = b2.x - a2.x;
30289
+ const aby = b2.y - a2.y;
30290
+ const abz = b2.z - a2.z;
30291
+ const abLenSq = abx * abx + aby * aby + abz * abz;
30292
+ let t2 = abLenSq > 0 ? ((p2.x - a2.x) * abx + (p2.y - a2.y) * aby + (p2.z - a2.z) * abz) / abLenSq : 0;
30293
+ if (t2 < 0) t2 = 0;
30294
+ else if (t2 > 1) t2 = 1;
30295
+ out.set(a2.x + abx * t2, a2.y + aby * t2, a2.z + abz * t2);
30296
+ return out.distanceToSquared(p2);
30297
+ }
30298
+ function getNearestEdge(geometry, localPoint, maxDistance) {
30299
+ const edges = getBakedEdges(geometry);
30300
+ if (edges.length === 0) return null;
30301
+ const maxSq = maxDistance * maxDistance;
30302
+ let best = null;
30303
+ let bestSq = maxSq;
30304
+ const a2 = new Vector3();
30305
+ const b2 = new Vector3();
30306
+ const closest = new Vector3();
30307
+ for (const edge of edges) {
30308
+ const pts = edge.points;
30309
+ for (let i = 0; i + 5 < pts.length; i += 3) {
30310
+ a2.set(pts[i], pts[i + 1], pts[i + 2]);
30311
+ b2.set(pts[i + 3], pts[i + 4], pts[i + 5]);
30312
+ const dSq = closestOnSegment(localPoint, a2, b2, closest);
30313
+ if (dSq < bestSq) {
30314
+ bestSq = dSq;
30315
+ best = { edge, point: closest.clone(), distance: Math.sqrt(dSq) };
30316
+ }
30317
+ }
30318
+ }
30319
+ return best;
30320
+ }
30321
+ function getNearestVertex(geometry, localPoint, maxDistance) {
30322
+ const edges = getBakedEdges(geometry);
30323
+ if (edges.length === 0) return null;
30324
+ const maxSq = maxDistance * maxDistance;
30325
+ let best = null;
30326
+ let bestSq = maxSq;
30327
+ const v = new Vector3();
30328
+ for (const edge of edges) {
30329
+ const pts = edge.points;
30330
+ for (let i = 0; i + 2 < pts.length; i += 3) {
30331
+ v.set(pts[i], pts[i + 1], pts[i + 2]);
30332
+ const dSq = v.distanceToSquared(localPoint);
30333
+ if (dSq < bestSq) {
30334
+ bestSq = dSq;
30335
+ best = { point: v.clone(), distance: Math.sqrt(dSq) };
30336
+ }
30337
+ }
30338
+ }
30339
+ return best;
30340
+ }
29983
30341
  const PREVIEW_RENDER_ORDER_STEP = 1;
29984
30342
  const HATCH_DIRECTION_A = new Vector2(Math.cos(MathUtils.degToRad(35)), Math.sin(MathUtils.degToRad(35)));
29985
30343
  const HATCH_DIRECTION_B = new Vector2(Math.cos(MathUtils.degToRad(125)), Math.sin(MathUtils.degToRad(125)));
@@ -31315,6 +31673,9 @@ function ZebraInspectionMaterial({ clippingPlanes }) {
31315
31673
  }
31316
31674
  const EMPTY_CLIPPING_PLANES$1 = [];
31317
31675
  const EMPTY_SECTION_PLANES$1 = [];
31676
+ const SELECTED_FACE_HIGHLIGHT_COLOR = "#ffa040";
31677
+ const SELECTED_EDGE_HIGHLIGHT_COLOR = "#3fd8ff";
31678
+ const SELECTED_VERTEX_HIGHLIGHT_COLOR = "#ffffff";
31318
31679
  function scanCellKey(ix, iy, iz) {
31319
31680
  return `${ix}:${iy}:${iz}`;
31320
31681
  }
@@ -31505,6 +31866,9 @@ function ForgeObject({
31505
31866
  sectionPlanes,
31506
31867
  sectionPreviewRenderOrderBase,
31507
31868
  debugHighlightColor,
31869
+ selectedFaceTriangleIndices,
31870
+ selectedEdgePoints,
31871
+ selectedVertexPoint,
31508
31872
  onPointerEnter,
31509
31873
  onPointerMove,
31510
31874
  onPointerLeave,
@@ -31546,7 +31910,7 @@ function ForgeObject({
31546
31910
  };
31547
31911
  }
31548
31912
  }, [obj.shape, wantsDirectSdf]);
31549
- const isScalarInspect = inspectChannel === "thickness" || inspectChannel === "roughness";
31913
+ const isScalarInspect = inspectChannel === "thickness" || inspectChannel === "throughThickness" || inspectChannel === "roughness";
31550
31914
  const isScalarScan = isScalarInspect && inspectDisplayMode === "scan";
31551
31915
  const inspectPointGeo = reactExports.useMemo(() => {
31552
31916
  if (!inspectPointCloud) return null;
@@ -31577,6 +31941,7 @@ function ForgeObject({
31577
31941
  geometry.setAttribute("position", new BufferAttribute(inspectScalarSurface.positions, 3));
31578
31942
  geometry.setAttribute("normal", new BufferAttribute(inspectScalarSurface.normals, 3));
31579
31943
  geometry.setAttribute("aValue", new BufferAttribute(inspectScalarSurface.aValue, 1));
31944
+ geometry.setAttribute("uv", new BufferAttribute(inspectScalarSurface.uvs ?? new Float32Array(vertexCount * 2), 2));
31580
31945
  geometry.setIndex(new BufferAttribute(inspectScalarSurface.index, 1));
31581
31946
  try {
31582
31947
  geometry.boundsTree = MeshBVH.deserialize(
@@ -31597,14 +31962,24 @@ function ForgeObject({
31597
31962
  if (!inspectScalarSurface || !effectiveColorScale) return null;
31598
31963
  return makeColorScaleTexture(colorScaleLUT(effectiveColorScale.colormap));
31599
31964
  }, [inspectScalarSurface, effectiveColorScale == null ? void 0 : effectiveColorScale.colormap]);
31965
+ const inspectScalarValueTexture = reactExports.useMemo(() => {
31966
+ if (!(inspectScalarSurface == null ? void 0 : inspectScalarSurface.textureValues) || !inspectScalarSurface.textureWidth || !inspectScalarSurface.textureHeight) return null;
31967
+ return makeScalarValueTexture(
31968
+ inspectScalarSurface.textureValues,
31969
+ inspectScalarSurface.textureWidth,
31970
+ inspectScalarSurface.textureHeight
31971
+ );
31972
+ }, [inspectScalarSurface]);
31600
31973
  const inspectScalarMaterialRef = reactExports.useRef(null);
31601
31974
  const invalidate2 = useThree((s) => s.invalidate);
31602
31975
  const inspectScalarUniforms = reactExports.useMemo(() => {
31603
31976
  if (!inspectColormapTexture || !effectiveColorScale) return null;
31604
31977
  return makeInspectScalarUniforms({
31605
31978
  colorScale: inspectColormapTexture,
31979
+ scalarTexture: inspectScalarValueTexture,
31606
31980
  domainMin: effectiveColorScale.domainMin,
31607
31981
  domainMax: effectiveColorScale.domainMax,
31982
+ colorScaleReversed: effectiveColorScale.reversed === true,
31608
31983
  quantizeBands: (inspectScalarParams == null ? void 0 : inspectScalarParams.quantizeBands) ?? 0,
31609
31984
  isoEnabled: (inspectScalarParams == null ? void 0 : inspectScalarParams.isolinesEnabled) ?? false,
31610
31985
  isoSpacing: (inspectScalarParams == null ? void 0 : inspectScalarParams.isolineSpacing) ?? 0,
@@ -31612,7 +31987,7 @@ function ForgeObject({
31612
31987
  criticalThreshold: (inspectScalarParams == null ? void 0 : inspectScalarParams.criticalThreshold) ?? 0,
31613
31988
  shadingEnabled: (inspectScalarParams == null ? void 0 : inspectScalarParams.shadingEnabled) ?? true
31614
31989
  });
31615
- }, [inspectColormapTexture]);
31990
+ }, [inspectColormapTexture, inspectScalarValueTexture]);
31616
31991
  const wantsScanProxy = Boolean(
31617
31992
  settings.visible && (inspectChannel === "none" && (renderStyle === "scan" || renderStyle === "matrix") || isScalarScan)
31618
31993
  );
@@ -31623,6 +31998,40 @@ function ForgeObject({
31623
31998
  const shell = createScanAnalysisColorGeometry(scanProxy.geometries.shell, inspectPointCloud, scanProxy.grid, matrix);
31624
31999
  return shell ? { shell } : null;
31625
32000
  }, [inspectPointCloud, isScalarScan, matrix, scanProxy]);
32001
+ const selectedFaceHighlightGeo = reactExports.useMemo(() => {
32002
+ if (!solidGeo || !selectedFaceTriangleIndices || selectedFaceTriangleIndices.length === 0) return null;
32003
+ return buildFaceHighlightGeometry(solidGeo, selectedFaceTriangleIndices);
32004
+ }, [solidGeo, selectedFaceTriangleIndices]);
32005
+ reactExports.useEffect(() => {
32006
+ return () => {
32007
+ selectedFaceHighlightGeo == null ? void 0 : selectedFaceHighlightGeo.dispose();
32008
+ };
32009
+ }, [selectedFaceHighlightGeo]);
32010
+ const selectedEdgeGeo = reactExports.useMemo(() => {
32011
+ if (!selectedEdgePoints || selectedEdgePoints.length < 6) return null;
32012
+ const segCount = selectedEdgePoints.length / 3 - 1;
32013
+ const positions = new Float32Array(segCount * 6);
32014
+ for (let i = 0; i < segCount; i++) {
32015
+ const a2 = i * 3;
32016
+ positions.set(selectedEdgePoints.slice(a2, a2 + 3), i * 6);
32017
+ positions.set(selectedEdgePoints.slice(a2 + 3, a2 + 6), i * 6 + 3);
32018
+ }
32019
+ const geo = new BufferGeometry();
32020
+ geo.setAttribute("position", new BufferAttribute(positions, 3));
32021
+ return geo;
32022
+ }, [selectedEdgePoints]);
32023
+ reactExports.useEffect(() => {
32024
+ return () => {
32025
+ selectedEdgeGeo == null ? void 0 : selectedEdgeGeo.dispose();
32026
+ };
32027
+ }, [selectedEdgeGeo]);
32028
+ const selectedVertexMarkerRadius = reactExports.useMemo(() => {
32029
+ var _a4;
32030
+ if (!selectedVertexPoint || !solidGeo) return 0;
32031
+ if (!solidGeo.boundingSphere) solidGeo.computeBoundingSphere();
32032
+ const r2 = ((_a4 = solidGeo.boundingSphere) == null ? void 0 : _a4.radius) ?? 1;
32033
+ return r2 * 0.02;
32034
+ }, [selectedVertexPoint, solidGeo]);
31626
32035
  reactExports.useEffect(() => {
31627
32036
  return () => {
31628
32037
  solidGeo == null ? void 0 : solidGeo.dispose();
@@ -31649,11 +32058,17 @@ function ForgeObject({
31649
32058
  inspectColormapTexture == null ? void 0 : inspectColormapTexture.dispose();
31650
32059
  };
31651
32060
  }, [inspectColormapTexture]);
32061
+ reactExports.useEffect(() => {
32062
+ return () => {
32063
+ inspectScalarValueTexture == null ? void 0 : inspectScalarValueTexture.dispose();
32064
+ };
32065
+ }, [inspectScalarValueTexture]);
31652
32066
  reactExports.useEffect(() => {
31653
32067
  if (!inspectScalarUniforms || !effectiveColorScale || !inspectScalarParams) return;
31654
32068
  updateInspectScalarUniforms(inspectScalarUniforms, {
31655
32069
  domainMin: effectiveColorScale.domainMin,
31656
32070
  domainMax: effectiveColorScale.domainMax,
32071
+ colorScaleReversed: effectiveColorScale.reversed === true,
31657
32072
  quantizeBands: inspectScalarParams.quantizeBands,
31658
32073
  isoEnabled: inspectScalarParams.isolinesEnabled,
31659
32074
  isoSpacing: inspectScalarParams.isolineSpacing,
@@ -32057,7 +32472,25 @@ function ForgeObject({
32057
32472
  polygonOffsetUnits: -1
32058
32473
  }
32059
32474
  ) }),
32060
- debugHighlightColor && edgesGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: edgesGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx("lineBasicMaterial", { color: debugHighlightColor, linewidth: 2, depthTest: false }) })
32475
+ debugHighlightColor && edgesGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: edgesGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx("lineBasicMaterial", { color: debugHighlightColor, linewidth: 2, depthTest: false }) }),
32476
+ selectedFaceHighlightGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry: selectedFaceHighlightGeo, raycast: () => null, renderOrder: 11, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
32477
+ "meshBasicMaterial",
32478
+ {
32479
+ color: SELECTED_FACE_HIGHLIGHT_COLOR,
32480
+ transparent: true,
32481
+ opacity: 0.45,
32482
+ side: DoubleSide,
32483
+ depthTest: false,
32484
+ polygonOffset: true,
32485
+ polygonOffsetFactor: -2,
32486
+ polygonOffsetUnits: -2
32487
+ }
32488
+ ) }),
32489
+ selectedEdgeGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: selectedEdgeGeo, raycast: () => null, renderOrder: 12, children: /* @__PURE__ */ jsxRuntimeExports.jsx("lineBasicMaterial", { color: SELECTED_EDGE_HIGHLIGHT_COLOR, linewidth: 2, depthTest: false, toneMapped: false }) }),
32490
+ selectedVertexPoint && selectedVertexMarkerRadius > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { position: selectedVertexPoint, raycast: () => null, renderOrder: 13, children: [
32491
+ /* @__PURE__ */ jsxRuntimeExports.jsx("sphereGeometry", { args: [selectedVertexMarkerRadius, 12, 12] }),
32492
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: SELECTED_VERTEX_HIGHLIGHT_COLOR, depthTest: false, toneMapped: false })
32493
+ ] })
32061
32494
  ]
32062
32495
  }
32063
32496
  );
@@ -32344,7 +32777,7 @@ function HoveredJointOverlay({ state: state2, config }) {
32344
32777
  ] });
32345
32778
  }
32346
32779
  function scalarRamp(options, domainMin, domainMax, unit) {
32347
- var _a3;
32780
+ var _a3, _b2;
32348
32781
  const colormap = ((_a3 = options.colorScale) == null ? void 0 : _a3.colormap) ?? DEFAULT_COLORMAP;
32349
32782
  const midpoint = domainMin + (domainMax - domainMin) / 2;
32350
32783
  const unitSuffix = unit ? ` ${unit}` : "";
@@ -32352,7 +32785,7 @@ function scalarRamp(options, domainMin, domainMax, unit) {
32352
32785
  const critical = options.criticalValue;
32353
32786
  const criticalTick = critical != null && Number.isFinite(critical) && span > 0 ? { position: Math.max(0, Math.min(1, (critical - domainMin) / span)), label: `${fmt(critical)}${unitSuffix}` } : void 0;
32354
32787
  return {
32355
- colors: colorScaleHexStops(colormap),
32788
+ colors: colorScaleHexStops(colormap, 8, ((_b2 = options.colorScale) == null ? void 0 : _b2.reversed) === true),
32356
32789
  leftLabel: `${fmt(domainMin)}${unitSuffix}`,
32357
32790
  centerLabel: `${fmt(midpoint)}${unitSuffix}`,
32358
32791
  rightLabel: `${fmt(domainMax)}${unitSuffix}`,
@@ -32366,7 +32799,7 @@ function fmt(value) {
32366
32799
  return fixed.replace(/\.?0+$/, "");
32367
32800
  }
32368
32801
  function inspectionLegendDefinitionFor(channel, displayMode, legendOptions = {}) {
32369
- var _a3, _b2, _c, _d;
32802
+ var _a3, _b2, _c, _d, _e, _f;
32370
32803
  switch (channel) {
32371
32804
  case "mask":
32372
32805
  return {
@@ -32468,13 +32901,32 @@ function inspectionLegendDefinitionFor(channel, displayMode, legendOptions = {})
32468
32901
  }
32469
32902
  return {
32470
32903
  title: "Wall Thickness",
32471
- summary: "Color maps continuously from thinner to thicker material across the selected range.",
32904
+ summary: "Warm colors mark thinner material; cool colors mark thicker material.",
32905
+ ramp
32906
+ };
32907
+ }
32908
+ case "throughThickness": {
32909
+ const colorRange = legendOptions.thicknessColorRange ?? DEFAULT_THICKNESS_COLOR_RANGE;
32910
+ const domainMin = ((_c = legendOptions.colorScale) == null ? void 0 : _c.domainMin) ?? colorRange.min;
32911
+ const domainMax = ((_d = legendOptions.colorScale) == null ? void 0 : _d.domainMax) ?? colorRange.max;
32912
+ const unit = legendOptions.unitLabel ?? "mm";
32913
+ const ramp = scalarRamp(legendOptions, domainMin, domainMax, unit);
32914
+ if (displayMode === "scan") {
32915
+ return {
32916
+ title: "Minimum Solid Span Scan",
32917
+ summary: "Each scan box uses the same minimum solid span ramp as the heatmap.",
32918
+ ramp
32919
+ };
32920
+ }
32921
+ return {
32922
+ title: "Minimum Solid Span",
32923
+ summary: "Warm colors mark short through-material spans; cool colors mark more surrounding material.",
32472
32924
  ramp
32473
32925
  };
32474
32926
  }
32475
32927
  case "roughness": {
32476
- const domainMin = ((_c = legendOptions.colorScale) == null ? void 0 : _c.domainMin) ?? 0;
32477
- const domainMax = ((_d = legendOptions.colorScale) == null ? void 0 : _d.domainMax) ?? 1;
32928
+ const domainMin = ((_e = legendOptions.colorScale) == null ? void 0 : _e.domainMin) ?? 0;
32929
+ const domainMax = ((_f = legendOptions.colorScale) == null ? void 0 : _f.domainMax) ?? 1;
32478
32930
  const unit = legendOptions.unitLabel ?? "deg";
32479
32931
  const ramp = scalarRamp(legendOptions, domainMin, domainMax, unit);
32480
32932
  if (displayMode === "scan") {
@@ -32682,7 +33134,7 @@ function rangeDraftValue(value) {
32682
33134
  function rangeKey(range) {
32683
33135
  return `${range.min}:${range.max}`;
32684
33136
  }
32685
- function clamp(value, min, max2) {
33137
+ function clamp$1(value, min, max2) {
32686
33138
  return Math.max(min, Math.min(max2, value));
32687
33139
  }
32688
33140
  function snapSliderValue(value) {
@@ -32707,20 +33159,20 @@ function DualRangeSlider({
32707
33159
  onCommit
32708
33160
  }) {
32709
33161
  const trackRef = reactExports.useRef(null);
32710
- const minPercent = clamp(range.min, 0, sliderMax) / sliderMax * 100;
32711
- const maxPercent = clamp(range.max, 0, sliderMax) / sliderMax * 100;
33162
+ const minPercent = clamp$1(range.min, 0, sliderMax) / sliderMax * 100;
33163
+ const maxPercent = clamp$1(range.max, 0, sliderMax) / sliderMax * 100;
32712
33164
  const valueFromPointer = (event) => {
32713
33165
  var _a3;
32714
33166
  const rect = (_a3 = trackRef.current) == null ? void 0 : _a3.getBoundingClientRect();
32715
33167
  if (!rect || rect.width <= 0) return null;
32716
- const ratio = clamp((event.clientX - rect.left) / rect.width, 0, 1);
33168
+ const ratio = clamp$1((event.clientX - rect.left) / rect.width, 0, 1);
32717
33169
  return snapSliderValue(ratio * sliderMax);
32718
33170
  };
32719
33171
  const updateThumb = (thumb, value) => {
32720
33172
  if (thumb === "min") {
32721
- onDraftChange({ min: clamp(value, 0, range.max - MIN_RANGE_SPAN), max: range.max });
33173
+ onDraftChange({ min: clamp$1(value, 0, range.max - MIN_RANGE_SPAN), max: range.max });
32722
33174
  } else {
32723
- onDraftChange({ min: range.min, max: Math.max(range.min + MIN_RANGE_SPAN, clamp(value, MIN_RANGE_SPAN, sliderMax)) });
33175
+ onDraftChange({ min: range.min, max: Math.max(range.min + MIN_RANGE_SPAN, clamp$1(value, MIN_RANGE_SPAN, sliderMax)) });
32724
33176
  }
32725
33177
  };
32726
33178
  const handlePointerMove = (thumb, event) => {
@@ -32960,7 +33412,7 @@ function InspectionLegend({
32960
33412
  criticalValue
32961
33413
  });
32962
33414
  const swatches = liveSwatches && liveSwatches.length > 0 ? liveSwatches : definition == null ? void 0 : definition.swatches;
32963
- const sliderGradient = colorScaleHexStops((colorScale == null ? void 0 : colorScale.colormap) ?? "viridis");
33415
+ const sliderGradient = colorScaleHexStops((colorScale == null ? void 0 : colorScale.colormap) ?? DEFAULT_COLORMAP, 8, (colorScale == null ? void 0 : colorScale.reversed) === true);
32964
33416
  reactExports.useEffect(() => {
32965
33417
  var _a3;
32966
33418
  const parent = (_a3 = panelRef.current) == null ? void 0 : _a3.parentElement;
@@ -32973,7 +33425,7 @@ function InspectionLegend({
32973
33425
  }, [channel]);
32974
33426
  if (!definition) return null;
32975
33427
  const warning = warnings[0];
32976
- const showThicknessControls = channel === "thickness" && thicknessColorRange !== void 0 && onThicknessColorRangeChange !== void 0;
33428
+ const showThicknessControls = (channel === "thickness" || channel === "throughThickness") && thicknessColorRange !== void 0 && onThicknessColorRangeChange !== void 0;
32977
33429
  const swatchCount = (swatches == null ? void 0 : swatches.length) ?? 0;
32978
33430
  const hasScrollableSwatches = swatchCount > 10;
32979
33431
  const effectivePanelStyle = {
@@ -33066,212 +33518,263 @@ function LabeledAxes({ size = 50 }) {
33066
33518
  /* @__PURE__ */ jsxRuntimeExports.jsx(Html, { position: [0, 0, size + 3], center: true, style: labelStyle("#4488ff"), children: "Z" })
33067
33519
  ] });
33068
33520
  }
33069
- const MEASURE_COLORS = {
33070
- face: "#60b8ff",
33071
- // hover face — gentle blue
33072
- edge: "#ffffff",
33073
- // hover edge white
33074
- vertex: "#ffffff",
33075
- // hover vertex — white
33076
- selection: "#ffa040",
33077
- // both selections — warm amber
33078
- line: "#ffa040",
33079
- panelLabel: "#888",
33080
- panelValue: "#ffd060"
33081
- };
33082
- const QUANT = 1e4;
33083
- const q = (v) => Math.round(v * QUANT);
33084
- const vertKey = (pos, i) => `${q(pos.getX(i))},${q(pos.getY(i))},${q(pos.getZ(i))}`;
33085
- const edgeKey = (a2, b2) => a2 < b2 ? `${a2}|${b2}` : `${b2}|${a2}`;
33086
- function floodFillFace(geometry, startTriIndex, normalTolerance = 0.9995) {
33521
+ const SHARP_DOT = 0.9995;
33522
+ const CORNER_TURN_DEG = 35;
33523
+ const LEN_RATIO_SPLIT = 4;
33524
+ const CONTINUITY_LIMIT_DEG = 60;
33525
+ const CORNER_TURN_COS = Math.cos(CORNER_TURN_DEG * Math.PI / 180);
33526
+ const CONTINUITY_LIMIT_COS = Math.cos(CONTINUITY_LIMIT_DEG * Math.PI / 180);
33527
+ function extractFeatureEdges(geometry) {
33087
33528
  const positions = geometry.getAttribute("position");
33088
33529
  const normals = geometry.getAttribute("normal");
33530
+ if (!positions) return [];
33089
33531
  const triCount = positions.count / 3;
33090
- const si = startTriIndex * 3;
33091
- const startNormal = new Vector3(normals.getX(si), normals.getY(si), normals.getZ(si));
33092
- const edgeToTris = /* @__PURE__ */ new Map();
33532
+ const data = /* @__PURE__ */ new Map();
33093
33533
  for (let t2 = 0; t2 < triCount; t2++) {
33094
33534
  const base = t2 * 3;
33095
- const v0 = vertKey(positions, base);
33096
- const v12 = vertKey(positions, base + 1);
33097
- const v22 = vertKey(positions, base + 2);
33098
- for (const ek of [edgeKey(v0, v12), edgeKey(v12, v22), edgeKey(v22, v0)]) {
33099
- let list = edgeToTris.get(ek);
33100
- if (!list) {
33101
- list = [];
33102
- edgeToTris.set(ek, list);
33103
- }
33104
- list.push(t2);
33105
- }
33106
- }
33107
- const visited = /* @__PURE__ */ new Set();
33108
- const queue = [startTriIndex];
33109
- visited.add(startTriIndex);
33110
- while (queue.length > 0) {
33111
- const t2 = queue.pop();
33112
- const base = t2 * 3;
33113
- const v0 = vertKey(positions, base);
33114
- const v12 = vertKey(positions, base + 1);
33115
- const v22 = vertKey(positions, base + 2);
33116
- for (const ek of [edgeKey(v0, v12), edgeKey(v12, v22), edgeKey(v22, v0)]) {
33117
- const neighbors = edgeToTris.get(ek);
33118
- if (!neighbors) continue;
33119
- for (const n of neighbors) {
33120
- if (visited.has(n)) continue;
33121
- const ni = n * 3;
33122
- const nNormal = new Vector3(normals.getX(ni), normals.getY(ni), normals.getZ(ni));
33123
- if (startNormal.dot(nNormal) >= normalTolerance) {
33124
- visited.add(n);
33125
- queue.push(n);
33126
- }
33535
+ const tn = normals ? new Vector3(normals.getX(base), normals.getY(base), normals.getZ(base)) : new Vector3(0, 0, 1);
33536
+ for (let e2 = 0; e2 < 3; e2++) {
33537
+ const i0 = base + e2;
33538
+ const i1 = base + (e2 + 1) % 3;
33539
+ const ek = edgeKey(vertKey(positions, i0), vertKey(positions, i1));
33540
+ let d = data.get(ek);
33541
+ if (!d) {
33542
+ d = {
33543
+ a: new Vector3().fromBufferAttribute(positions, i0),
33544
+ b: new Vector3().fromBufferAttribute(positions, i1),
33545
+ normals: []
33546
+ };
33547
+ data.set(ek, d);
33127
33548
  }
33549
+ d.normals.push(tn);
33128
33550
  }
33129
33551
  }
33130
- const indices = Array.from(visited);
33131
- let totalArea = 0;
33132
- const centroid = new Vector3();
33133
- const tmpA = new Vector3();
33134
- const tmpB = new Vector3();
33135
- const tmpC = new Vector3();
33136
- for (const t2 of indices) {
33137
- const base = t2 * 3;
33138
- tmpA.set(positions.getX(base), positions.getY(base), positions.getZ(base));
33139
- tmpB.set(positions.getX(base + 1), positions.getY(base + 1), positions.getZ(base + 1));
33140
- tmpC.set(positions.getX(base + 2), positions.getY(base + 2), positions.getZ(base + 2));
33141
- const ab = tmpB.clone().sub(tmpA);
33142
- const ac = tmpC.clone().sub(tmpA);
33143
- const triArea = ab.cross(ac).length() * 0.5;
33144
- totalArea += triArea;
33145
- const triCenter = tmpA.clone().add(tmpB).add(tmpC).multiplyScalar(1 / 3);
33146
- centroid.add(triCenter.multiplyScalar(triArea));
33552
+ const out = [];
33553
+ for (const d of data.values()) {
33554
+ const sharp = d.normals.length === 2 && d.normals[0].dot(d.normals[1]) < SHARP_DOT;
33555
+ const boundary = d.normals.length === 1;
33556
+ if (sharp || boundary) out.push({ a: d.a, b: d.b });
33147
33557
  }
33148
- if (totalArea > 0) centroid.multiplyScalar(1 / totalArea);
33149
- return { triangleIndices: indices, normal: startNormal.clone(), center: centroid, area: totalArea };
33558
+ return out;
33150
33559
  }
33151
- function buildFaceHighlightGeometry(sourceGeometry, triangleIndices) {
33152
- const srcPos = sourceGeometry.getAttribute("position");
33153
- const count = triangleIndices.length * 9;
33154
- const positions = new Float32Array(count);
33155
- for (let i = 0; i < triangleIndices.length; i++) {
33156
- const base = triangleIndices[i] * 3;
33157
- const out = i * 9;
33158
- for (let v = 0; v < 3; v++) {
33159
- positions[out + v * 3] = srcPos.getX(base + v);
33160
- positions[out + v * 3 + 1] = srcPos.getY(base + v);
33161
- positions[out + v * 3 + 2] = srcPos.getZ(base + v);
33560
+ function buildGraph(edges) {
33561
+ const point = /* @__PURE__ */ new Map();
33562
+ const adj = /* @__PURE__ */ new Map();
33563
+ const key = (p2) => `${q(p2.x)},${q(p2.y)},${q(p2.z)}`;
33564
+ for (const { a: a2, b: b2 } of edges) {
33565
+ const ka = key(a2);
33566
+ const kb = key(b2);
33567
+ if (ka === kb) continue;
33568
+ point.set(ka, a2);
33569
+ point.set(kb, b2);
33570
+ (adj.get(ka) ?? adj.set(ka, /* @__PURE__ */ new Set()).get(ka)).add(kb);
33571
+ (adj.get(kb) ?? adj.set(kb, /* @__PURE__ */ new Set()).get(kb)).add(ka);
33572
+ }
33573
+ return { point, adj: new Map(Array.from(adj, ([k2, v]) => [k2, Array.from(v)])) };
33574
+ }
33575
+ function nearestSegment(graph, localPoint) {
33576
+ let best = null;
33577
+ let bestSq = Infinity;
33578
+ const closest = new Vector3();
33579
+ const seen = /* @__PURE__ */ new Set();
33580
+ for (const [u, neighbors] of graph.adj) {
33581
+ const a2 = graph.point.get(u);
33582
+ for (const v of neighbors) {
33583
+ const ek = edgeKey(u, v);
33584
+ if (seen.has(ek)) continue;
33585
+ seen.add(ek);
33586
+ const b2 = graph.point.get(v);
33587
+ const dSq = closestOnSegment(localPoint, a2, b2, closest);
33588
+ if (dSq < bestSq) {
33589
+ bestSq = dSq;
33590
+ best = { u, v };
33591
+ }
33162
33592
  }
33163
33593
  }
33164
- const geo = new BufferGeometry();
33165
- geo.setAttribute("position", new BufferAttribute(positions, 3));
33166
- return geo;
33594
+ return best;
33167
33595
  }
33168
- function findEdgeChain(geometry, hitPoint, mesh) {
33169
- const positions = geometry.getAttribute("position");
33170
- const normals = geometry.getAttribute("normal");
33171
- const triCount = positions.count / 3;
33172
- const edgeData = /* @__PURE__ */ new Map();
33173
- for (let t2 = 0; t2 < triCount; t2++) {
33174
- const base = t2 * 3;
33175
- const triNormal = new Vector3(normals.getX(base), normals.getY(base), normals.getZ(base));
33176
- for (let e2 = 0; e2 < 3; e2++) {
33177
- const i0 = base + e2;
33178
- const i1 = base + (e2 + 1) % 3;
33179
- const vk0 = vertKey(positions, i0);
33180
- const vk1 = vertKey(positions, i1);
33181
- const ek = edgeKey(vk0, vk1);
33182
- let data = edgeData.get(ek);
33183
- if (!data) {
33184
- const a2 = new Vector3(positions.getX(i0), positions.getY(i0), positions.getZ(i0));
33185
- const b2 = new Vector3(positions.getX(i1), positions.getY(i1), positions.getZ(i1));
33186
- data = { a: a2, b: b2, normals: [] };
33187
- edgeData.set(ek, data);
33188
- }
33189
- data.normals.push(triNormal);
33190
- }
33191
- }
33192
- const sharpEdges = [];
33193
- for (const [key, data] of edgeData) {
33194
- if (data.normals.length === 2 && data.normals[0].dot(data.normals[1]) < 0.9995) {
33195
- sharpEdges.push({ a: data.a, b: data.b, key });
33196
- } else if (data.normals.length === 1) {
33197
- sharpEdges.push({ a: data.a, b: data.b, key });
33198
- }
33199
- }
33200
- if (sharpEdges.length === 0) return null;
33201
- const localHit = hitPoint.clone().applyMatrix4(mesh.matrixWorld.clone().invert());
33202
- let closestEdge = null;
33203
- let closestDist = Infinity;
33204
- for (const edge of sharpEdges) {
33205
- const ab = edge.b.clone().sub(edge.a);
33206
- const denom = ab.lengthSq();
33207
- if (denom === 0) continue;
33208
- const t2 = MathUtils.clamp(localHit.clone().sub(edge.a).dot(ab) / denom, 0, 1);
33209
- const closest = edge.a.clone().add(ab.multiplyScalar(t2));
33210
- const dist = closest.distanceTo(localHit);
33211
- if (dist < closestDist) {
33212
- closestDist = dist;
33213
- closestEdge = edge;
33214
- }
33215
- }
33216
- if (!closestEdge) return null;
33217
- const dir = closestEdge.b.clone().sub(closestEdge.a).normalize();
33218
- const vertToEdges = /* @__PURE__ */ new Map();
33219
- for (const edge of sharpEdges) {
33220
- const vk0 = `${q(edge.a.x)},${q(edge.a.y)},${q(edge.a.z)}`;
33221
- const vk1 = `${q(edge.b.x)},${q(edge.b.y)},${q(edge.b.z)}`;
33222
- for (const vk of [vk0, vk1]) {
33223
- let list = vertToEdges.get(vk);
33224
- if (!list) {
33225
- list = [];
33226
- vertToEdges.set(vk, list);
33227
- }
33228
- list.push(edge);
33596
+ function walk(graph, fromKey, curKey, mode, stopAtKey) {
33597
+ const out = [];
33598
+ const visited = /* @__PURE__ */ new Set([fromKey, curKey]);
33599
+ let prev = fromKey;
33600
+ let cur = curKey;
33601
+ for (let guard = 0; guard < 1e5; guard++) {
33602
+ const prevP = graph.point.get(prev);
33603
+ const curP = graph.point.get(cur);
33604
+ const dIn = curP.clone().sub(prevP);
33605
+ const lenIn = dIn.length();
33606
+ if (lenIn === 0) break;
33607
+ dIn.multiplyScalar(1 / lenIn);
33608
+ let bestKey = null;
33609
+ let bestDot = -Infinity;
33610
+ let bestLen = 0;
33611
+ for (const w of graph.adj.get(cur) ?? []) {
33612
+ if (w === prev) continue;
33613
+ const wp = graph.point.get(w);
33614
+ const dOut = wp.clone().sub(curP);
33615
+ const lenOut = dOut.length();
33616
+ if (lenOut === 0) continue;
33617
+ const dot = dIn.dot(dOut) / lenOut;
33618
+ if (dot > bestDot) {
33619
+ bestDot = dot;
33620
+ bestKey = w;
33621
+ bestLen = lenOut;
33622
+ }
33623
+ }
33624
+ if (!bestKey) break;
33625
+ if (bestDot < CONTINUITY_LIMIT_COS) break;
33626
+ if (mode === "single") {
33627
+ if (bestDot < CORNER_TURN_COS) break;
33628
+ const ratio = bestLen >= lenIn ? bestLen / lenIn : lenIn / bestLen;
33629
+ if (ratio > LEN_RATIO_SPLIT) break;
33630
+ }
33631
+ if (bestKey === stopAtKey) {
33632
+ out.push(bestKey);
33633
+ break;
33229
33634
  }
33635
+ if (visited.has(bestKey)) break;
33636
+ visited.add(bestKey);
33637
+ out.push(bestKey);
33638
+ prev = cur;
33639
+ cur = bestKey;
33230
33640
  }
33231
- const chainEdges = /* @__PURE__ */ new Set();
33232
- const chainQueue = [closestEdge];
33233
- chainEdges.add(closestEdge.key);
33234
- const segments = [];
33235
- while (chainQueue.length > 0) {
33236
- const current = chainQueue.pop();
33237
- segments.push([current.a.clone(), current.b.clone()]);
33238
- const curDir = current.b.clone().sub(current.a).normalize();
33239
- const vk0 = `${q(current.a.x)},${q(current.a.y)},${q(current.a.z)}`;
33240
- const vk1 = `${q(current.b.x)},${q(current.b.y)},${q(current.b.z)}`;
33241
- for (const vk of [vk0, vk1]) {
33242
- const neighbors = vertToEdges.get(vk);
33243
- if (!neighbors) continue;
33244
- for (const neighbor of neighbors) {
33245
- if (chainEdges.has(neighbor.key)) continue;
33246
- const nDir = neighbor.b.clone().sub(neighbor.a).normalize();
33247
- if (Math.abs(curDir.dot(nDir)) > 0.966) {
33248
- chainEdges.add(neighbor.key);
33249
- chainQueue.push(neighbor);
33250
- }
33251
- }
33252
- }
33641
+ return out;
33642
+ }
33643
+ function chainFromSeed(graph, seed, mode) {
33644
+ const forward = walk(graph, seed.u, seed.v, mode, seed.u);
33645
+ const closed = forward.length > 0 && forward[forward.length - 1] === seed.u;
33646
+ let keys;
33647
+ if (closed) {
33648
+ keys = [seed.u, seed.v, ...forward];
33649
+ } else {
33650
+ const backward = walk(graph, seed.v, seed.u, mode, seed.v);
33651
+ keys = [...backward.slice().reverse(), seed.u, seed.v, ...forward];
33652
+ }
33653
+ return keys.map((k2) => graph.point.get(k2).clone());
33654
+ }
33655
+ function getNearestEdgeChain(geometry, localPoint) {
33656
+ const graph = buildGraph(extractFeatureEdges(geometry));
33657
+ const seed = nearestSegment(graph, localPoint);
33658
+ if (!seed) return null;
33659
+ return chainFromSeed(graph, seed, "single");
33660
+ }
33661
+ function getWholeEdgePath(geometry, localPoint) {
33662
+ const graph = buildGraph(extractFeatureEdges(geometry));
33663
+ const seed = nearestSegment(graph, localPoint);
33664
+ if (!seed) return null;
33665
+ return chainFromSeed(graph, seed, "whole");
33666
+ }
33667
+ function circleMeasurement(curve) {
33668
+ if (!curve || curve.kind !== "circle") return null;
33669
+ const radius = curve.radius;
33670
+ return {
33671
+ radius,
33672
+ diameter: radius * 2,
33673
+ circumference: 2 * Math.PI * radius,
33674
+ center: [curve.center[0], curve.center[1], curve.center[2]],
33675
+ axis: [curve.axis[0], curve.axis[1], curve.axis[2]]
33676
+ };
33677
+ }
33678
+ function worldScale(m2) {
33679
+ const s = new Vector3();
33680
+ m2.decompose(new Vector3(), new Quaternion(), s);
33681
+ return (Math.abs(s.x) + Math.abs(s.y) + Math.abs(s.z)) / 3;
33682
+ }
33683
+ function applyPoint(p2, m2) {
33684
+ const v = new Vector3(p2[0], p2[1], p2[2]).applyMatrix4(m2);
33685
+ return [v.x, v.y, v.z];
33686
+ }
33687
+ function edgeEntityFromBakedEdge(edge, meshMatrixWorld, meshUuid) {
33688
+ const scale = worldScale(meshMatrixWorld);
33689
+ const pts = edge.points;
33690
+ const n = pts.length / 3;
33691
+ const firstLocal = [pts[0], pts[1], pts[2]];
33692
+ const lastLocal = n > 0 ? [pts[(n - 1) * 3], pts[(n - 1) * 3 + 1], pts[(n - 1) * 3 + 2]] : firstLocal;
33693
+ const start = applyPoint(firstLocal, meshMatrixWorld);
33694
+ const end = applyPoint(lastLocal, meshMatrixWorld);
33695
+ const polyline = edge.curve.kind === "line" ? void 0 : Array.from({ length: n }, (_, i) => applyPoint([pts[i * 3], pts[i * 3 + 1], pts[i * 3 + 2]], meshMatrixWorld));
33696
+ if (edge.curve.kind === "line") {
33697
+ const s = new Vector3(...edge.curve.start).applyMatrix4(meshMatrixWorld);
33698
+ const e2 = new Vector3(...edge.curve.end).applyMatrix4(meshMatrixWorld);
33699
+ const dir2 = e2.clone().sub(s);
33700
+ const length = dir2.length();
33701
+ const direction2 = length > 0 ? dir2.normalize() : new Vector3(1, 0, 0);
33702
+ return {
33703
+ kind: "edge",
33704
+ start: [s.x, s.y, s.z],
33705
+ end: [e2.x, e2.y, e2.z],
33706
+ length,
33707
+ direction: [direction2.x, direction2.y, direction2.z],
33708
+ meshUuid,
33709
+ curve: { kind: "line", start: [s.x, s.y, s.z], end: [e2.x, e2.y, e2.z], faceName: edge.curve.faceName }
33710
+ };
33253
33711
  }
33254
- let minT = Infinity, maxT = -Infinity;
33255
- let startPt = closestEdge.a.clone(), endPt = closestEdge.b.clone();
33256
- const origin = closestEdge.a;
33257
- for (const [a2, b2] of segments) {
33258
- for (const pt2 of [a2, b2]) {
33259
- const t2 = pt2.clone().sub(origin).dot(dir);
33260
- if (t2 < minT) {
33261
- minT = t2;
33262
- startPt = pt2.clone();
33263
- }
33264
- if (t2 > maxT) {
33265
- maxT = t2;
33266
- endPt = pt2.clone();
33267
- }
33268
- }
33712
+ if (edge.curve.kind === "circle") {
33713
+ const center = new Vector3(...edge.curve.center).applyMatrix4(meshMatrixWorld);
33714
+ const axis = new Vector3(...edge.curve.axis).transformDirection(meshMatrixWorld).normalize();
33715
+ const radius = edge.curve.radius * scale;
33716
+ return {
33717
+ kind: "edge",
33718
+ start,
33719
+ end,
33720
+ length: 2 * Math.PI * radius,
33721
+ // circumference
33722
+ direction: [axis.x, axis.y, axis.z],
33723
+ meshUuid,
33724
+ curve: { kind: "circle", center: [center.x, center.y, center.z], axis: [axis.x, axis.y, axis.z], radius, faceName: edge.curve.faceName },
33725
+ polyline
33726
+ };
33269
33727
  }
33728
+ const dir = new Vector3(end[0] - start[0], end[1] - start[1], end[2] - start[2]);
33729
+ const direction = dir.lengthSq() > 0 ? dir.normalize() : new Vector3(1, 0, 0);
33270
33730
  return {
33271
- start: startPt.applyMatrix4(mesh.matrixWorld),
33272
- end: endPt.applyMatrix4(mesh.matrixWorld),
33273
- segments
33731
+ kind: "edge",
33732
+ start,
33733
+ end,
33734
+ length: edge.length * scale,
33735
+ direction: [direction.x, direction.y, direction.z],
33736
+ meshUuid,
33737
+ curve: { kind: "unidentified" },
33738
+ polyline
33739
+ };
33740
+ }
33741
+ const MEASURE_COLORS = {
33742
+ face: "#60b8ff",
33743
+ // hover face — gentle blue
33744
+ edge: "#ffffff",
33745
+ // hover edge — white
33746
+ vertex: "#ffffff",
33747
+ // hover vertex — white
33748
+ selection: "#ffa040",
33749
+ // both selections — warm amber
33750
+ line: "#ffa040",
33751
+ panelLabel: "#888",
33752
+ panelValue: "#ffd060"
33753
+ };
33754
+ function edgeEntityFromLocalPolyline(localPts, meshMatrixWorld, meshUuid) {
33755
+ const world = localPts.map((p2) => p2.clone().applyMatrix4(meshMatrixWorld));
33756
+ const start = world[0];
33757
+ const end = world[world.length - 1];
33758
+ let length = 0;
33759
+ const segments = [];
33760
+ for (let i = 1; i < world.length; i++) {
33761
+ length += world[i].distanceTo(world[i - 1]);
33762
+ segments.push([world[i - 1], world[i]]);
33763
+ }
33764
+ const dirVec = end.clone().sub(start);
33765
+ const direction = dirVec.lengthSq() > 0 ? dirVec.normalize() : new Vector3(1, 0, 0);
33766
+ const isLine = world.length <= 2;
33767
+ const entity = {
33768
+ kind: "edge",
33769
+ start: [start.x, start.y, start.z],
33770
+ end: [end.x, end.y, end.z],
33771
+ length,
33772
+ direction: [direction.x, direction.y, direction.z],
33773
+ meshUuid,
33774
+ curve: isLine ? { kind: "line", start: [start.x, start.y, start.z], end: [end.x, end.y, end.z] } : { kind: "unidentified" },
33775
+ polyline: isLine ? void 0 : world.map((p2) => [p2.x, p2.y, p2.z])
33274
33776
  };
33777
+ return { entity, preview: { kind: "edge", edgeSegments: segments, meshUuid } };
33275
33778
  }
33276
33779
  function computeMeasureResult(a2, b2) {
33277
33780
  const v32 = (xyz) => new Vector3(...xyz);
@@ -33413,9 +33916,11 @@ function MeasureTool() {
33413
33916
  const measureMode = useForgeStore((s) => s.measureMode);
33414
33917
  const measureSelections = useForgeStore((s) => s.measureSelections);
33415
33918
  const addMeasureSelection = useForgeStore((s) => s.addMeasureSelection);
33919
+ const replaceLastMeasureSelection = useForgeStore((s) => s.replaceLastMeasureSelection);
33416
33920
  const { camera, raycaster, scene, gl } = useThree();
33417
33921
  const [hover, setHover] = reactExports.useState(null);
33418
33922
  const pointerDownRef = reactExports.useRef(null);
33923
+ const lastSelectUpRef = reactExports.useRef(null);
33419
33924
  const [selectionVisuals, setSelectionVisuals] = reactExports.useState({ geos: [], matrices: [], edgeSegments: [], vertexPositions: [] });
33420
33925
  reactExports.useEffect(() => {
33421
33926
  const geos = [];
@@ -33443,7 +33948,15 @@ function MeasureTool() {
33443
33948
  } else if (sel.kind === "edge") {
33444
33949
  geos.push(null);
33445
33950
  matrices.push(null);
33446
- edgeSegs.push([[new Vector3(...sel.start), new Vector3(...sel.end)]]);
33951
+ if (sel.polyline && sel.polyline.length >= 2) {
33952
+ const segs = [];
33953
+ for (let i = 1; i < sel.polyline.length; i++) {
33954
+ segs.push([new Vector3(...sel.polyline[i - 1]), new Vector3(...sel.polyline[i])]);
33955
+ }
33956
+ edgeSegs.push(segs);
33957
+ } else {
33958
+ edgeSegs.push([[new Vector3(...sel.start), new Vector3(...sel.end)]]);
33959
+ }
33447
33960
  vertexPos.push(null);
33448
33961
  } else {
33449
33962
  geos.push(null);
@@ -33491,7 +34004,7 @@ function MeasureTool() {
33491
34004
  [gl.domElement]
33492
34005
  );
33493
34006
  const detectEntity = reactExports.useCallback(
33494
- (event) => {
34007
+ (event, wholePath = false) => {
33495
34008
  if (!measureMode) return null;
33496
34009
  const pointer = getPointerNDC(event);
33497
34010
  raycaster.setFromCamera(new Vector2(pointer.x, pointer.y), camera);
@@ -33521,58 +34034,89 @@ function MeasureTool() {
33521
34034
  const vA = new Vector3().fromBufferAttribute(positions, ia).applyMatrix4(mesh.matrixWorld);
33522
34035
  const vB = new Vector3().fromBufferAttribute(positions, ib).applyMatrix4(mesh.matrixWorld);
33523
34036
  const vC = new Vector3().fromBufferAttribute(positions, ic).applyMatrix4(mesh.matrixWorld);
33524
- let closestVertexDist = Infinity;
33525
- let closestVertex = null;
33526
- for (const v of [vA, vB, vC]) {
33527
- const s = worldToScreen2D(v);
33528
- const d = Math.hypot(screenX - s.x, screenY - s.y);
33529
- if (d < closestVertexDist && d < SNAP_PX) {
33530
- closestVertexDist = d;
33531
- closestVertex = v;
33532
- }
33533
- }
33534
- if (closestVertex) {
33535
- const entity2 = {
33536
- kind: "vertex",
33537
- position: [closestVertex.x, closestVertex.y, closestVertex.z],
33538
- meshUuid: mesh.uuid
33539
- };
33540
- return {
33541
- entity: entity2,
33542
- preview: { kind: "vertex", vertexPosition: closestVertex.clone() }
33543
- };
33544
- }
33545
- const edgeResult = findEdgeChain(geometry, hit.point, mesh);
33546
- if (edgeResult) {
33547
- const closestOnEdge = (() => {
33548
- const ab = edgeResult.end.clone().sub(edgeResult.start);
33549
- const denom = ab.lengthSq();
33550
- if (denom === 0) return edgeResult.start.clone();
33551
- const t2 = MathUtils.clamp(hit.point.clone().sub(edgeResult.start).dot(ab) / denom, 0, 1);
33552
- return edgeResult.start.clone().add(ab.multiplyScalar(t2));
33553
- })();
33554
- const edgeScreenPt = worldToScreen2D(closestOnEdge);
33555
- const edgeScreenDist = Math.hypot(screenX - edgeScreenPt.x, screenY - edgeScreenPt.y);
33556
- if (edgeScreenDist < SNAP_PX * 1.5) {
33557
- const dir = edgeResult.end.clone().sub(edgeResult.start).normalize();
34037
+ const invWorld = new Matrix4().copy(mesh.matrixWorld).invert();
34038
+ const localHit = hit.point.clone().applyMatrix4(invWorld);
34039
+ const bakedEdges = getBakedEdges(geometry);
34040
+ if (bakedEdges.length > 0) {
34041
+ if (!wholePath) {
34042
+ let cornerWorld = null;
34043
+ let cornerDist = SNAP_PX;
34044
+ for (const e2 of bakedEdges) {
34045
+ if (e2.curve.kind !== "line") continue;
34046
+ for (const pt2 of [e2.curve.start, e2.curve.end]) {
34047
+ const w = new Vector3(pt2[0], pt2[1], pt2[2]).applyMatrix4(mesh.matrixWorld);
34048
+ const s = worldToScreen2D(w);
34049
+ const d = Math.hypot(screenX - s.x, screenY - s.y);
34050
+ if (d < cornerDist) {
34051
+ cornerDist = d;
34052
+ cornerWorld = w;
34053
+ }
34054
+ }
34055
+ }
34056
+ if (cornerWorld) {
34057
+ const entity2 = {
34058
+ kind: "vertex",
34059
+ position: [cornerWorld.x, cornerWorld.y, cornerWorld.z],
34060
+ meshUuid: mesh.uuid
34061
+ };
34062
+ return { entity: entity2, preview: { kind: "vertex", vertexPosition: cornerWorld.clone() } };
34063
+ }
34064
+ }
34065
+ const carrierEdge = getNearestEdge(geometry, localHit, Number.POSITIVE_INFINITY);
34066
+ if (carrierEdge && (!wholePath || carrierEdge.edge.curve.kind === "circle")) {
34067
+ const worldPt = carrierEdge.point.clone().applyMatrix4(mesh.matrixWorld);
34068
+ const sp = worldToScreen2D(worldPt);
34069
+ if (Math.hypot(screenX - sp.x, screenY - sp.y) < SNAP_PX * 1.5) {
34070
+ const entity2 = edgeEntityFromBakedEdge(carrierEdge.edge, mesh.matrixWorld, mesh.uuid);
34071
+ const segs = [];
34072
+ const p2 = carrierEdge.edge.points;
34073
+ for (let i = 0; i + 5 < p2.length; i += 3) {
34074
+ segs.push([
34075
+ new Vector3(p2[i], p2[i + 1], p2[i + 2]).applyMatrix4(mesh.matrixWorld),
34076
+ new Vector3(p2[i + 3], p2[i + 4], p2[i + 5]).applyMatrix4(mesh.matrixWorld)
34077
+ ]);
34078
+ }
34079
+ return { entity: entity2, preview: { kind: "edge", edgeSegments: segs, meshUuid: mesh.uuid } };
34080
+ }
34081
+ }
34082
+ } else if (!wholePath) {
34083
+ let closestVertexDist = Infinity;
34084
+ let closestVertex = null;
34085
+ for (const v of [vA, vB, vC]) {
34086
+ const s = worldToScreen2D(v);
34087
+ const d = Math.hypot(screenX - s.x, screenY - s.y);
34088
+ if (d < closestVertexDist && d < SNAP_PX) {
34089
+ closestVertexDist = d;
34090
+ closestVertex = v;
34091
+ }
34092
+ }
34093
+ if (closestVertex) {
33558
34094
  const entity2 = {
33559
- kind: "edge",
33560
- start: [edgeResult.start.x, edgeResult.start.y, edgeResult.start.z],
33561
- end: [edgeResult.end.x, edgeResult.end.y, edgeResult.end.z],
33562
- length: edgeResult.start.distanceTo(edgeResult.end),
33563
- direction: [dir.x, dir.y, dir.z],
34095
+ kind: "vertex",
34096
+ position: [closestVertex.x, closestVertex.y, closestVertex.z],
33564
34097
  meshUuid: mesh.uuid
33565
34098
  };
33566
- const worldSegments = edgeResult.segments.map(
33567
- ([a2, b2]) => [a2.clone().applyMatrix4(mesh.matrixWorld), b2.clone().applyMatrix4(mesh.matrixWorld)]
33568
- );
33569
- return {
33570
- entity: entity2,
33571
- preview: { kind: "edge", edgeSegments: worldSegments, meshUuid: mesh.uuid }
33572
- };
34099
+ return { entity: entity2, preview: { kind: "vertex", vertexPosition: closestVertex.clone() } };
34100
+ }
34101
+ }
34102
+ const chain = wholePath ? getWholeEdgePath(geometry, localHit) : getNearestEdgeChain(geometry, localHit);
34103
+ if (chain && chain.length >= 2) {
34104
+ const closest = new Vector3();
34105
+ const tmp = new Vector3();
34106
+ let nearestSq = Infinity;
34107
+ for (let i = 1; i < chain.length; i++) {
34108
+ const dSq = closestOnSegment(localHit, chain[i - 1], chain[i], tmp);
34109
+ if (dSq < nearestSq) {
34110
+ nearestSq = dSq;
34111
+ closest.copy(tmp);
34112
+ }
34113
+ }
34114
+ const screenPt = worldToScreen2D(closest.clone().applyMatrix4(mesh.matrixWorld));
34115
+ if (Math.hypot(screenX - screenPt.x, screenY - screenPt.y) < SNAP_PX * 1.5) {
34116
+ return edgeEntityFromLocalPolyline(chain, mesh.matrixWorld, mesh.uuid);
33573
34117
  }
33574
34118
  }
33575
- const ffResult = floodFillFace(geometry, faceIndex);
34119
+ const ffResult = getFaceRegion(geometry, faceIndex);
33576
34120
  const worldNormal = ffResult.normal.clone().transformDirection(mesh.matrixWorld).normalize();
33577
34121
  const worldCenter = ffResult.center.clone().applyMatrix4(mesh.matrixWorld);
33578
34122
  const highlightGeo = buildFaceHighlightGeometry(geometry, ffResult.triangleIndices);
@@ -33636,17 +34180,31 @@ function MeasureTool() {
33636
34180
  const down = pointerDownRef.current;
33637
34181
  pointerDownRef.current = null;
33638
34182
  if (!down || down.moved) return;
34183
+ const clearHover = () => setHover((prev) => {
34184
+ var _a4;
34185
+ (_a4 = prev == null ? void 0 : prev.faceHighlightGeo) == null ? void 0 : _a4.dispose();
34186
+ return null;
34187
+ });
34188
+ const now = performance.now();
34189
+ const last2 = lastSelectUpRef.current;
34190
+ const isDouble = !!last2 && last2.added && now - last2.t < 300 && Math.hypot(event.clientX - last2.x, event.clientY - last2.y) < 6;
34191
+ if (isDouble) {
34192
+ const whole = detectEntity(event, true);
34193
+ if (whole) {
34194
+ replaceLastMeasureSelection(whole.entity);
34195
+ clearHover();
34196
+ }
34197
+ lastSelectUpRef.current = null;
34198
+ return;
34199
+ }
33639
34200
  const result = detectEntity(event);
33640
34201
  if (result) {
33641
34202
  addMeasureSelection(result.entity);
33642
- setHover((prev) => {
33643
- var _a4;
33644
- (_a4 = prev == null ? void 0 : prev.faceHighlightGeo) == null ? void 0 : _a4.dispose();
33645
- return null;
33646
- });
34203
+ clearHover();
33647
34204
  }
34205
+ lastSelectUpRef.current = { t: now, x: event.clientX, y: event.clientY, added: !!result };
33648
34206
  },
33649
- [addMeasureSelection, detectEntity, measureMode]
34207
+ [addMeasureSelection, detectEntity, measureMode, replaceLastMeasureSelection]
33650
34208
  );
33651
34209
  reactExports.useEffect(() => {
33652
34210
  if (!measureMode) {
@@ -33859,15 +34417,31 @@ function MeasureInfoPanel() {
33859
34417
  ] });
33860
34418
  }
33861
34419
  if (sel.kind === "edge") {
34420
+ const circle = circleMeasurement(sel.curve);
33862
34421
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fc-viewport-floating-panel fc-measure-info-panel", style: PANEL_STYLE, children: [
33863
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, marginBottom: 6, color: MEASURE_COLORS.selection }, children: "Edge" }),
33864
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
33865
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Length" }),
33866
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue }, children: formatLength(sel.length, lengthUnit) })
33867
- ] }),
33868
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
33869
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Direction" }),
33870
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: fmtNormal(sel.direction) })
34422
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, marginBottom: 6, color: MEASURE_COLORS.selection }, children: circle ? "Circular Edge" : "Edge" }),
34423
+ circle ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
34424
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34425
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Radius" }),
34426
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue, fontWeight: 600, fontSize: 14 }, children: formatLength(circle.radius, lengthUnit) })
34427
+ ] }),
34428
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34429
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Diameter" }),
34430
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue, fontWeight: 600, fontSize: 14 }, children: formatLength(circle.diameter, lengthUnit) })
34431
+ ] }),
34432
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34433
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Circumference" }),
34434
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue }, children: formatLength(circle.circumference, lengthUnit) })
34435
+ ] })
34436
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
34437
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34438
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Length" }),
34439
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue }, children: formatLength(sel.length, lengthUnit) })
34440
+ ] }),
34441
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34442
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Direction" }),
34443
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: fmtNormal(sel.direction) })
34444
+ ] })
33871
34445
  ] }),
33872
34446
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { marginTop: 6, fontSize: 10, color: MEASURE_COLORS.panelLabel }, children: "Click another entity to measure" })
33873
34447
  ] });
@@ -37456,10 +38030,10 @@ function slotMaxWidth(slot, occupied, inset) {
37456
38030
  if (occupied.has(opposite)) return `min(390px, calc(50% - ${inset + 4}px))`;
37457
38031
  return `min(390px, calc(100% - ${inset * 2}px))`;
37458
38032
  }
37459
- function ViewportOverlayHost({ entries, inset = 12, gap = 8 }) {
38033
+ function ViewportOverlayHost({ entries: entries2, inset = 12, gap = 8 }) {
37460
38034
  const grouped = /* @__PURE__ */ new Map();
37461
38035
  const occupied = /* @__PURE__ */ new Set();
37462
- for (const entry of entries) {
38036
+ for (const entry of entries2) {
37463
38037
  if (entry.content === null || entry.content === void 0 || typeof entry.content === "boolean") continue;
37464
38038
  occupied.add(entry.slot);
37465
38039
  const slotEntries = grouped.get(entry.slot) ?? [];
@@ -38530,7 +39104,7 @@ const computeExplodeTreeOffsets = (root, explodeAmount, explodeConfig) => {
38530
39104
  if (Math.abs(config.amount) <= 1e-8) return {};
38531
39105
  const rootCenter = explodeBoundsCenter(root.bounds) ?? [0, 0, 0];
38532
39106
  const offsets = {};
38533
- const walk = (node, depth, inherited, parentCenter, parentDirection) => {
39107
+ const walk2 = (node, depth, inherited, parentCenter, parentDirection) => {
38534
39108
  const center = explodeBoundsCenter(node.bounds) ?? parentCenter;
38535
39109
  const directive = resolveExplodeDirective([node.path.join("/")], node.label, void 0, config);
38536
39110
  const motion = depth > 1 && node.children.length === 0 && !hasExplodeOverride(directive) ? (() => {
@@ -38554,9 +39128,9 @@ const computeExplodeTreeOffsets = (root, explodeAmount, explodeConfig) => {
38554
39128
  node.objectIds.forEach((objectId) => {
38555
39129
  offsets[objectId] = total;
38556
39130
  });
38557
- node.children.forEach((child) => walk(child, depth + 1, total, center, motion.branchDirection));
39131
+ node.children.forEach((child) => walk2(child, depth + 1, total, center, motion.branchDirection));
38558
39132
  };
38559
- root.children.forEach((child) => walk(child, 1, [0, 0, 0], rootCenter, void 0));
39133
+ root.children.forEach((child) => walk2(child, 1, [0, 0, 0], rootCenter, void 0));
38560
39134
  return offsets;
38561
39135
  };
38562
39136
  const EMPTY_RIG_INSPECTION_OVERLAY_STATE = {
@@ -38936,6 +39510,7 @@ function useViewportState() {
38936
39510
  const lengthUnit = useForgeStore((s) => s.lengthUnit);
38937
39511
  const constructionGhost = useForgeStore((s) => s.constructionGhost);
38938
39512
  const dimensionsVisible = useForgeStore((s) => s.dimensionsVisible);
39513
+ const paramAnchorsVisible = useForgeStore((s) => s.paramAnchorsVisible);
38939
39514
  const attachmentsVisible = useForgeStore((s) => s.attachmentsVisible);
38940
39515
  const _surfacesVisible = useForgeStore((s) => s.surfacesVisible);
38941
39516
  const cutPlaneEnabled = useForgeStore((s) => s.cutPlaneEnabled);
@@ -39356,6 +39931,7 @@ function useViewportState() {
39356
39931
  renderLabels,
39357
39932
  debugHighlights3D,
39358
39933
  dimensionsVisible,
39934
+ paramAnchorsVisible,
39359
39935
  attachmentsVisible,
39360
39936
  attachmentPoints,
39361
39937
  cutPlaneEnabled,
@@ -39474,6 +40050,48 @@ function shouldRequestInitialFit({
39474
40050
  if (initialFitRequested && previousPreviewFile === previewFile) return false;
39475
40051
  return true;
39476
40052
  }
40053
+ const SUBFEATURE_SNAP_PX = 14;
40054
+ const CLICK_DRAG_TOLERANCE_PX = 4;
40055
+ function worldToScreenPx(pt2, camera, rect) {
40056
+ const p2 = pt2.clone().project(camera);
40057
+ return {
40058
+ x: (p2.x * 0.5 + 0.5) * rect.width + rect.left,
40059
+ y: (-p2.y * 0.5 + 0.5) * rect.height + rect.top
40060
+ };
40061
+ }
40062
+ function drillIntoSubFeature(mesh, worldHit, camera, rect, clientX, clientY, objectId, store) {
40063
+ const geometry = mesh.geometry;
40064
+ const inv = new Matrix4().copy(mesh.matrixWorld).invert();
40065
+ const localHit = worldHit.clone().applyMatrix4(inv);
40066
+ const vtx = getNearestVertex(geometry, localHit, Number.POSITIVE_INFINITY);
40067
+ if (vtx) {
40068
+ const worldPt = vtx.point.clone().applyMatrix4(mesh.matrixWorld);
40069
+ const s = worldToScreenPx(worldPt, camera, rect);
40070
+ if (Math.hypot(clientX - s.x, clientY - s.y) <= SUBFEATURE_SNAP_PX) {
40071
+ store.setSelectedEdge(null);
40072
+ store.setSelectedVertex({ objectId, point: [vtx.point.x, vtx.point.y, vtx.point.z] });
40073
+ return true;
40074
+ }
40075
+ }
40076
+ const edge = getNearestEdge(geometry, localHit, Number.POSITIVE_INFINITY);
40077
+ if (edge) {
40078
+ const worldPt = edge.point.clone().applyMatrix4(mesh.matrixWorld);
40079
+ const s = worldToScreenPx(worldPt, camera, rect);
40080
+ if (Math.hypot(clientX - s.x, clientY - s.y) <= SUBFEATURE_SNAP_PX) {
40081
+ store.setSelectedVertex(null);
40082
+ store.setSelectedEdge({
40083
+ objectId,
40084
+ faceA: edge.edge.faceA,
40085
+ faceB: edge.edge.faceB,
40086
+ curve: edge.edge.curve,
40087
+ length: edge.edge.length,
40088
+ points: edge.edge.points
40089
+ });
40090
+ return true;
40091
+ }
40092
+ }
40093
+ return false;
40094
+ }
39477
40095
  function useViewportHandlers({
39478
40096
  containerRef,
39479
40097
  contextMenuRef,
@@ -39505,6 +40123,14 @@ function useViewportHandlers({
39505
40123
  const [faceInfoData, setFaceInfoData] = reactExports.useState(null);
39506
40124
  const [faceInfoLoading, setFaceInfoLoading] = reactExports.useState(false);
39507
40125
  const [sketchEntityInfo, setSketchEntityInfo] = reactExports.useState(null);
40126
+ const pointerDownPosRef = reactExports.useRef(null);
40127
+ reactExports.useEffect(() => {
40128
+ const onDown = (e2) => {
40129
+ pointerDownPosRef.current = { x: e2.clientX, y: e2.clientY };
40130
+ };
40131
+ window.addEventListener("pointerdown", onDown, { capture: true });
40132
+ return () => window.removeEventListener("pointerdown", onDown, { capture: true });
40133
+ }, []);
39508
40134
  const closeObjectContextMenu = reactExports.useCallback(() => {
39509
40135
  setObjectContextMenu(null);
39510
40136
  }, []);
@@ -39558,40 +40184,56 @@ function useViewportHandlers({
39558
40184
  hideHoverTooltip();
39559
40185
  setHoveredObjectId(null);
39560
40186
  }, [hideHoverTooltip, objectPickSyncEnabled, setHoveredObjectId]);
39561
- reactExports.useEffect(() => {
39562
- const handleEscape = (event) => {
39563
- if (event.key !== "Escape") return;
39564
- if (objectContextMenu) {
39565
- closeObjectContextMenu();
39566
- return;
39567
- }
39568
- if (faceInfoPanel) {
39569
- setFaceInfoPanel(null);
39570
- return;
39571
- }
39572
- if (sketchEntityInfo) {
39573
- setSketchEntityInfo(null);
39574
- return;
39575
- }
39576
- const store = useForgeStore.getState();
39577
- if (store.measureMode) {
39578
- if (store.measureSelections.length > 0) {
39579
- store.clearMeasureSelections();
39580
- } else {
39581
- store.toggleMeasure();
39582
- }
39583
- return;
39584
- }
39585
- if (store.constructionGhost !== null) {
39586
- store.setConstructionGhost(null);
39587
- return;
40187
+ const handleViewportEscape = reactExports.useCallback(() => {
40188
+ if (objectContextMenu) {
40189
+ closeObjectContextMenu();
40190
+ return true;
40191
+ }
40192
+ const store = useForgeStore.getState();
40193
+ if (store.selectedVertex) {
40194
+ store.setSelectedVertex(null);
40195
+ return true;
40196
+ }
40197
+ if (store.selectedEdge) {
40198
+ store.setSelectedEdge(null);
40199
+ return true;
40200
+ }
40201
+ if (store.selectedFace) {
40202
+ store.setSelectedFace(null);
40203
+ setFaceInfoPanel(null);
40204
+ return true;
40205
+ }
40206
+ if (faceInfoPanel) {
40207
+ setFaceInfoPanel(null);
40208
+ return true;
40209
+ }
40210
+ if (sketchEntityInfo) {
40211
+ setSketchEntityInfo(null);
40212
+ return true;
40213
+ }
40214
+ if (store.measureMode) {
40215
+ if (store.measureSelections.length > 0) {
40216
+ store.clearMeasureSelections();
40217
+ } else {
40218
+ store.toggleMeasure();
39588
40219
  }
39589
- if (store.focusedObjectIds.length === 0) return;
40220
+ return true;
40221
+ }
40222
+ if (store.constructionGhost !== null) {
40223
+ store.setConstructionGhost(null);
40224
+ return true;
40225
+ }
40226
+ if (store.focusedObjectIds.length > 0) {
39590
40227
  clearFocusedObject();
39591
- };
39592
- window.addEventListener("keydown", handleEscape);
39593
- return () => window.removeEventListener("keydown", handleEscape);
40228
+ return true;
40229
+ }
40230
+ if (store.selectedObjectId) {
40231
+ store.selectObject(null);
40232
+ return true;
40233
+ }
40234
+ return false;
39594
40235
  }, [clearFocusedObject, closeObjectContextMenu, faceInfoPanel, objectContextMenu, sketchEntityInfo]);
40236
+ useEscapeAction(handleViewportEscape, { active: true, label: "Viewport selection", priority: ESCAPE_PRIORITY.viewport });
39595
40237
  reactExports.useEffect(() => {
39596
40238
  const handleViewShortcut = (event) => {
39597
40239
  if (event.isComposing || event.repeat) return;
@@ -39681,11 +40323,42 @@ function useViewportHandlers({
39681
40323
  );
39682
40324
  const handleObjectClick = reactExports.useCallback(
39683
40325
  (obj, event) => {
40326
+ var _a3;
39684
40327
  if (!objectPickSyncEnabled || measureMode || isViewportInteracting) return;
40328
+ const down = pointerDownPosRef.current;
40329
+ if (down && Math.hypot(event.clientX - down.x, event.clientY - down.y) > CLICK_DRAG_TOLERANCE_PX) return;
39685
40330
  event.stopPropagation();
39686
- selectObject(obj.id);
40331
+ const store = useForgeStore.getState();
40332
+ const alreadySelected = store.selectedObjectId === obj.id;
40333
+ if (!alreadySelected) {
40334
+ selectObject(obj.id);
40335
+ setFaceInfoPanel(null);
40336
+ return;
40337
+ }
40338
+ const rectForPick = (_a3 = containerRef.current) == null ? void 0 : _a3.getBoundingClientRect();
40339
+ if (event.object instanceof Mesh && rectForPick && event.point && drillIntoSubFeature(event.object, event.point, event.camera, rectForPick, event.clientX, event.clientY, obj.id, store)) {
40340
+ store.setSelectedFace(null);
40341
+ setFaceInfoPanel(null);
40342
+ return;
40343
+ }
40344
+ const triangleIndex = event.faceIndex ?? (event.face ? Math.floor(event.face.a / 3) : null);
40345
+ if (event.object instanceof Mesh && triangleIndex !== null && triangleIndex >= 0) {
40346
+ store.setSelectedEdge(null);
40347
+ store.setSelectedVertex(null);
40348
+ const region = getFaceRegion(event.object.geometry, triangleIndex);
40349
+ const worldNormal = region.normal.clone().transformDirection(event.object.matrixWorld).normalize();
40350
+ const worldCenter = region.center.clone().applyMatrix4(event.object.matrixWorld);
40351
+ store.setSelectedFace({
40352
+ objectId: obj.id,
40353
+ carrierName: region.carrierName,
40354
+ triangleIndices: region.triangleIndices,
40355
+ meshUuid: event.object.uuid,
40356
+ normal: [worldNormal.x, worldNormal.y, worldNormal.z],
40357
+ center: [worldCenter.x, worldCenter.y, worldCenter.z]
40358
+ });
40359
+ }
39687
40360
  },
39688
- [isViewportInteracting, measureMode, objectPickSyncEnabled, selectObject]
40361
+ [containerRef, isViewportInteracting, measureMode, objectPickSyncEnabled, selectObject, setFaceInfoPanel]
39689
40362
  );
39690
40363
  const handleObjectDoubleClick = reactExports.useCallback(
39691
40364
  (obj, event) => {
@@ -39693,8 +40366,13 @@ function useViewportHandlers({
39693
40366
  event.stopPropagation();
39694
40367
  const additive = event.shiftKey || event.metaKey || event.ctrlKey;
39695
40368
  focusObject(obj.id, { additive });
40369
+ const store = useForgeStore.getState();
40370
+ store.setSelectedFace(null);
40371
+ store.setSelectedEdge(null);
40372
+ store.setSelectedVertex(null);
40373
+ setFaceInfoPanel(null);
39696
40374
  },
39697
- [focusObject, isViewportInteracting, measureMode]
40375
+ [focusObject, isViewportInteracting, measureMode, setFaceInfoPanel]
39698
40376
  );
39699
40377
  const handleObjectContextMenu = reactExports.useCallback(
39700
40378
  (obj, event) => {
@@ -40400,7 +41078,7 @@ const HoverTooltipLayer = reactExports.forwardRef(function HoverTooltipLayer2({
40400
41078
  }
40401
41079
  );
40402
41080
  });
40403
- const buttonStyle = {
41081
+ const buttonStyle$3 = {
40404
41082
  border: "1px solid var(--fc-border)",
40405
41083
  background: "var(--fc-btn)",
40406
41084
  color: "var(--fc-text)",
@@ -40411,7 +41089,7 @@ const buttonStyle = {
40411
41089
  lineHeight: 1
40412
41090
  };
40413
41091
  const iconButtonStyle = {
40414
- ...buttonStyle,
41092
+ ...buttonStyle$3,
40415
41093
  width: 30,
40416
41094
  height: 30,
40417
41095
  padding: 0,
@@ -40563,7 +41241,7 @@ function ModelJourneyBar({
40563
41241
  padding: 4
40564
41242
  },
40565
41243
  children: [
40566
- /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", style: buttonStyle, onClick: () => activateStep(firstId, getStepStartIndex(firstJourney)), children: [
41244
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", style: buttonStyle$3, onClick: () => activateStep(firstId, getStepStartIndex(firstJourney)), children: [
40567
41245
  "Explore: ",
40568
41246
  label
40569
41247
  ] }),
@@ -40657,7 +41335,7 @@ function ModelJourneyBar({
40657
41335
  "button",
40658
41336
  {
40659
41337
  type: "button",
40660
- style: buttonStyle,
41338
+ style: buttonStyle$3,
40661
41339
  onClick: () => {
40662
41340
  setActive({ ...active, interrupted: false });
40663
41341
  applyStep(activeStep);
@@ -40665,7 +41343,7 @@ function ModelJourneyBar({
40665
41343
  children: "Resume"
40666
41344
  }
40667
41345
  ),
40668
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", style: buttonStyle, disabled: atEnd, onClick: () => activateStep(active.journeyId, active.stepIndex + 1), children: "Next" })
41346
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", style: buttonStyle$3, disabled: atEnd, onClick: () => activateStep(active.journeyId, active.stepIndex + 1), children: "Next" })
40669
41347
  ] }),
40670
41348
  caption && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { marginTop: 6, paddingLeft: 76, fontSize: 12, color: "var(--fc-textDim)" }, children: caption })
40671
41349
  ]
@@ -40796,6 +41474,1819 @@ function RigInspectionOverlay({ state: state2, config }) {
40796
41474
  state2.joints.map((joint) => /* @__PURE__ */ jsxRuntimeExports.jsx(HoveredJointOverlay, { state: joint, config: joint.hidden ? hiddenConfig : config }, joint.joint.name))
40797
41475
  ] });
40798
41476
  }
41477
+ function roundToStep(value, step) {
41478
+ return Number((Math.round(value / step) * step).toFixed(8));
41479
+ }
41480
+ function clamp(value, min, max2) {
41481
+ return Math.max(min, Math.min(max2, value));
41482
+ }
41483
+ function currentPath2DPoints(pathDef, overrides) {
41484
+ const countOverride = overrides[`${pathDef.name}.__count__`];
41485
+ const count = typeof countOverride === "number" ? clamp(Math.round(countOverride), pathDef.minPoints, pathDef.maxPoints) : pathDef.points.length;
41486
+ const points = [];
41487
+ for (let i = 0; i < count; i += 1) {
41488
+ const base = pathDef.points[i] ?? pathDef.defaultPoints[i] ?? { x: 0, y: 0 };
41489
+ const rawX = overrides[`${pathDef.name}[${i}].x`];
41490
+ const rawY = overrides[`${pathDef.name}[${i}].y`];
41491
+ points.push({
41492
+ x: typeof rawX === "number" ? rawX : base.x,
41493
+ y: typeof rawY === "number" ? rawY : base.y
41494
+ });
41495
+ }
41496
+ return points;
41497
+ }
41498
+ function frameForPath2D(width, height, pathDef) {
41499
+ const spanX = Math.max(1, pathDef.x.max - pathDef.x.min);
41500
+ const spanY = Math.max(1, pathDef.y.max - pathDef.y.min);
41501
+ const availableWidth = Math.max(1, width - 28);
41502
+ const availableHeight = Math.max(1, height - 28);
41503
+ const scale = Math.min(availableWidth / spanX, availableHeight / spanY);
41504
+ return {
41505
+ originX: (width - spanX * scale) / 2 - pathDef.x.min * scale,
41506
+ originY: (height + spanY * scale) / 2 + pathDef.y.min * scale,
41507
+ scale
41508
+ };
41509
+ }
41510
+ function path2DToScreen(point, frame2) {
41511
+ return { x: frame2.originX + point.x * frame2.scale, y: frame2.originY - point.y * frame2.scale };
41512
+ }
41513
+ function screenToPath2D(x, y, frame2) {
41514
+ return { x: (x - frame2.originX) / frame2.scale, y: (frame2.originY - y) / frame2.scale };
41515
+ }
41516
+ function panPath2DFrame(frame2, dx, dy) {
41517
+ return { ...frame2, originX: frame2.originX + dx, originY: frame2.originY + dy };
41518
+ }
41519
+ function zoomPath2DFrame(frame2, centerX, centerY, factor) {
41520
+ const nextScale = clamp(frame2.scale * factor, 0.05, 60);
41521
+ const model = screenToPath2D(centerX, centerY, frame2);
41522
+ return {
41523
+ originX: centerX - model.x * nextScale,
41524
+ originY: centerY + model.y * nextScale,
41525
+ scale: nextScale
41526
+ };
41527
+ }
41528
+ function drawPath2DGrid(ctx, width, height, frame2) {
41529
+ ctx.strokeStyle = "rgba(255,255,255,0.08)";
41530
+ ctx.lineWidth = 1;
41531
+ const modelGridStep = frame2.scale < 0.35 ? 100 : frame2.scale < 1 ? 50 : frame2.scale < 2.5 ? 20 : 10;
41532
+ const gridStep = modelGridStep * frame2.scale;
41533
+ for (let x = frame2.originX % gridStep; x < width; x += gridStep) {
41534
+ ctx.beginPath();
41535
+ ctx.moveTo(x, 0);
41536
+ ctx.lineTo(x, height);
41537
+ ctx.stroke();
41538
+ }
41539
+ for (let y = frame2.originY % gridStep; y < height; y += gridStep) {
41540
+ ctx.beginPath();
41541
+ ctx.moveTo(0, y);
41542
+ ctx.lineTo(width, y);
41543
+ ctx.stroke();
41544
+ }
41545
+ }
41546
+ function path2DPatch(pathName, points) {
41547
+ const patch = { [`${pathName}.__count__`]: points.length };
41548
+ points.forEach((point, index) => {
41549
+ patch[`${pathName}[${index}].x`] = point.x;
41550
+ patch[`${pathName}[${index}].y`] = point.y;
41551
+ });
41552
+ return patch;
41553
+ }
41554
+ function trailingPath2DKeys(pathName, oldCount, newCount) {
41555
+ const keys = [];
41556
+ for (let i = newCount; i < oldCount; i += 1) {
41557
+ keys.push(`${pathName}[${i}].x`, `${pathName}[${i}].y`);
41558
+ }
41559
+ return keys;
41560
+ }
41561
+ function hasPointSelectionModifier(event) {
41562
+ return event.shiftKey || event.metaKey || event.ctrlKey;
41563
+ }
41564
+ function normalizePointSelection(selectedIndices, pointCount, activeIndex) {
41565
+ if (pointCount <= 0) return [];
41566
+ const boundedActive = Math.max(0, Math.min(activeIndex, pointCount - 1));
41567
+ const normalized = [];
41568
+ const seen = /* @__PURE__ */ new Set();
41569
+ for (const index of selectedIndices) {
41570
+ if (!Number.isInteger(index) || index < 0 || index >= pointCount || seen.has(index)) continue;
41571
+ seen.add(index);
41572
+ normalized.push(index);
41573
+ }
41574
+ if (!seen.has(boundedActive)) normalized.push(boundedActive);
41575
+ return normalized.sort((a2, b2) => a2 - b2);
41576
+ }
41577
+ function pointSelectionForPointerDown(currentSelectedIndices, hitIndex, activeIndex, additive, pointCount) {
41578
+ const current = normalizePointSelection(currentSelectedIndices, pointCount, activeIndex);
41579
+ const alreadySelected = current.includes(hitIndex);
41580
+ if (!additive) {
41581
+ return {
41582
+ activeIndex: hitIndex,
41583
+ selectedIndices: alreadySelected ? current : [hitIndex],
41584
+ shouldDrag: true
41585
+ };
41586
+ }
41587
+ if (alreadySelected && current.length > 1) {
41588
+ const selectedIndices = current.filter((index) => index !== hitIndex);
41589
+ return {
41590
+ activeIndex: selectedIndices.includes(activeIndex) ? activeIndex : selectedIndices[0],
41591
+ selectedIndices,
41592
+ shouldDrag: false
41593
+ };
41594
+ }
41595
+ if (alreadySelected) {
41596
+ return { activeIndex: hitIndex, selectedIndices: current, shouldDrag: true };
41597
+ }
41598
+ return {
41599
+ activeIndex: hitIndex,
41600
+ selectedIndices: normalizePointSelection([...current, hitIndex], pointCount, hitIndex),
41601
+ shouldDrag: true
41602
+ };
41603
+ }
41604
+ const buttonStyle$2 = {
41605
+ background: "var(--fc-small-button-bg, none)",
41606
+ border: "1px solid var(--fc-small-button-border, var(--fc-border))",
41607
+ borderRadius: 3,
41608
+ color: "var(--fc-textDim)",
41609
+ fontSize: 10,
41610
+ padding: "1px 5px",
41611
+ cursor: "pointer",
41612
+ lineHeight: "14px",
41613
+ userSelect: "none"
41614
+ };
41615
+ function Path2DParamEditor({
41616
+ pathDef,
41617
+ allowFullScreen = true,
41618
+ large = false
41619
+ }) {
41620
+ const canvasRef = reactExports.useRef(null);
41621
+ const dragState = reactExports.useRef(null);
41622
+ const draftPointsRef = reactExports.useRef(null);
41623
+ const panStart = reactExports.useRef(null);
41624
+ const paramOverrides = useForgeStore((state2) => state2.paramOverrides);
41625
+ const setParams = useForgeStore((state2) => state2.setParams);
41626
+ const [expanded, setExpanded] = reactExports.useState(true);
41627
+ const [fullScreen, setFullScreen] = reactExports.useState(false);
41628
+ const [viewFrame, setViewFrame] = reactExports.useState(null);
41629
+ const [draftPoints, setDraftPoints] = reactExports.useState(null);
41630
+ const [selected, setSelected] = reactExports.useState(0);
41631
+ const [selectedIndices, setSelectedIndices] = reactExports.useState([0]);
41632
+ useEscapeAction(
41633
+ () => {
41634
+ setFullScreen(false);
41635
+ return true;
41636
+ },
41637
+ { active: fullScreen, label: `${pathDef.name} path editor`, priority: ESCAPE_PRIORITY.modal }
41638
+ );
41639
+ const committedPoints = reactExports.useMemo(() => currentPath2DPoints(pathDef, paramOverrides), [paramOverrides, pathDef]);
41640
+ const points = draftPoints ?? committedPoints;
41641
+ const activeIndex = points.length > 0 ? Math.min(selected, points.length - 1) : 0;
41642
+ const normalizedSelectedIndices = reactExports.useMemo(
41643
+ () => normalizePointSelection(selectedIndices, points.length, activeIndex),
41644
+ [activeIndex, points.length, selectedIndices]
41645
+ );
41646
+ const selectedIndexSet = reactExports.useMemo(() => new Set(normalizedSelectedIndices), [normalizedSelectedIndices]);
41647
+ const selectedPoint = points[activeIndex] ?? points[0];
41648
+ const unitLabel = pathDef.unit ? ` ${pathDef.unit}` : "";
41649
+ const canAdd = points.length < pathDef.maxPoints;
41650
+ const canRemove = points.length - normalizedSelectedIndices.length >= pathDef.minPoints;
41651
+ reactExports.useEffect(() => {
41652
+ if (selected >= points.length) setSelected(Math.max(0, points.length - 1));
41653
+ }, [points.length, selected]);
41654
+ reactExports.useEffect(() => {
41655
+ const canvas = canvasRef.current;
41656
+ if (!canvas) return;
41657
+ const ctx = canvas.getContext("2d");
41658
+ if (!ctx) return;
41659
+ const draw = () => {
41660
+ const rect = canvas.getBoundingClientRect();
41661
+ const dpr = window.devicePixelRatio || 1;
41662
+ const nextWidth = Math.max(1, Math.round(rect.width * dpr));
41663
+ const nextHeight = Math.max(1, Math.round(rect.height * dpr));
41664
+ if (canvas.width !== nextWidth) canvas.width = nextWidth;
41665
+ if (canvas.height !== nextHeight) canvas.height = nextHeight;
41666
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
41667
+ ctx.clearRect(0, 0, rect.width, rect.height);
41668
+ const frame2 = viewFrame ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, pathDef);
41669
+ drawPath2DGrid(ctx, rect.width, rect.height, frame2);
41670
+ if (points.length > 0) {
41671
+ const projected = points.map((point) => path2DToScreen(point, frame2));
41672
+ ctx.beginPath();
41673
+ ctx.moveTo(projected[0].x, projected[0].y);
41674
+ for (let i = 1; i < projected.length; i += 1) ctx.lineTo(projected[i].x, projected[i].y);
41675
+ if (pathDef.closed) ctx.closePath();
41676
+ ctx.fillStyle = pathDef.closed ? "rgba(77, 147, 191, 0.22)" : "transparent";
41677
+ ctx.strokeStyle = pathDef.closed ? "#4d93bf" : "#3c8f83";
41678
+ ctx.lineWidth = pathDef.closed ? 2 : 7;
41679
+ if (pathDef.closed) ctx.fill();
41680
+ ctx.stroke();
41681
+ projected.forEach((point, index) => {
41682
+ const isSelected = selectedIndexSet.has(index);
41683
+ const isActive = index === activeIndex;
41684
+ ctx.beginPath();
41685
+ ctx.arc(point.x, point.y, isActive ? 8 : isSelected ? 7 : 6, 0, Math.PI * 2);
41686
+ ctx.fillStyle = index === 0 ? "#d84f45" : "#e2b647";
41687
+ ctx.fill();
41688
+ ctx.lineWidth = isActive ? 3 : isSelected ? 2.25 : 1.5;
41689
+ ctx.strokeStyle = isSelected ? "#fff6d4" : "#171914";
41690
+ ctx.stroke();
41691
+ });
41692
+ }
41693
+ };
41694
+ draw();
41695
+ const observer = new ResizeObserver(draw);
41696
+ observer.observe(canvas);
41697
+ return () => observer.disconnect();
41698
+ }, [activeIndex, fullScreen, large, pathDef, points, selectedIndexSet, viewFrame]);
41699
+ reactExports.useEffect(() => {
41700
+ const canvas = canvasRef.current;
41701
+ if (!canvas) return;
41702
+ const handleWheel = (event) => {
41703
+ event.preventDefault();
41704
+ event.stopPropagation();
41705
+ const rect = canvas.getBoundingClientRect();
41706
+ const x = event.clientX - rect.left;
41707
+ const y = event.clientY - rect.top;
41708
+ setViewFrame(
41709
+ (current) => zoomPath2DFrame(current ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, pathDef), x, y, event.deltaY > 0 ? 0.88 : 1.14)
41710
+ );
41711
+ };
41712
+ canvas.addEventListener("wheel", handleWheel, { passive: false });
41713
+ return () => canvas.removeEventListener("wheel", handleWheel);
41714
+ }, [pathDef]);
41715
+ const pointerPoint = (event) => {
41716
+ const canvas = event.currentTarget;
41717
+ const rect = canvas.getBoundingClientRect();
41718
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
41719
+ };
41720
+ const nearestHandle = (canvas, x, y) => {
41721
+ const frame2 = viewFrame ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, pathDef);
41722
+ let bestIndex = null;
41723
+ let bestDistance = Infinity;
41724
+ points.forEach((point, index) => {
41725
+ const screen2 = path2DToScreen(point, frame2);
41726
+ const distance = Math.hypot(screen2.x - x, screen2.y - y);
41727
+ if (distance < bestDistance) {
41728
+ bestDistance = distance;
41729
+ bestIndex = index;
41730
+ }
41731
+ });
41732
+ return bestDistance <= 18 ? bestIndex : null;
41733
+ };
41734
+ const moveDraggedPoints = (drag, pointer) => {
41735
+ const dx = pointer.x - drag.startPointer.x;
41736
+ const dy = pointer.y - drag.startPointer.y;
41737
+ const dragIndices = new Set(drag.indices);
41738
+ const nextPoints = drag.startPoints.map(
41739
+ (point, index) => dragIndices.has(index) ? {
41740
+ x: roundToStep(point.x + dx, pathDef.x.step),
41741
+ y: roundToStep(point.y + dy, pathDef.y.step)
41742
+ } : point
41743
+ );
41744
+ draftPointsRef.current = nextPoints;
41745
+ setDraftPoints(nextPoints);
41746
+ };
41747
+ const commitDraftDrag = () => {
41748
+ const nextPoints = draftPointsRef.current;
41749
+ draftPointsRef.current = null;
41750
+ setDraftPoints(null);
41751
+ if (nextPoints) setParams(path2DPatch(pathDef.name, nextPoints));
41752
+ };
41753
+ const cancelDraftDrag = () => {
41754
+ draftPointsRef.current = null;
41755
+ setDraftPoints(null);
41756
+ };
41757
+ const insertPointAfterSelection = () => {
41758
+ if (!canAdd) return;
41759
+ const insertAt = Math.min(activeIndex + 1, points.length);
41760
+ const a2 = points[activeIndex] ?? points[points.length - 1] ?? { x: 0, y: 0 };
41761
+ const b2 = points[insertAt] ?? (pathDef.closed ? points[0] : a2);
41762
+ const nextPoint = {
41763
+ x: roundToStep((a2.x + b2.x) / 2, pathDef.x.step),
41764
+ y: roundToStep((a2.y + b2.y) / 2, pathDef.y.step)
41765
+ };
41766
+ const nextPoints = [...points];
41767
+ nextPoints.splice(insertAt, 0, nextPoint);
41768
+ setSelected(insertAt);
41769
+ setSelectedIndices([insertAt]);
41770
+ setParams(path2DPatch(pathDef.name, nextPoints));
41771
+ };
41772
+ const removeSelectedPoint = () => {
41773
+ if (!canRemove) return;
41774
+ const removeIndices = new Set(normalizedSelectedIndices);
41775
+ const nextPoints = points.filter((_, index) => !removeIndices.has(index));
41776
+ const firstRemoved = normalizedSelectedIndices[0] ?? activeIndex;
41777
+ const nextSelected = Math.max(0, Math.min(firstRemoved, nextPoints.length - 1));
41778
+ setSelected(nextSelected);
41779
+ setSelectedIndices([nextSelected]);
41780
+ setParams(path2DPatch(pathDef.name, nextPoints), trailingPath2DKeys(pathDef.name, points.length, nextPoints.length));
41781
+ };
41782
+ const canvasFrame = (canvas) => viewFrame ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, pathDef);
41783
+ const canvasHeight = large ? "min(62vh, 640px)" : fullScreen ? "calc(100vh - 150px)" : 220;
41784
+ const expandedLayout = large || fullScreen;
41785
+ const editorPanel = /* @__PURE__ */ jsxRuntimeExports.jsxs(
41786
+ "div",
41787
+ {
41788
+ style: {
41789
+ border: "1px solid var(--fc-border)",
41790
+ borderRadius: 4,
41791
+ padding: 8,
41792
+ background: "var(--fc-bgSubtle, var(--fc-bgSurface, #1c2128))",
41793
+ userSelect: "none",
41794
+ WebkitUserSelect: "none",
41795
+ ...expandedLayout ? {
41796
+ display: "flex",
41797
+ flexDirection: "column"
41798
+ } : {},
41799
+ ...fullScreen ? {
41800
+ position: "fixed",
41801
+ inset: 16,
41802
+ zIndex: 2200,
41803
+ boxShadow: "0 18px 52px rgba(0, 0, 0, 0.45)",
41804
+ boxSizing: "border-box",
41805
+ color: "var(--fc-text)"
41806
+ } : {}
41807
+ },
41808
+ children: [
41809
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 4, marginBottom: 6 }, children: [
41810
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setViewFrame(null), title: "Fit path in editor", style: buttonStyle$2, children: "Fit" }),
41811
+ allowFullScreen && /* @__PURE__ */ jsxRuntimeExports.jsx(
41812
+ "button",
41813
+ {
41814
+ type: "button",
41815
+ onClick: () => setFullScreen(!fullScreen),
41816
+ title: fullScreen ? "Close expanded editor" : "Expand editor",
41817
+ style: buttonStyle$2,
41818
+ children: fullScreen ? "Close" : "Full"
41819
+ }
41820
+ )
41821
+ ] }),
41822
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
41823
+ "canvas",
41824
+ {
41825
+ ref: canvasRef,
41826
+ height: 220,
41827
+ "aria-label": `${pathDef.name} editor`,
41828
+ onPointerDown: (event) => {
41829
+ event.preventDefault();
41830
+ const pos = pointerPoint(event);
41831
+ const hit = nearestHandle(event.currentTarget, pos.x, pos.y);
41832
+ if (hit === null) panStart.current = { x: pos.x, y: pos.y, frame: canvasFrame(event.currentTarget) };
41833
+ else {
41834
+ const nextSelection = pointSelectionForPointerDown(
41835
+ normalizedSelectedIndices,
41836
+ hit,
41837
+ activeIndex,
41838
+ hasPointSelectionModifier(event),
41839
+ points.length
41840
+ );
41841
+ setSelected(nextSelection.activeIndex);
41842
+ setSelectedIndices(nextSelection.selectedIndices);
41843
+ if (nextSelection.shouldDrag) {
41844
+ const frame2 = canvasFrame(event.currentTarget);
41845
+ draftPointsRef.current = null;
41846
+ setDraftPoints(null);
41847
+ dragState.current = {
41848
+ indices: nextSelection.selectedIndices,
41849
+ startPointer: screenToPath2D(pos.x, pos.y, frame2),
41850
+ startPoints: points.map((point) => ({ ...point })),
41851
+ frame: frame2
41852
+ };
41853
+ }
41854
+ }
41855
+ event.currentTarget.setPointerCapture(event.pointerId);
41856
+ },
41857
+ onPointerMove: (event) => {
41858
+ const pos = pointerPoint(event);
41859
+ if (dragState.current) {
41860
+ moveDraggedPoints(dragState.current, screenToPath2D(pos.x, pos.y, dragState.current.frame));
41861
+ return;
41862
+ }
41863
+ if (panStart.current) {
41864
+ setViewFrame(panPath2DFrame(panStart.current.frame, pos.x - panStart.current.x, pos.y - panStart.current.y));
41865
+ }
41866
+ },
41867
+ onPointerUp: (event) => {
41868
+ if (dragState.current) commitDraftDrag();
41869
+ dragState.current = null;
41870
+ panStart.current = null;
41871
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
41872
+ },
41873
+ onPointerCancel: (event) => {
41874
+ if (dragState.current) cancelDraftDrag();
41875
+ dragState.current = null;
41876
+ panStart.current = null;
41877
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
41878
+ },
41879
+ style: { display: "block", width: "100%", height: canvasHeight, borderRadius: 3, background: "var(--fc-bg)", touchAction: "none" }
41880
+ }
41881
+ ),
41882
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, marginTop: 6 }, children: [
41883
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { color: "var(--fc-textDim)", fontSize: 10, fontVariantNumeric: "tabular-nums" }, children: [
41884
+ normalizedSelectedIndices.length > 1 ? `${normalizedSelectedIndices.length} selected · ` : "",
41885
+ "P",
41886
+ Math.min(activeIndex + 1, points.length),
41887
+ " x ",
41888
+ selectedPoint ? selectedPoint.x : 0,
41889
+ unitLabel,
41890
+ " · y ",
41891
+ selectedPoint ? selectedPoint.y : 0,
41892
+ unitLabel
41893
+ ] }),
41894
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", gap: 4 }, children: [
41895
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: insertPointAfterSelection, disabled: !canAdd, title: "Add point after selected", style: buttonStyle$2, children: "+ Point" }),
41896
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: removeSelectedPoint, disabled: !canRemove, title: "Remove selected point(s)", style: buttonStyle$2, children: "Remove" })
41897
+ ] })
41898
+ ] })
41899
+ ]
41900
+ }
41901
+ );
41902
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { marginBottom: 8 }, children: [
41903
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
41904
+ "div",
41905
+ {
41906
+ onClick: () => setExpanded(!expanded),
41907
+ style: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 12, cursor: "pointer", marginBottom: 4 },
41908
+ children: [
41909
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { color: "var(--fc-text)", fontWeight: 600 }, children: [
41910
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: 9, marginRight: 4 }, children: expanded ? "▼" : "▶" }),
41911
+ pathDef.name,
41912
+ " (",
41913
+ points.length,
41914
+ ")"
41915
+ ] }),
41916
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "var(--fc-textDim)", fontSize: 10 }, children: pathDef.closed ? "outline" : "centerline" })
41917
+ ]
41918
+ }
41919
+ ),
41920
+ expanded && editorPanel
41921
+ ] });
41922
+ }
41923
+ function currentPlacement2DItems(layoutDef, overrides) {
41924
+ const zones = new Set(layoutDef.zones.map((zone) => zone.id));
41925
+ return layoutDef.items.map((item) => {
41926
+ const rawX = overrides[`${layoutDef.name}.${item.id}.x`];
41927
+ const rawY = overrides[`${layoutDef.name}.${item.id}.y`];
41928
+ const rawAngle = overrides[`${layoutDef.name}.${item.id}.angle`];
41929
+ const rawZone = overrides[`${layoutDef.name}.${item.id}.zone`];
41930
+ const zone = typeof rawZone === "string" && zones.has(rawZone) ? rawZone : item.zone;
41931
+ return {
41932
+ ...item,
41933
+ footprint: item.footprint.type === "rect" ? { ...item.footprint } : { ...item.footprint },
41934
+ x: typeof rawX === "number" ? rawX : item.x,
41935
+ y: typeof rawY === "number" ? rawY : item.y,
41936
+ angle: typeof rawAngle === "number" ? rawAngle : item.angle,
41937
+ zone
41938
+ };
41939
+ });
41940
+ }
41941
+ function placement2DItemPatch(layoutName, item) {
41942
+ const patch = {
41943
+ [`${layoutName}.${item.id}.x`]: item.x,
41944
+ [`${layoutName}.${item.id}.y`]: item.y,
41945
+ [`${layoutName}.${item.id}.angle`]: item.angle
41946
+ };
41947
+ if (item.zone) patch[`${layoutName}.${item.id}.zone`] = item.zone;
41948
+ return patch;
41949
+ }
41950
+ function placement2DFrameForItem(item, frame2, zones) {
41951
+ return (item.zone ? zones.find((zone) => zone.id === item.zone) : void 0) ?? frame2;
41952
+ }
41953
+ function placement2DZoneAtPoint(zones, point) {
41954
+ return zones.find((zone) => placement2DPointInsideFrame(point, zone));
41955
+ }
41956
+ function placement2DClampItem(item, frame2, zones) {
41957
+ const target = placement2DFrameForItem(item, frame2, zones);
41958
+ const center = placement2DClampCenterToFrame(item, target);
41959
+ return { ...item, x: center.x, y: center.y };
41960
+ }
41961
+ function placement2DHasBoundsViolation(layoutDef, item) {
41962
+ return !placement2DFootprintInsideFrame(item, placement2DFrameForItem(item, layoutDef.frame, layoutDef.zones));
41963
+ }
41964
+ function placement2DCollisionPairs(items) {
41965
+ const pairs = [];
41966
+ for (let i = 0; i < items.length; i += 1) {
41967
+ for (let j2 = i + 1; j2 < items.length; j2 += 1) {
41968
+ if ((items[i].zone ?? "") !== (items[j2].zone ?? "")) continue;
41969
+ if (placement2DItemsOverlap(items[i], items[j2])) pairs.push([items[i].id, items[j2].id]);
41970
+ }
41971
+ }
41972
+ return pairs;
41973
+ }
41974
+ function placement2DItemCollides(candidate, items) {
41975
+ return items.some(
41976
+ (item) => item.id !== candidate.id && (item.zone ?? "") === (candidate.zone ?? "") && placement2DItemsOverlap(candidate, item)
41977
+ );
41978
+ }
41979
+ function placement2DSnapItem(item, snap) {
41980
+ return { ...item, x: roundToStep(item.x, snap), y: roundToStep(item.y, snap), angle: roundToStep(item.angle, 1) };
41981
+ }
41982
+ function placement2DFrameForCanvas(width, height, layoutDef) {
41983
+ const bounds = placement2DFrameBounds(layoutDef.frame);
41984
+ const spanX = Math.max(1, bounds.maxX - bounds.minX);
41985
+ const spanY = Math.max(1, bounds.maxY - bounds.minY);
41986
+ const availableWidth = Math.max(1, width - 28);
41987
+ const availableHeight = Math.max(1, height - 28);
41988
+ const scale = Math.min(availableWidth / spanX, availableHeight / spanY);
41989
+ return {
41990
+ originX: (width - spanX * scale) / 2 - bounds.minX * scale,
41991
+ originY: (height + spanY * scale) / 2 + bounds.minY * scale,
41992
+ scale
41993
+ };
41994
+ }
41995
+ function placement2DPointHitsItem(point, item) {
41996
+ if (item.footprint.type === "circle") return Math.hypot(point.x - item.x, point.y - item.y) <= item.footprint.radius;
41997
+ const angle = -item.angle * Math.PI / 180;
41998
+ const dx = point.x - item.x;
41999
+ const dy = point.y - item.y;
42000
+ const localX = dx * Math.cos(angle) - dy * Math.sin(angle);
42001
+ const localY = dx * Math.sin(angle) + dy * Math.cos(angle);
42002
+ return Math.abs(localX) <= item.footprint.width / 2 && Math.abs(localY) <= item.footprint.height / 2;
42003
+ }
42004
+ const buttonStyle$1 = {
42005
+ background: "var(--fc-small-button-bg, none)",
42006
+ border: "1px solid var(--fc-small-button-border, var(--fc-border))",
42007
+ borderRadius: 3,
42008
+ color: "var(--fc-textDim)",
42009
+ fontSize: 10,
42010
+ padding: "1px 5px",
42011
+ cursor: "pointer",
42012
+ lineHeight: "14px",
42013
+ userSelect: "none"
42014
+ };
42015
+ const selectStyle$1 = {
42016
+ background: "var(--fc-bg)",
42017
+ color: "var(--fc-text)",
42018
+ border: "1px solid var(--fc-border)",
42019
+ borderRadius: 3,
42020
+ padding: "1px 4px",
42021
+ fontSize: 11,
42022
+ cursor: "pointer",
42023
+ lineHeight: "16px"
42024
+ };
42025
+ function drawFrame(ctx, frame2, view2, label, active) {
42026
+ const bounds = placement2DFrameBounds(frame2);
42027
+ const min = path2DToScreen({ x: bounds.minX, y: bounds.minY }, view2);
42028
+ const max2 = path2DToScreen({ x: bounds.maxX, y: bounds.maxY }, view2);
42029
+ const x = Math.min(min.x, max2.x);
42030
+ const y = Math.min(min.y, max2.y);
42031
+ const width = Math.abs(max2.x - min.x);
42032
+ const height = Math.abs(max2.y - min.y);
42033
+ ctx.save();
42034
+ ctx.fillStyle = active ? "rgba(77, 147, 191, 0.13)" : "rgba(255,255,255,0.035)";
42035
+ ctx.strokeStyle = active ? "#4d93bf" : "rgba(255,255,255,0.22)";
42036
+ ctx.lineWidth = active ? 2 : 1;
42037
+ ctx.setLineDash(active ? [] : [5, 5]);
42038
+ ctx.fillRect(x, y, width, height);
42039
+ ctx.strokeRect(x, y, width, height);
42040
+ ctx.setLineDash([]);
42041
+ if (label) {
42042
+ ctx.fillStyle = active ? "#9ed6ff" : "rgba(255,255,255,0.55)";
42043
+ ctx.font = "11px system-ui, sans-serif";
42044
+ ctx.fillText(label, x + 6, y + 15, Math.max(20, width - 12));
42045
+ }
42046
+ ctx.restore();
42047
+ }
42048
+ function drawItem(ctx, item, view2, selected, conflicted) {
42049
+ const center = path2DToScreen({ x: item.x, y: item.y }, view2);
42050
+ const color = conflicted ? "#d65f45" : selected ? "#e2b647" : "#4d93bf";
42051
+ ctx.save();
42052
+ ctx.translate(center.x, center.y);
42053
+ ctx.rotate(-item.angle * Math.PI / 180);
42054
+ ctx.fillStyle = conflicted ? "rgba(214, 95, 69, 0.24)" : selected ? "rgba(226, 182, 71, 0.25)" : "rgba(77, 147, 191, 0.22)";
42055
+ ctx.strokeStyle = color;
42056
+ ctx.lineWidth = selected ? 2.5 : 1.5;
42057
+ if (item.locked) ctx.setLineDash([4, 4]);
42058
+ if (item.footprint.type === "circle") {
42059
+ ctx.beginPath();
42060
+ ctx.arc(0, 0, item.footprint.radius * view2.scale, 0, Math.PI * 2);
42061
+ ctx.fill();
42062
+ ctx.stroke();
42063
+ } else {
42064
+ const width = item.footprint.width * view2.scale;
42065
+ const height = item.footprint.height * view2.scale;
42066
+ ctx.beginPath();
42067
+ ctx.rect(-width / 2, -height / 2, width, height);
42068
+ ctx.fill();
42069
+ ctx.stroke();
42070
+ }
42071
+ ctx.setLineDash([]);
42072
+ ctx.rotate(item.angle * Math.PI / 180);
42073
+ ctx.fillStyle = "#f4f7f0";
42074
+ ctx.font = selected ? "600 11px system-ui, sans-serif" : "11px system-ui, sans-serif";
42075
+ ctx.textAlign = "center";
42076
+ ctx.textBaseline = "middle";
42077
+ ctx.fillText(item.label ?? item.id, 0, 0, 90);
42078
+ ctx.restore();
42079
+ }
42080
+ function Placement2DParamEditor({
42081
+ layoutDef,
42082
+ allowFullScreen = true,
42083
+ large = false
42084
+ }) {
42085
+ var _a3, _b2;
42086
+ const canvasRef = reactExports.useRef(null);
42087
+ const dragRef = reactExports.useRef(null);
42088
+ const draftItemsRef = reactExports.useRef(null);
42089
+ const panStart = reactExports.useRef(null);
42090
+ const paramOverrides = useForgeStore((state2) => state2.paramOverrides);
42091
+ const setParams = useForgeStore((state2) => state2.setParams);
42092
+ const [expanded, setExpanded] = reactExports.useState(true);
42093
+ const [fullScreen, setFullScreen] = reactExports.useState(false);
42094
+ const [viewFrame, setViewFrame] = reactExports.useState(null);
42095
+ const [draftItems, setDraftItems] = reactExports.useState(null);
42096
+ const [selectedId, setSelectedId] = reactExports.useState(((_a3 = layoutDef.items[0]) == null ? void 0 : _a3.id) ?? "");
42097
+ const [blockedId, setBlockedId] = reactExports.useState(null);
42098
+ useEscapeAction(
42099
+ () => {
42100
+ setFullScreen(false);
42101
+ return true;
42102
+ },
42103
+ { active: fullScreen, label: `${layoutDef.name} placement editor`, priority: ESCAPE_PRIORITY.modal }
42104
+ );
42105
+ const committedItems = reactExports.useMemo(() => currentPlacement2DItems(layoutDef, paramOverrides), [layoutDef, paramOverrides]);
42106
+ const items = draftItems ?? committedItems;
42107
+ const collisionPairs = reactExports.useMemo(() => placement2DCollisionPairs(items), [items]);
42108
+ const conflictedIds = reactExports.useMemo(() => new Set(collisionPairs.flat()), [collisionPairs]);
42109
+ const selectedItem = items.find((item) => item.id === selectedId) ?? items[0];
42110
+ const unitLabel = layoutDef.unit ? ` ${layoutDef.unit}` : "";
42111
+ const collisionCount = collisionPairs.length;
42112
+ const boundsCount = items.filter((item) => placement2DHasBoundsViolation(layoutDef, item)).length;
42113
+ reactExports.useEffect(() => {
42114
+ var _a4;
42115
+ if (!items.some((item) => item.id === selectedId)) setSelectedId(((_a4 = items[0]) == null ? void 0 : _a4.id) ?? "");
42116
+ }, [items, selectedId]);
42117
+ reactExports.useEffect(() => {
42118
+ const canvas = canvasRef.current;
42119
+ if (!canvas) return;
42120
+ const ctx = canvas.getContext("2d");
42121
+ if (!ctx) return;
42122
+ const draw = () => {
42123
+ const rect = canvas.getBoundingClientRect();
42124
+ const dpr = window.devicePixelRatio || 1;
42125
+ const nextWidth = Math.max(1, Math.round(rect.width * dpr));
42126
+ const nextHeight = Math.max(1, Math.round(rect.height * dpr));
42127
+ if (canvas.width !== nextWidth) canvas.width = nextWidth;
42128
+ if (canvas.height !== nextHeight) canvas.height = nextHeight;
42129
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
42130
+ ctx.clearRect(0, 0, rect.width, rect.height);
42131
+ const frame2 = viewFrame ?? placement2DFrameForCanvas(canvas.clientWidth, canvas.clientHeight, layoutDef);
42132
+ drawPath2DGrid(ctx, rect.width, rect.height, frame2);
42133
+ if (layoutDef.zones.length === 0) {
42134
+ drawFrame(ctx, layoutDef.frame, frame2, "Frame", true);
42135
+ } else {
42136
+ for (const zone of layoutDef.zones) {
42137
+ drawFrame(
42138
+ ctx,
42139
+ zone,
42140
+ frame2,
42141
+ zone.label ?? zone.id,
42142
+ items.some((item) => item.zone === zone.id)
42143
+ );
42144
+ }
42145
+ }
42146
+ for (const item of items) drawItem(ctx, item, frame2, item.id === selectedId, conflictedIds.has(item.id) || blockedId === item.id);
42147
+ };
42148
+ draw();
42149
+ const observer = new ResizeObserver(draw);
42150
+ observer.observe(canvas);
42151
+ return () => observer.disconnect();
42152
+ }, [blockedId, conflictedIds, items, layoutDef, selectedId, viewFrame]);
42153
+ reactExports.useEffect(() => {
42154
+ const canvas = canvasRef.current;
42155
+ if (!canvas) return;
42156
+ const handleWheel = (event) => {
42157
+ event.preventDefault();
42158
+ event.stopPropagation();
42159
+ const rect = canvas.getBoundingClientRect();
42160
+ const x = event.clientX - rect.left;
42161
+ const y = event.clientY - rect.top;
42162
+ setViewFrame(
42163
+ (current) => zoomPath2DFrame(
42164
+ current ?? placement2DFrameForCanvas(canvas.clientWidth, canvas.clientHeight, layoutDef),
42165
+ x,
42166
+ y,
42167
+ event.deltaY > 0 ? 0.88 : 1.14
42168
+ )
42169
+ );
42170
+ };
42171
+ canvas.addEventListener("wheel", handleWheel, { passive: false });
42172
+ return () => canvas.removeEventListener("wheel", handleWheel);
42173
+ }, [layoutDef]);
42174
+ const canvasFrame = (canvas) => viewFrame ?? placement2DFrameForCanvas(canvas.clientWidth, canvas.clientHeight, layoutDef);
42175
+ const canvasHeight = large ? "min(62vh, 640px)" : fullScreen ? "calc(100vh - 166px)" : 240;
42176
+ const expandedLayout = large || fullScreen;
42177
+ const pointerPoint = (event) => {
42178
+ const rect = event.currentTarget.getBoundingClientRect();
42179
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
42180
+ };
42181
+ const itemAtPointer = (canvas, x, y) => {
42182
+ const model = screenToPath2D(x, y, canvasFrame(canvas));
42183
+ for (let index = items.length - 1; index >= 0; index -= 1) {
42184
+ if (placement2DPointHitsItem(model, items[index])) return items[index];
42185
+ }
42186
+ return null;
42187
+ };
42188
+ const resolveCandidateItem = (rawCandidate, sourceItems) => {
42189
+ let candidate = placement2DSnapItem(rawCandidate, layoutDef.rules.snap);
42190
+ if (layoutDef.rules.bounds === "prevent") {
42191
+ candidate = placement2DClampItem(candidate, layoutDef.frame, layoutDef.zones);
42192
+ if (placement2DHasBoundsViolation(layoutDef, candidate)) {
42193
+ setBlockedId(candidate.id);
42194
+ return null;
42195
+ }
42196
+ }
42197
+ const collides = placement2DItemCollides(candidate, sourceItems);
42198
+ setBlockedId(collides ? candidate.id : null);
42199
+ if (layoutDef.rules.collisions === "prevent" && collides) return null;
42200
+ return candidate;
42201
+ };
42202
+ const commitItem = (rawCandidate) => {
42203
+ const candidate = resolveCandidateItem(rawCandidate, items);
42204
+ if (!candidate) return;
42205
+ setParams(placement2DItemPatch(layoutDef.name, candidate));
42206
+ };
42207
+ const previewDraggedItem = (rawCandidate) => {
42208
+ const sourceItems = draftItemsRef.current ?? items;
42209
+ const candidate = resolveCandidateItem(rawCandidate, sourceItems);
42210
+ if (!candidate) return;
42211
+ const nextItems = sourceItems.map((item) => item.id === candidate.id ? candidate : item);
42212
+ draftItemsRef.current = nextItems;
42213
+ setDraftItems(nextItems);
42214
+ };
42215
+ const commitDraftDrag = (id) => {
42216
+ const nextItems = draftItemsRef.current;
42217
+ draftItemsRef.current = null;
42218
+ setDraftItems(null);
42219
+ const item = nextItems == null ? void 0 : nextItems.find((candidate) => candidate.id === id);
42220
+ if (item) setParams(placement2DItemPatch(layoutDef.name, item));
42221
+ };
42222
+ const cancelDraftDrag = () => {
42223
+ draftItemsRef.current = null;
42224
+ setDraftItems(null);
42225
+ };
42226
+ const setSelectedZone = (zone) => {
42227
+ if (!selectedItem) return;
42228
+ commitItem({ ...selectedItem, zone });
42229
+ };
42230
+ const setSelectedAngle = (angle) => {
42231
+ if (!selectedItem) return;
42232
+ commitItem({ ...selectedItem, angle });
42233
+ };
42234
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { marginBottom: 8 }, children: [
42235
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
42236
+ "div",
42237
+ {
42238
+ onClick: () => setExpanded(!expanded),
42239
+ style: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 12, cursor: "pointer", marginBottom: 4 },
42240
+ children: [
42241
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { color: "var(--fc-text)", fontWeight: 600 }, children: [
42242
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: 9, marginRight: 4 }, children: expanded ? "v" : ">" }),
42243
+ layoutDef.name,
42244
+ " (",
42245
+ items.length,
42246
+ ")"
42247
+ ] }),
42248
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: collisionCount > 0 || boundsCount > 0 ? "#d65f45" : "var(--fc-textDim)", fontSize: 10 }, children: "Placement Sheet" })
42249
+ ]
42250
+ }
42251
+ ),
42252
+ expanded && /* @__PURE__ */ jsxRuntimeExports.jsxs(
42253
+ "div",
42254
+ {
42255
+ style: {
42256
+ border: "1px solid var(--fc-border)",
42257
+ borderRadius: 4,
42258
+ padding: 8,
42259
+ background: "var(--fc-bgSubtle, var(--fc-bgSurface, #1c2128))",
42260
+ userSelect: "none",
42261
+ WebkitUserSelect: "none",
42262
+ ...expandedLayout ? {
42263
+ display: "flex",
42264
+ flexDirection: "column"
42265
+ } : {},
42266
+ ...fullScreen ? {
42267
+ position: "fixed",
42268
+ inset: 16,
42269
+ zIndex: 2200,
42270
+ boxShadow: "0 18px 52px rgba(0, 0, 0, 0.45)",
42271
+ boxSizing: "border-box",
42272
+ color: "var(--fc-text)"
42273
+ } : {}
42274
+ },
42275
+ children: [
42276
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 6, marginBottom: 6 }, children: [
42277
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42278
+ "select",
42279
+ {
42280
+ "aria-label": `${layoutDef.name} selected item`,
42281
+ value: (selectedItem == null ? void 0 : selectedItem.id) ?? "",
42282
+ onChange: (event) => setSelectedId(event.target.value),
42283
+ style: { ...selectStyle$1, minWidth: 0, flex: "1 1 auto" },
42284
+ children: items.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: item.id, children: item.label ?? item.id }, item.id))
42285
+ }
42286
+ ),
42287
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", gap: 4 }, children: [
42288
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setViewFrame(null), title: "Fit placement sheet in editor", style: buttonStyle$1, children: "Fit" }),
42289
+ allowFullScreen && /* @__PURE__ */ jsxRuntimeExports.jsx(
42290
+ "button",
42291
+ {
42292
+ type: "button",
42293
+ onClick: () => setFullScreen(!fullScreen),
42294
+ title: fullScreen ? "Close expanded editor" : "Expand editor",
42295
+ style: buttonStyle$1,
42296
+ children: fullScreen ? "Close" : "Full"
42297
+ }
42298
+ )
42299
+ ] })
42300
+ ] }),
42301
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42302
+ "canvas",
42303
+ {
42304
+ ref: canvasRef,
42305
+ height: 240,
42306
+ "aria-label": `${layoutDef.name} placement editor`,
42307
+ onPointerDown: (event) => {
42308
+ event.preventDefault();
42309
+ const pos = pointerPoint(event);
42310
+ const hit = itemAtPointer(event.currentTarget, pos.x, pos.y);
42311
+ if (!hit) {
42312
+ panStart.current = { x: pos.x, y: pos.y, frame: canvasFrame(event.currentTarget) };
42313
+ } else {
42314
+ setSelectedId(hit.id);
42315
+ const model = screenToPath2D(pos.x, pos.y, canvasFrame(event.currentTarget));
42316
+ if (!hit.locked) {
42317
+ draftItemsRef.current = null;
42318
+ setDraftItems(null);
42319
+ dragRef.current = { id: hit.id, offsetX: model.x - hit.x, offsetY: model.y - hit.y };
42320
+ }
42321
+ }
42322
+ event.currentTarget.setPointerCapture(event.pointerId);
42323
+ },
42324
+ onPointerMove: (event) => {
42325
+ const pos = pointerPoint(event);
42326
+ const drag = dragRef.current;
42327
+ if (drag) {
42328
+ const item = items.find((candidate) => candidate.id === drag.id);
42329
+ if (!item) return;
42330
+ const model = screenToPath2D(pos.x, pos.y, canvasFrame(event.currentTarget));
42331
+ const center = { x: model.x - drag.offsetX, y: model.y - drag.offsetY };
42332
+ const zone = placement2DZoneAtPoint(layoutDef.zones, center);
42333
+ previewDraggedItem({ ...item, x: center.x, y: center.y, zone: (zone == null ? void 0 : zone.id) ?? item.zone });
42334
+ return;
42335
+ }
42336
+ if (panStart.current) {
42337
+ setViewFrame(panPath2DFrame(panStart.current.frame, pos.x - panStart.current.x, pos.y - panStart.current.y));
42338
+ }
42339
+ },
42340
+ onPointerUp: (event) => {
42341
+ const drag = dragRef.current;
42342
+ if (drag) commitDraftDrag(drag.id);
42343
+ dragRef.current = null;
42344
+ panStart.current = null;
42345
+ setBlockedId(null);
42346
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
42347
+ },
42348
+ onPointerCancel: (event) => {
42349
+ if (dragRef.current) cancelDraftDrag();
42350
+ dragRef.current = null;
42351
+ panStart.current = null;
42352
+ setBlockedId(null);
42353
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
42354
+ },
42355
+ style: {
42356
+ display: "block",
42357
+ width: "100%",
42358
+ height: canvasHeight,
42359
+ borderRadius: 3,
42360
+ background: "var(--fc-bg)",
42361
+ touchAction: "none"
42362
+ }
42363
+ }
42364
+ ),
42365
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "grid", gridTemplateColumns: "minmax(0, 1fr) auto", gap: 8, alignItems: "center", marginTop: 6 }, children: [
42366
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { color: "var(--fc-textDim)", fontSize: 10, fontVariantNumeric: "tabular-nums", minWidth: 0 }, children: [
42367
+ selectedItem ? `${selectedItem.id} x ${selectedItem.x}${unitLabel} | y ${selectedItem.y}${unitLabel}` : "No item",
42368
+ collisionCount > 0 ? ` | ${collisionCount} overlap${collisionCount === 1 ? "" : "s"}` : "",
42369
+ boundsCount > 0 ? ` | ${boundsCount} out of bounds` : ""
42370
+ ] }),
42371
+ selectedItem && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
42372
+ layoutDef.zones.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
42373
+ "select",
42374
+ {
42375
+ "aria-label": `${layoutDef.name} selected item zone`,
42376
+ value: selectedItem.zone ?? ((_b2 = layoutDef.zones[0]) == null ? void 0 : _b2.id) ?? "",
42377
+ onChange: (event) => setSelectedZone(event.target.value),
42378
+ style: selectStyle$1,
42379
+ children: layoutDef.zones.map((zone) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: zone.id, children: zone.label ?? zone.id }, zone.id))
42380
+ }
42381
+ ),
42382
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42383
+ "button",
42384
+ {
42385
+ type: "button",
42386
+ onClick: () => setSelectedAngle(selectedItem.angle - 15),
42387
+ title: "Rotate selected item left",
42388
+ style: buttonStyle$1,
42389
+ children: "-15"
42390
+ }
42391
+ ),
42392
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { color: "var(--fc-textDim)", fontSize: 10, minWidth: 42, textAlign: "center" }, children: [
42393
+ selectedItem.angle,
42394
+ " deg"
42395
+ ] }),
42396
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42397
+ "button",
42398
+ {
42399
+ type: "button",
42400
+ onClick: () => setSelectedAngle(selectedItem.angle + 15),
42401
+ title: "Rotate selected item right",
42402
+ style: buttonStyle$1,
42403
+ children: "+15"
42404
+ }
42405
+ )
42406
+ ] })
42407
+ ] })
42408
+ ]
42409
+ }
42410
+ )
42411
+ ] });
42412
+ }
42413
+ const SPLINE_2D_CONTINUITIES = ["G0", "G1", "G2"];
42414
+ function continuityFromOverride(value, fallback) {
42415
+ if (value === "G0" || value === "G1" || value === "G2") return value;
42416
+ if (typeof value === "number") {
42417
+ const index = Math.round(value);
42418
+ return SPLINE_2D_CONTINUITIES[index] ?? fallback;
42419
+ }
42420
+ return fallback;
42421
+ }
42422
+ function currentSpline2DPoints(curveDef, overrides) {
42423
+ const countOverride = overrides[`${curveDef.name}.__count__`];
42424
+ const count = typeof countOverride === "number" ? clamp(Math.round(countOverride), curveDef.minPoints, curveDef.maxPoints) : curveDef.points.length;
42425
+ const points = [];
42426
+ for (let i = 0; i < count; i += 1) {
42427
+ const base = curveDef.points[i] ?? curveDef.defaultPoints[i] ?? { x: 0, y: 0, g: "G2" };
42428
+ const rawX = overrides[`${curveDef.name}[${i}].x`];
42429
+ const rawY = overrides[`${curveDef.name}[${i}].y`];
42430
+ const rawG = overrides[`${curveDef.name}[${i}].g`];
42431
+ points.push({
42432
+ x: typeof rawX === "number" ? rawX : base.x,
42433
+ y: typeof rawY === "number" ? rawY : base.y,
42434
+ g: continuityFromOverride(rawG, base.g)
42435
+ });
42436
+ }
42437
+ return points;
42438
+ }
42439
+ function spline2DPatch(curveName, points) {
42440
+ const patch = { [`${curveName}.__count__`]: points.length };
42441
+ points.forEach((point, index) => {
42442
+ patch[`${curveName}[${index}].x`] = point.x;
42443
+ patch[`${curveName}[${index}].y`] = point.y;
42444
+ patch[`${curveName}[${index}].g`] = point.g;
42445
+ });
42446
+ return patch;
42447
+ }
42448
+ function trailingSpline2DKeys(curveName, oldCount, newCount) {
42449
+ const keys = [];
42450
+ for (let i = newCount; i < oldCount; i += 1) {
42451
+ keys.push(`${curveName}[${i}].x`, `${curveName}[${i}].y`, `${curveName}[${i}].g`);
42452
+ }
42453
+ return keys;
42454
+ }
42455
+ function splineContinuityColor(continuity) {
42456
+ if (continuity === "G0") return "#d65f45";
42457
+ if (continuity === "G1") return "#e2b647";
42458
+ return "#4d93bf";
42459
+ }
42460
+ const buttonStyle = {
42461
+ background: "var(--fc-small-button-bg, none)",
42462
+ border: "1px solid var(--fc-small-button-border, var(--fc-border))",
42463
+ borderRadius: 3,
42464
+ color: "var(--fc-textDim)",
42465
+ fontSize: 10,
42466
+ padding: "1px 5px",
42467
+ cursor: "pointer",
42468
+ lineHeight: "14px",
42469
+ userSelect: "none"
42470
+ };
42471
+ const selectStyle = {
42472
+ background: "var(--fc-bg)",
42473
+ color: "var(--fc-text)",
42474
+ border: "1px solid var(--fc-border)",
42475
+ borderRadius: 3,
42476
+ padding: "1px 4px",
42477
+ fontSize: 11,
42478
+ cursor: "pointer",
42479
+ lineHeight: "16px"
42480
+ };
42481
+ function drawSmoothPreview(ctx, projected, points, closed) {
42482
+ if (projected.length < 2) return;
42483
+ ctx.beginPath();
42484
+ ctx.moveTo(projected[0].x, projected[0].y);
42485
+ const segmentCount = closed ? projected.length : projected.length - 1;
42486
+ for (let index = 0; index < segmentCount; index += 1) {
42487
+ const nextIndex = (index + 1) % projected.length;
42488
+ const current = projected[index];
42489
+ const next = projected[nextIndex];
42490
+ const currentG = points[index].g;
42491
+ const nextG = points[nextIndex].g;
42492
+ if (currentG === "G0" || nextG === "G0") {
42493
+ ctx.lineTo(next.x, next.y);
42494
+ continue;
42495
+ }
42496
+ const prev = projected[index === 0 ? closed ? projected.length - 1 : 0 : index - 1];
42497
+ const after = projected[nextIndex === projected.length - 1 ? closed ? 0 : nextIndex : nextIndex + 1];
42498
+ const reach = currentG === "G2" || nextG === "G2" ? 1 / 6 : 1 / 8;
42499
+ const c1 = { x: current.x + (next.x - prev.x) * reach, y: current.y + (next.y - prev.y) * reach };
42500
+ const c2 = { x: next.x - (after.x - current.x) * reach, y: next.y - (after.y - current.y) * reach };
42501
+ ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, next.x, next.y);
42502
+ }
42503
+ if (closed) ctx.closePath();
42504
+ ctx.stroke();
42505
+ }
42506
+ function Spline2DParamEditor({
42507
+ curveDef,
42508
+ allowFullScreen = true,
42509
+ large = false
42510
+ }) {
42511
+ const canvasRef = reactExports.useRef(null);
42512
+ const dragState = reactExports.useRef(null);
42513
+ const draftPointsRef = reactExports.useRef(null);
42514
+ const panStart = reactExports.useRef(null);
42515
+ const paramOverrides = useForgeStore((state2) => state2.paramOverrides);
42516
+ const setParams = useForgeStore((state2) => state2.setParams);
42517
+ const [expanded, setExpanded] = reactExports.useState(true);
42518
+ const [fullScreen, setFullScreen] = reactExports.useState(false);
42519
+ const [viewFrame, setViewFrame] = reactExports.useState(null);
42520
+ const [draftPoints, setDraftPoints] = reactExports.useState(null);
42521
+ const [selected, setSelected] = reactExports.useState(0);
42522
+ const [selectedIndices, setSelectedIndices] = reactExports.useState([0]);
42523
+ useEscapeAction(
42524
+ () => {
42525
+ setFullScreen(false);
42526
+ return true;
42527
+ },
42528
+ { active: fullScreen, label: `${curveDef.name} spline editor`, priority: ESCAPE_PRIORITY.modal }
42529
+ );
42530
+ const committedPoints = reactExports.useMemo(() => currentSpline2DPoints(curveDef, paramOverrides), [curveDef, paramOverrides]);
42531
+ const points = draftPoints ?? committedPoints;
42532
+ const activeIndex = points.length > 0 ? Math.min(selected, points.length - 1) : 0;
42533
+ const normalizedSelectedIndices = reactExports.useMemo(
42534
+ () => normalizePointSelection(selectedIndices, points.length, activeIndex),
42535
+ [activeIndex, points.length, selectedIndices]
42536
+ );
42537
+ const selectedIndexSet = reactExports.useMemo(() => new Set(normalizedSelectedIndices), [normalizedSelectedIndices]);
42538
+ const selectedPoint = points[activeIndex] ?? points[0];
42539
+ const unitLabel = curveDef.unit ? ` ${curveDef.unit}` : "";
42540
+ const canAdd = points.length < curveDef.maxPoints;
42541
+ const canRemove = points.length - normalizedSelectedIndices.length >= curveDef.minPoints;
42542
+ reactExports.useEffect(() => {
42543
+ if (selected >= points.length) setSelected(Math.max(0, points.length - 1));
42544
+ }, [points.length, selected]);
42545
+ reactExports.useEffect(() => {
42546
+ const canvas = canvasRef.current;
42547
+ if (!canvas) return;
42548
+ const ctx = canvas.getContext("2d");
42549
+ if (!ctx) return;
42550
+ const draw = () => {
42551
+ const rect = canvas.getBoundingClientRect();
42552
+ const dpr = window.devicePixelRatio || 1;
42553
+ const nextWidth = Math.max(1, Math.round(rect.width * dpr));
42554
+ const nextHeight = Math.max(1, Math.round(rect.height * dpr));
42555
+ if (canvas.width !== nextWidth) canvas.width = nextWidth;
42556
+ if (canvas.height !== nextHeight) canvas.height = nextHeight;
42557
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
42558
+ ctx.clearRect(0, 0, rect.width, rect.height);
42559
+ const frame2 = viewFrame ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, curveDef);
42560
+ drawPath2DGrid(ctx, rect.width, rect.height, frame2);
42561
+ if (points.length === 0) return;
42562
+ const projected = points.map((point) => path2DToScreen(point, frame2));
42563
+ ctx.save();
42564
+ ctx.setLineDash([5, 5]);
42565
+ ctx.strokeStyle = "rgba(255,255,255,0.18)";
42566
+ ctx.lineWidth = 1;
42567
+ ctx.beginPath();
42568
+ ctx.moveTo(projected[0].x, projected[0].y);
42569
+ for (let index = 1; index < projected.length; index += 1) ctx.lineTo(projected[index].x, projected[index].y);
42570
+ if (curveDef.closed) ctx.closePath();
42571
+ ctx.stroke();
42572
+ ctx.restore();
42573
+ ctx.strokeStyle = "#4d93bf";
42574
+ ctx.lineWidth = 4;
42575
+ drawSmoothPreview(ctx, projected, points, curveDef.closed);
42576
+ projected.forEach((point, index) => {
42577
+ const isSelected = selectedIndexSet.has(index);
42578
+ const isActive = index === activeIndex;
42579
+ ctx.beginPath();
42580
+ ctx.arc(point.x, point.y, isActive ? 8 : isSelected ? 7 : 6, 0, Math.PI * 2);
42581
+ ctx.fillStyle = splineContinuityColor(points[index].g);
42582
+ ctx.fill();
42583
+ ctx.lineWidth = isActive ? 3 : isSelected ? 2.25 : 1.5;
42584
+ ctx.strokeStyle = isSelected ? "#fff6d4" : "#171914";
42585
+ ctx.stroke();
42586
+ });
42587
+ };
42588
+ draw();
42589
+ const observer = new ResizeObserver(draw);
42590
+ observer.observe(canvas);
42591
+ return () => observer.disconnect();
42592
+ }, [activeIndex, curveDef, fullScreen, large, points, selectedIndexSet, viewFrame]);
42593
+ reactExports.useEffect(() => {
42594
+ const canvas = canvasRef.current;
42595
+ if (!canvas) return;
42596
+ const handleWheel = (event) => {
42597
+ event.preventDefault();
42598
+ event.stopPropagation();
42599
+ const rect = canvas.getBoundingClientRect();
42600
+ const x = event.clientX - rect.left;
42601
+ const y = event.clientY - rect.top;
42602
+ setViewFrame(
42603
+ (current) => zoomPath2DFrame(current ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, curveDef), x, y, event.deltaY > 0 ? 0.88 : 1.14)
42604
+ );
42605
+ };
42606
+ canvas.addEventListener("wheel", handleWheel, { passive: false });
42607
+ return () => canvas.removeEventListener("wheel", handleWheel);
42608
+ }, [curveDef]);
42609
+ const setContinuityForSelection = (continuity) => {
42610
+ const nextPoints = points.map((point, index) => selectedIndexSet.has(index) ? { ...point, g: continuity } : point);
42611
+ setParams(spline2DPatch(curveDef.name, nextPoints));
42612
+ };
42613
+ const pointerPoint = (event) => {
42614
+ const canvas = event.currentTarget;
42615
+ const rect = canvas.getBoundingClientRect();
42616
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
42617
+ };
42618
+ const nearestHandle = (canvas, x, y) => {
42619
+ const frame2 = viewFrame ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, curveDef);
42620
+ let bestIndex = null;
42621
+ let bestDistance = Infinity;
42622
+ points.forEach((point, index) => {
42623
+ const screen2 = path2DToScreen(point, frame2);
42624
+ const distance = Math.hypot(screen2.x - x, screen2.y - y);
42625
+ if (distance < bestDistance) {
42626
+ bestDistance = distance;
42627
+ bestIndex = index;
42628
+ }
42629
+ });
42630
+ return bestDistance <= 18 ? bestIndex : null;
42631
+ };
42632
+ const moveDraggedPoints = (drag, pointer) => {
42633
+ const dx = pointer.x - drag.startPointer.x;
42634
+ const dy = pointer.y - drag.startPointer.y;
42635
+ const dragIndices = new Set(drag.indices);
42636
+ const nextPoints = drag.startPoints.map(
42637
+ (point, index) => dragIndices.has(index) ? {
42638
+ ...point,
42639
+ x: roundToStep(point.x + dx, curveDef.x.step),
42640
+ y: roundToStep(point.y + dy, curveDef.y.step)
42641
+ } : point
42642
+ );
42643
+ draftPointsRef.current = nextPoints;
42644
+ setDraftPoints(nextPoints);
42645
+ };
42646
+ const commitDraftDrag = () => {
42647
+ const nextPoints = draftPointsRef.current;
42648
+ draftPointsRef.current = null;
42649
+ setDraftPoints(null);
42650
+ if (nextPoints) setParams(spline2DPatch(curveDef.name, nextPoints));
42651
+ };
42652
+ const cancelDraftDrag = () => {
42653
+ draftPointsRef.current = null;
42654
+ setDraftPoints(null);
42655
+ };
42656
+ const insertPointAfterSelection = () => {
42657
+ if (!canAdd) return;
42658
+ const insertAt = Math.min(activeIndex + 1, points.length);
42659
+ const a2 = points[activeIndex] ?? points[points.length - 1] ?? { x: 0, y: 0, g: "G2" };
42660
+ const b2 = points[insertAt] ?? (curveDef.closed ? points[0] : a2);
42661
+ const nextPoint = {
42662
+ x: roundToStep((a2.x + b2.x) / 2, curveDef.x.step),
42663
+ y: roundToStep((a2.y + b2.y) / 2, curveDef.y.step),
42664
+ g: a2.g === "G0" ? "G1" : a2.g
42665
+ };
42666
+ const nextPoints = [...points];
42667
+ nextPoints.splice(insertAt, 0, nextPoint);
42668
+ setSelected(insertAt);
42669
+ setSelectedIndices([insertAt]);
42670
+ setParams(spline2DPatch(curveDef.name, nextPoints));
42671
+ };
42672
+ const removeSelectedPoint = () => {
42673
+ if (!canRemove) return;
42674
+ const removeIndices = new Set(normalizedSelectedIndices);
42675
+ const nextPoints = points.filter((_, index) => !removeIndices.has(index));
42676
+ const firstRemoved = normalizedSelectedIndices[0] ?? activeIndex;
42677
+ const nextSelected = Math.max(0, Math.min(firstRemoved, nextPoints.length - 1));
42678
+ setSelected(nextSelected);
42679
+ setSelectedIndices([nextSelected]);
42680
+ setParams(spline2DPatch(curveDef.name, nextPoints), trailingSpline2DKeys(curveDef.name, points.length, nextPoints.length));
42681
+ };
42682
+ const handleButtonPointerDown = (event, action) => {
42683
+ event.preventDefault();
42684
+ event.stopPropagation();
42685
+ action();
42686
+ };
42687
+ const handleButtonKeyDown = (event, action) => {
42688
+ if (event.key !== "Enter" && event.key !== " ") return;
42689
+ event.preventDefault();
42690
+ event.stopPropagation();
42691
+ action();
42692
+ };
42693
+ const canvasFrame = (canvas) => viewFrame ?? frameForPath2D(canvas.clientWidth, canvas.clientHeight, curveDef);
42694
+ const canvasHeight = large ? "min(62vh, 640px)" : fullScreen ? "calc(100vh - 150px)" : 220;
42695
+ const expandedLayout = large || fullScreen;
42696
+ const editorPanel = /* @__PURE__ */ jsxRuntimeExports.jsxs(
42697
+ "div",
42698
+ {
42699
+ style: {
42700
+ border: "1px solid var(--fc-border)",
42701
+ borderRadius: 4,
42702
+ padding: 8,
42703
+ background: "var(--fc-bgSubtle, var(--fc-bgSurface, #1c2128))",
42704
+ userSelect: "none",
42705
+ WebkitUserSelect: "none",
42706
+ ...expandedLayout ? {
42707
+ display: "flex",
42708
+ flexDirection: "column"
42709
+ } : {},
42710
+ ...fullScreen ? {
42711
+ position: "fixed",
42712
+ inset: 16,
42713
+ zIndex: 2200,
42714
+ boxShadow: "0 18px 52px rgba(0, 0, 0, 0.45)",
42715
+ boxSizing: "border-box",
42716
+ color: "var(--fc-text)"
42717
+ } : {}
42718
+ },
42719
+ children: [
42720
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 4, marginBottom: 6 }, children: [
42721
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setViewFrame(null), title: "Fit spline in editor", style: buttonStyle, children: "Fit" }),
42722
+ allowFullScreen && /* @__PURE__ */ jsxRuntimeExports.jsx(
42723
+ "button",
42724
+ {
42725
+ type: "button",
42726
+ onClick: () => setFullScreen(!fullScreen),
42727
+ title: fullScreen ? "Close expanded editor" : "Expand editor",
42728
+ style: buttonStyle,
42729
+ children: fullScreen ? "Close" : "Full"
42730
+ }
42731
+ )
42732
+ ] }),
42733
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42734
+ "canvas",
42735
+ {
42736
+ ref: canvasRef,
42737
+ height: 220,
42738
+ "aria-label": `${curveDef.name} spline editor`,
42739
+ onPointerDown: (event) => {
42740
+ event.preventDefault();
42741
+ const pos = pointerPoint(event);
42742
+ const hit = nearestHandle(event.currentTarget, pos.x, pos.y);
42743
+ if (hit === null) panStart.current = { x: pos.x, y: pos.y, frame: canvasFrame(event.currentTarget) };
42744
+ else {
42745
+ const nextSelection = pointSelectionForPointerDown(
42746
+ normalizedSelectedIndices,
42747
+ hit,
42748
+ activeIndex,
42749
+ hasPointSelectionModifier(event),
42750
+ points.length
42751
+ );
42752
+ setSelected(nextSelection.activeIndex);
42753
+ setSelectedIndices(nextSelection.selectedIndices);
42754
+ if (nextSelection.shouldDrag) {
42755
+ const frame2 = canvasFrame(event.currentTarget);
42756
+ draftPointsRef.current = null;
42757
+ setDraftPoints(null);
42758
+ dragState.current = {
42759
+ indices: nextSelection.selectedIndices,
42760
+ startPointer: screenToPath2D(pos.x, pos.y, frame2),
42761
+ startPoints: points.map((point) => ({ ...point })),
42762
+ frame: frame2
42763
+ };
42764
+ }
42765
+ }
42766
+ event.currentTarget.setPointerCapture(event.pointerId);
42767
+ },
42768
+ onPointerMove: (event) => {
42769
+ const pos = pointerPoint(event);
42770
+ if (dragState.current) {
42771
+ moveDraggedPoints(dragState.current, screenToPath2D(pos.x, pos.y, dragState.current.frame));
42772
+ return;
42773
+ }
42774
+ if (panStart.current) {
42775
+ setViewFrame(panPath2DFrame(panStart.current.frame, pos.x - panStart.current.x, pos.y - panStart.current.y));
42776
+ }
42777
+ },
42778
+ onPointerUp: (event) => {
42779
+ if (dragState.current) commitDraftDrag();
42780
+ dragState.current = null;
42781
+ panStart.current = null;
42782
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
42783
+ },
42784
+ onPointerCancel: (event) => {
42785
+ if (dragState.current) cancelDraftDrag();
42786
+ dragState.current = null;
42787
+ panStart.current = null;
42788
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
42789
+ },
42790
+ style: { display: "block", width: "100%", height: canvasHeight, borderRadius: 3, background: "var(--fc-bg)", touchAction: "none" }
42791
+ }
42792
+ ),
42793
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, marginTop: 6 }, children: [
42794
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { color: "var(--fc-textDim)", fontSize: 10, fontVariantNumeric: "tabular-nums", minWidth: 0 }, children: [
42795
+ normalizedSelectedIndices.length > 1 ? `${normalizedSelectedIndices.length} selected · ` : "",
42796
+ "P",
42797
+ Math.min(activeIndex + 1, points.length),
42798
+ " x ",
42799
+ selectedPoint ? selectedPoint.x : 0,
42800
+ unitLabel,
42801
+ " · y ",
42802
+ selectedPoint ? selectedPoint.y : 0,
42803
+ unitLabel
42804
+ ] }),
42805
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", gap: 4, alignItems: "center" }, children: [
42806
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42807
+ "select",
42808
+ {
42809
+ "aria-label": `${curveDef.name} selected point continuity`,
42810
+ value: (selectedPoint == null ? void 0 : selectedPoint.g) ?? "G2",
42811
+ onChange: (event) => setContinuityForSelection(event.target.value),
42812
+ style: selectStyle,
42813
+ children: SPLINE_2D_CONTINUITIES.map((continuity) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: continuity, children: continuity }, continuity))
42814
+ }
42815
+ ),
42816
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42817
+ "button",
42818
+ {
42819
+ type: "button",
42820
+ onPointerDown: (event) => handleButtonPointerDown(event, insertPointAfterSelection),
42821
+ onKeyDown: (event) => handleButtonKeyDown(event, insertPointAfterSelection),
42822
+ disabled: !canAdd,
42823
+ title: "Add point after selected",
42824
+ style: buttonStyle,
42825
+ children: "+ Point"
42826
+ }
42827
+ ),
42828
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42829
+ "button",
42830
+ {
42831
+ type: "button",
42832
+ onPointerDown: (event) => handleButtonPointerDown(event, removeSelectedPoint),
42833
+ onKeyDown: (event) => handleButtonKeyDown(event, removeSelectedPoint),
42834
+ disabled: !canRemove,
42835
+ title: "Remove selected point(s)",
42836
+ style: buttonStyle,
42837
+ children: "Remove"
42838
+ }
42839
+ )
42840
+ ] })
42841
+ ] })
42842
+ ]
42843
+ }
42844
+ );
42845
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { marginBottom: 8 }, children: [
42846
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
42847
+ "div",
42848
+ {
42849
+ onClick: () => setExpanded(!expanded),
42850
+ style: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 12, cursor: "pointer", marginBottom: 4 },
42851
+ children: [
42852
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { color: "var(--fc-text)", fontWeight: 600 }, children: [
42853
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontSize: 9, marginRight: 4 }, children: expanded ? "▼" : "▶" }),
42854
+ curveDef.name,
42855
+ " (",
42856
+ points.length,
42857
+ ")"
42858
+ ] }),
42859
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { color: "var(--fc-textDim)", fontSize: 10 }, children: [
42860
+ "Spline d",
42861
+ curveDef.degree
42862
+ ] })
42863
+ ]
42864
+ }
42865
+ ),
42866
+ expanded && editorPanel
42867
+ ] });
42868
+ }
42869
+ const SPATIAL_PARAM_KIND_COLORS = {
42870
+ scalar: "#46b6a9",
42871
+ text: "#9ca3af",
42872
+ list: "#e5a83c",
42873
+ path2d: "#e5a83c",
42874
+ spline2d: "#5da9e9",
42875
+ placement2d: "#c77dff"
42876
+ };
42877
+ function spatialParamAnchorPosition(anchor) {
42878
+ return anchor.kind === "point" ? anchor.at : anchor.origin;
42879
+ }
42880
+ function spatialParamAnchorLabel(name, anchor) {
42881
+ return anchor.label ?? name;
42882
+ }
42883
+ function scalarSummary(param) {
42884
+ if (param.boolean) return param.value >= 0.5 ? "on" : "off";
42885
+ if (param.choices) return param.choices[Math.max(0, Math.min(Math.round(param.value), param.choices.length - 1))] ?? String(param.value);
42886
+ return `${param.value}${param.unit ? ` ${param.unit}` : ""}`;
42887
+ }
42888
+ function collectSpatialParamAnchors(params, stringParams, listParams, path2dParams, spline2dParams, placement2dParams) {
42889
+ const anchors = [];
42890
+ params.forEach((param) => {
42891
+ if (param.anchor) anchors.push({ name: param.name, kind: "scalar", anchor: param.anchor, summary: scalarSummary(param) });
42892
+ });
42893
+ stringParams.forEach((param) => {
42894
+ if (param.anchor) anchors.push({ name: param.name, kind: "text", anchor: param.anchor, summary: param.value });
42895
+ });
42896
+ listParams.forEach((param) => {
42897
+ if (param.anchor) anchors.push({ name: param.name, kind: "list", anchor: param.anchor, summary: `${param.items.length} items` });
42898
+ });
42899
+ path2dParams.forEach((param) => {
42900
+ if (param.anchor)
42901
+ anchors.push({ name: param.name, kind: "path2d", anchor: param.anchor, summary: `${param.points.length} pts`, pathDef: param });
42902
+ });
42903
+ spline2dParams.forEach((param) => {
42904
+ if (param.anchor)
42905
+ anchors.push({ name: param.name, kind: "spline2d", anchor: param.anchor, summary: `${param.points.length} pts`, splineDef: param });
42906
+ });
42907
+ placement2dParams.forEach((param) => {
42908
+ if (param.anchor)
42909
+ anchors.push({
42910
+ name: param.name,
42911
+ kind: "placement2d",
42912
+ anchor: param.anchor,
42913
+ summary: `${param.items.length} items`,
42914
+ placementDef: param
42915
+ });
42916
+ });
42917
+ return anchors;
42918
+ }
42919
+ function stopSpatialPointerDown$1(event) {
42920
+ event.stopPropagation();
42921
+ }
42922
+ function preventControlTextSelection$1(event) {
42923
+ const target = event.target;
42924
+ if (target instanceof HTMLElement && target.closest("input, textarea, select, button, canvas")) return;
42925
+ event.preventDefault();
42926
+ }
42927
+ function preventMarkerDrag(event) {
42928
+ event.preventDefault();
42929
+ }
42930
+ function stopSpatialWheel$1(event) {
42931
+ event.stopPropagation();
42932
+ }
42933
+ function SpatialParamMarker({ item }) {
42934
+ const focusedParamName = useForgeStore((s) => s.focusedParamName);
42935
+ const focusParam = useForgeStore((s) => s.focusParam);
42936
+ const openSpatialParamSheet = useForgeStore((s) => s.openSpatialParamSheet);
42937
+ const closeSpatialParamSheet = useForgeStore((s) => s.closeSpatialParamSheet);
42938
+ const selected = focusedParamName === item.name;
42939
+ const color = item.anchor.color ?? SPATIAL_PARAM_KIND_COLORS[item.kind];
42940
+ const isManual = item.kind === "path2d" || item.kind === "spline2d" || item.kind === "placement2d";
42941
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
42942
+ Html,
42943
+ {
42944
+ position: spatialParamAnchorPosition(item.anchor),
42945
+ zIndexRange: [90, 0],
42946
+ style: { pointerEvents: "auto", userSelect: "none", WebkitUserSelect: "none" },
42947
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
42948
+ "button",
42949
+ {
42950
+ type: "button",
42951
+ "aria-label": `Focus parameter ${item.name}`,
42952
+ draggable: false,
42953
+ onDragStart: preventMarkerDrag,
42954
+ onPointerDown: (event) => {
42955
+ event.stopPropagation();
42956
+ event.preventDefault();
42957
+ },
42958
+ onClick: (event) => {
42959
+ event.stopPropagation();
42960
+ focusParam(item.name);
42961
+ if (isManual) openSpatialParamSheet(item.name);
42962
+ else closeSpatialParamSheet();
42963
+ },
42964
+ style: {
42965
+ display: "inline-flex",
42966
+ gap: 7,
42967
+ alignItems: "center",
42968
+ width: "max-content",
42969
+ maxWidth: 220,
42970
+ minWidth: 0,
42971
+ minHeight: 0,
42972
+ padding: 0,
42973
+ border: 0,
42974
+ background: "transparent",
42975
+ color: "var(--fc-text, #f8fafc)",
42976
+ cursor: "pointer",
42977
+ font: "inherit",
42978
+ lineHeight: 1,
42979
+ transform: "translate(8px, -50%)",
42980
+ userSelect: "none",
42981
+ WebkitUserSelect: "none",
42982
+ touchAction: "none"
42983
+ },
42984
+ children: [
42985
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
42986
+ "span",
42987
+ {
42988
+ style: {
42989
+ width: 18,
42990
+ height: 18,
42991
+ flex: "0 0 18px",
42992
+ borderRadius: 999,
42993
+ background: color,
42994
+ border: selected ? "2px solid #fff6d4" : "2px solid rgba(255,255,255,0.78)",
42995
+ boxShadow: selected ? "0 0 0 4px rgba(255, 246, 212, 0.18), 0 8px 20px rgba(0,0,0,0.32)" : "0 8px 20px rgba(0,0,0,0.32)"
42996
+ }
42997
+ }
42998
+ ),
42999
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
43000
+ "span",
43001
+ {
43002
+ style: {
43003
+ padding: "4px 7px",
43004
+ borderRadius: 4,
43005
+ border: selected ? `1px solid ${color}` : "1px solid rgba(255,255,255,0.14)",
43006
+ background: "rgba(18, 24, 27, 0.9)",
43007
+ boxShadow: "0 10px 22px rgba(0,0,0,0.24)",
43008
+ boxSizing: "border-box",
43009
+ display: "block",
43010
+ maxWidth: 176,
43011
+ minWidth: 0,
43012
+ overflow: "hidden",
43013
+ fontSize: 11,
43014
+ fontWeight: 700,
43015
+ lineHeight: 1.15,
43016
+ textAlign: "left",
43017
+ whiteSpace: "nowrap",
43018
+ textOverflow: "ellipsis",
43019
+ userSelect: "none",
43020
+ WebkitUserSelect: "none"
43021
+ },
43022
+ children: [
43023
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { display: "block", overflow: "hidden", textOverflow: "ellipsis" }, children: spatialParamAnchorLabel(item.name, item.anchor) }),
43024
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43025
+ "span",
43026
+ {
43027
+ style: {
43028
+ display: "block",
43029
+ overflow: "hidden",
43030
+ textOverflow: "ellipsis",
43031
+ color: "var(--fc-textDim, #9ca3af)",
43032
+ fontSize: 10,
43033
+ fontWeight: 650
43034
+ },
43035
+ children: item.summary
43036
+ }
43037
+ )
43038
+ ]
43039
+ }
43040
+ )
43041
+ ]
43042
+ }
43043
+ )
43044
+ }
43045
+ );
43046
+ }
43047
+ function SpatialParamSheet({ item }) {
43048
+ const expandSpatialParamSheet = useForgeStore((s) => s.expandSpatialParamSheet);
43049
+ const closeSpatialParamSheet = useForgeStore((s) => s.closeSpatialParamSheet);
43050
+ useEscapeAction(
43051
+ () => {
43052
+ closeSpatialParamSheet();
43053
+ return true;
43054
+ },
43055
+ { active: true, label: "Spatial parameter sheet", priority: ESCAPE_PRIORITY.popover }
43056
+ );
43057
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
43058
+ Html,
43059
+ {
43060
+ position: spatialParamAnchorPosition(item.anchor),
43061
+ zIndexRange: [95, 0],
43062
+ style: { pointerEvents: "auto", userSelect: "none", WebkitUserSelect: "none" },
43063
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
43064
+ "div",
43065
+ {
43066
+ className: "fc-viewport-floating-panel fc-spatial-param-sheet",
43067
+ onPointerDown: stopSpatialPointerDown$1,
43068
+ onMouseDown: preventControlTextSelection$1,
43069
+ onWheel: stopSpatialWheel$1,
43070
+ style: {
43071
+ width: "clamp(320px, 34vw, 430px)",
43072
+ maxWidth: "calc(100vw - 32px)",
43073
+ maxHeight: "min(72vh, 560px)",
43074
+ transform: "translate(24px, -50%)",
43075
+ border: "1px solid var(--fc-floating-panel-border, var(--fc-border, #333))",
43076
+ borderRadius: 6,
43077
+ background: "var(--fc-floating-panel-bg, rgba(18, 24, 27, 0.96))",
43078
+ boxShadow: "0 18px 52px rgba(0,0,0,0.38)",
43079
+ boxSizing: "border-box",
43080
+ padding: 10,
43081
+ overflow: "hidden",
43082
+ color: "var(--fc-text)",
43083
+ userSelect: "none",
43084
+ WebkitUserSelect: "none"
43085
+ },
43086
+ children: [
43087
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", marginBottom: 8 }, children: [
43088
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { minWidth: 0 }, children: [
43089
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { style: { display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: 12 }, children: item.name }),
43090
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43091
+ "span",
43092
+ {
43093
+ style: {
43094
+ display: "block",
43095
+ overflow: "hidden",
43096
+ textOverflow: "ellipsis",
43097
+ whiteSpace: "nowrap",
43098
+ fontSize: 10,
43099
+ color: "var(--fc-textDim)",
43100
+ marginTop: 1
43101
+ },
43102
+ children: spatialParamAnchorLabel(item.name, item.anchor)
43103
+ }
43104
+ )
43105
+ ] }),
43106
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", gap: 4 }, children: [
43107
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43108
+ "button",
43109
+ {
43110
+ type: "button",
43111
+ onClick: () => expandSpatialParamSheet(item.name),
43112
+ style: {
43113
+ border: "1px solid var(--fc-border)",
43114
+ borderRadius: 4,
43115
+ background: "none",
43116
+ color: "var(--fc-textDim)",
43117
+ cursor: "pointer",
43118
+ fontSize: 11,
43119
+ lineHeight: "16px",
43120
+ padding: "2px 7px"
43121
+ },
43122
+ children: "Full"
43123
+ }
43124
+ ),
43125
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43126
+ "button",
43127
+ {
43128
+ type: "button",
43129
+ onClick: closeSpatialParamSheet,
43130
+ style: {
43131
+ border: "1px solid var(--fc-border)",
43132
+ borderRadius: 4,
43133
+ background: "none",
43134
+ color: "var(--fc-textDim)",
43135
+ cursor: "pointer",
43136
+ fontSize: 11,
43137
+ lineHeight: "16px",
43138
+ padding: "2px 7px"
43139
+ },
43140
+ children: "Close"
43141
+ }
43142
+ )
43143
+ ] })
43144
+ ] }),
43145
+ item.pathDef && /* @__PURE__ */ jsxRuntimeExports.jsx(Path2DParamEditor, { pathDef: item.pathDef, allowFullScreen: false }),
43146
+ item.splineDef && /* @__PURE__ */ jsxRuntimeExports.jsx(Spline2DParamEditor, { curveDef: item.splineDef, allowFullScreen: false }),
43147
+ item.placementDef && /* @__PURE__ */ jsxRuntimeExports.jsx(Placement2DParamEditor, { layoutDef: item.placementDef, allowFullScreen: false })
43148
+ ]
43149
+ }
43150
+ )
43151
+ }
43152
+ );
43153
+ }
43154
+ function SpatialParamAnchorsOverlay() {
43155
+ const params = useForgeStore((s) => s.params);
43156
+ const stringParams = useForgeStore((s) => s.stringParams);
43157
+ const listParams = useForgeStore((s) => s.listParams);
43158
+ const path2dParams = useForgeStore((s) => s.path2dParams);
43159
+ const spline2dParams = useForgeStore((s) => s.spline2dParams);
43160
+ const placement2dParams = useForgeStore((s) => s.placement2dParams);
43161
+ const sheetName = useForgeStore((s) => s.spatialParamSheetName);
43162
+ const anchors = reactExports.useMemo(
43163
+ () => collectSpatialParamAnchors(params, stringParams, listParams, path2dParams, spline2dParams, placement2dParams),
43164
+ [params, stringParams, listParams, path2dParams, placement2dParams, spline2dParams]
43165
+ );
43166
+ const sheetItem = reactExports.useMemo(
43167
+ () => anchors.find((item) => item.name === sheetName && (item.pathDef || item.splineDef || item.placementDef)) ?? null,
43168
+ [anchors, sheetName]
43169
+ );
43170
+ if (anchors.length === 0) return null;
43171
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("group", { renderOrder: 13, children: [
43172
+ anchors.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx(SpatialParamMarker, { item }, item.name)),
43173
+ sheetItem && /* @__PURE__ */ jsxRuntimeExports.jsx(SpatialParamSheet, { item: sheetItem })
43174
+ ] });
43175
+ }
43176
+ function stopSpatialPointerDown(event) {
43177
+ event.stopPropagation();
43178
+ }
43179
+ function preventControlTextSelection(event) {
43180
+ const target = event.target;
43181
+ if (target instanceof HTMLElement && target.closest("input, textarea, select, button, canvas")) return;
43182
+ event.preventDefault();
43183
+ }
43184
+ function stopSpatialWheel(event) {
43185
+ event.stopPropagation();
43186
+ }
43187
+ function SpatialParamExpandedSheetOverlay() {
43188
+ const params = useForgeStore((s) => s.params);
43189
+ const stringParams = useForgeStore((s) => s.stringParams);
43190
+ const listParams = useForgeStore((s) => s.listParams);
43191
+ const path2dParams = useForgeStore((s) => s.path2dParams);
43192
+ const spline2dParams = useForgeStore((s) => s.spline2dParams);
43193
+ const placement2dParams = useForgeStore((s) => s.placement2dParams);
43194
+ const sheetName = useForgeStore((s) => s.expandedSpatialParamSheetName);
43195
+ const closeExpandedSpatialParamSheet = useForgeStore((s) => s.closeExpandedSpatialParamSheet);
43196
+ const anchors = reactExports.useMemo(
43197
+ () => collectSpatialParamAnchors(params, stringParams, listParams, path2dParams, spline2dParams, placement2dParams),
43198
+ [params, stringParams, listParams, path2dParams, placement2dParams, spline2dParams]
43199
+ );
43200
+ const item = reactExports.useMemo(
43201
+ () => anchors.find((anchor) => anchor.name === sheetName && (anchor.pathDef || anchor.splineDef || anchor.placementDef)) ?? null,
43202
+ [anchors, sheetName]
43203
+ );
43204
+ useEscapeAction(
43205
+ () => {
43206
+ closeExpandedSpatialParamSheet();
43207
+ return true;
43208
+ },
43209
+ { active: item !== null, label: "Expanded spatial parameter sheet", priority: ESCAPE_PRIORITY.modal }
43210
+ );
43211
+ if (!item) return null;
43212
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
43213
+ "div",
43214
+ {
43215
+ className: "fc-spatial-param-expanded-overlay",
43216
+ role: "dialog",
43217
+ "aria-modal": "true",
43218
+ "aria-label": `${item.name} spatial parameter editor`,
43219
+ style: {
43220
+ position: "fixed",
43221
+ inset: 0,
43222
+ zIndex: 2200,
43223
+ padding: 24,
43224
+ boxSizing: "border-box",
43225
+ display: "flex",
43226
+ background: "rgba(2, 7, 10, 0.58)",
43227
+ backdropFilter: "blur(2px)",
43228
+ WebkitBackdropFilter: "blur(2px)",
43229
+ pointerEvents: "auto",
43230
+ userSelect: "none",
43231
+ WebkitUserSelect: "none"
43232
+ },
43233
+ onPointerDown: stopSpatialPointerDown,
43234
+ onMouseDown: preventControlTextSelection,
43235
+ onWheel: stopSpatialWheel,
43236
+ onContextMenu: (event) => event.preventDefault(),
43237
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
43238
+ "div",
43239
+ {
43240
+ className: "fc-viewport-floating-panel fc-spatial-param-expanded-surface",
43241
+ style: {
43242
+ display: "flex",
43243
+ flexDirection: "column",
43244
+ width: "100%",
43245
+ minWidth: 0,
43246
+ minHeight: 0,
43247
+ border: "1px solid var(--fc-floating-panel-border, var(--fc-border))",
43248
+ borderRadius: 6,
43249
+ background: "var(--fc-bgPanel, #161b22)",
43250
+ boxShadow: "0 18px 52px rgba(0,0,0,0.42)",
43251
+ padding: 12,
43252
+ color: "var(--fc-text)",
43253
+ overflow: "hidden"
43254
+ },
43255
+ children: [
43256
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, marginBottom: 10 }, children: [
43257
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { minWidth: 0 }, children: [
43258
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 13, fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: item.name }),
43259
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 11, color: "var(--fc-textDim)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: spatialParamAnchorLabel(item.name, item.anchor) })
43260
+ ] }),
43261
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43262
+ "button",
43263
+ {
43264
+ type: "button",
43265
+ onClick: closeExpandedSpatialParamSheet,
43266
+ style: {
43267
+ border: "1px solid var(--fc-border)",
43268
+ borderRadius: 4,
43269
+ background: "transparent",
43270
+ color: "var(--fc-textDim)",
43271
+ cursor: "pointer",
43272
+ fontSize: 12,
43273
+ padding: "3px 8px"
43274
+ },
43275
+ children: "Close"
43276
+ }
43277
+ )
43278
+ ] }),
43279
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { minHeight: 0, overflow: "auto" }, children: [
43280
+ item.pathDef && /* @__PURE__ */ jsxRuntimeExports.jsx(Path2DParamEditor, { pathDef: item.pathDef, allowFullScreen: false, large: true }),
43281
+ item.splineDef && /* @__PURE__ */ jsxRuntimeExports.jsx(Spline2DParamEditor, { curveDef: item.splineDef, allowFullScreen: false, large: true }),
43282
+ item.placementDef && /* @__PURE__ */ jsxRuntimeExports.jsx(Placement2DParamEditor, { layoutDef: item.placementDef, allowFullScreen: false, large: true })
43283
+ ] })
43284
+ ]
43285
+ }
43286
+ )
43287
+ }
43288
+ );
43289
+ }
40799
43290
  const IDLE_STATE = {
40800
43291
  status: "idle",
40801
43292
  error: null,
@@ -40965,7 +43456,7 @@ function useGeometryComparison(args) {
40965
43456
  }, [args.activeBackend, args.candidate, args.enabled, args.files, args.previewFile, args.quality]);
40966
43457
  return state2;
40967
43458
  }
40968
- const PARALLEL_CHANNELS = /* @__PURE__ */ new Set(["thickness", "roughness"]);
43459
+ const PARALLEL_CHANNELS = /* @__PURE__ */ new Set(["thickness", "throughThickness", "roughness"]);
40969
43460
  const MAX_INSPECT_WORKERS = Math.max(
40970
43461
  1,
40971
43462
  Math.min((typeof navigator !== "undefined" && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4) - 1, 8)
@@ -41021,7 +43512,7 @@ function mergeResults(results, channel, analysisId) {
41021
43512
  class InspectWorkerClient {
41022
43513
  constructor(workerFactory = () => new Worker(new URL(
41023
43514
  /* @vite-ignore */
41024
- "/assets/inspectWorker-ymhBV4Ll.js",
43515
+ "/assets/inspectWorker-Cuby2qfT.js",
41025
43516
  import.meta.url
41026
43517
  ), { type: "module" })) {
41027
43518
  __publicField(this, "reqId", 0);
@@ -41075,8 +43566,16 @@ class InspectWorkerClient {
41075
43566
  }
41076
43567
  }
41077
43568
  const inspectWorkerClient = new InspectWorkerClient();
41078
- const WORKER_CHANNELS = /* @__PURE__ */ new Set(["thickness", "roughness", "connectivity", "floating", "distance", "collisions"]);
41079
- const SCALAR_WORKER_CHANNELS = /* @__PURE__ */ new Set(["thickness", "roughness"]);
43569
+ const WORKER_CHANNELS = /* @__PURE__ */ new Set([
43570
+ "thickness",
43571
+ "throughThickness",
43572
+ "roughness",
43573
+ "connectivity",
43574
+ "floating",
43575
+ "distance",
43576
+ "collisions"
43577
+ ]);
43578
+ const SCALAR_WORKER_CHANNELS = /* @__PURE__ */ new Set(["thickness", "throughThickness", "roughness"]);
41080
43579
  const MESH_COMPONENT_WORKER_CHANNELS = /* @__PURE__ */ new Set(["connectivity", "floating", "distance"]);
41081
43580
  const IDENTITY_MATRIX_ELEMENTS = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
41082
43581
  const INSPECT_BUILD_YIELD_INTERVAL_MS = 8;
@@ -41211,14 +43710,14 @@ function addCandidate(candidates, candidate) {
41211
43710
  }
41212
43711
  if (candidate.bboxOverlapVolume > candidates[smallestIndex].bboxOverlapVolume) candidates[smallestIndex] = candidate;
41213
43712
  }
41214
- async function collectCollisionCandidates(entries, maybeYield) {
43713
+ async function collectCollisionCandidates(entries2, maybeYield) {
41215
43714
  const candidates = [];
41216
43715
  let candidateCount = 0;
41217
43716
  let prunedPairCount = 0;
41218
- for (let sourceIndex = 0; sourceIndex < entries.length; sourceIndex += 1) {
41219
- const source = entries[sourceIndex];
41220
- for (let targetIndex = sourceIndex + 1; targetIndex < entries.length; targetIndex += 1) {
41221
- const target = entries[targetIndex];
43717
+ for (let sourceIndex = 0; sourceIndex < entries2.length; sourceIndex += 1) {
43718
+ const source = entries2[sourceIndex];
43719
+ for (let targetIndex = sourceIndex + 1; targetIndex < entries2.length; targetIndex += 1) {
43720
+ const target = entries2[targetIndex];
41222
43721
  if (!aabbOverlaps(source.worldBounds, target.worldBounds)) continue;
41223
43722
  const bboxOverlapVolume = aabbOverlapVolume(source.worldBounds, target.worldBounds);
41224
43723
  if (bboxOverlapVolume <= DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume) {
@@ -41265,7 +43764,7 @@ async function cloneCollisionShapePayloadForWorker(shape, maybeYield) {
41265
43764
  async function buildCollisionWorkerObjects(args) {
41266
43765
  var _a3;
41267
43766
  const warnings = [];
41268
- const entries = [];
43767
+ const entries2 = [];
41269
43768
  const maybeYield = createBuildYield(args.isCancelled);
41270
43769
  for (const obj of args.objects) {
41271
43770
  if (args.isCancelled()) throw new InspectBuildCancelledError();
@@ -41275,10 +43774,10 @@ async function buildCollisionWorkerObjects(args) {
41275
43774
  const local = localBounds(obj);
41276
43775
  const world = transformedBounds(obj, matrix);
41277
43776
  if (!local || !world) continue;
41278
- entries.push({ obj, matrix, localBounds: local, worldBounds: world });
43777
+ entries2.push({ obj, matrix, localBounds: local, worldBounds: world });
41279
43778
  await maybeYield();
41280
43779
  }
41281
- const { candidates, candidateCount, prunedPairCount } = await collectCollisionCandidates(entries, maybeYield);
43780
+ const { candidates, candidateCount, prunedPairCount } = await collectCollisionCandidates(entries2, maybeYield);
41282
43781
  if (candidateCount === 0) {
41283
43782
  return {
41284
43783
  objects: [],
@@ -41310,7 +43809,7 @@ async function buildCollisionWorkerObjects(args) {
41310
43809
  const out = [];
41311
43810
  let payloadBytes = 0;
41312
43811
  for (const entryIndex of selectedIndexes) {
41313
- const entry = entries[entryIndex];
43812
+ const entry = entries2[entryIndex];
41314
43813
  const collisionShape = await cloneCollisionShapePayloadForWorker(entry.obj.shape, maybeYield);
41315
43814
  const nextBytes = collisionPayloadBytes(collisionShape);
41316
43815
  if (payloadBytes + nextBytes > VIEWPORT_COLLISION_MAX_PAYLOAD_BYTES) {
@@ -41386,6 +43885,15 @@ function analyzePayloadFor(channel, objects, inspectPointSampleCount, groundZ, w
41386
43885
  }
41387
43886
  };
41388
43887
  }
43888
+ if (channel === "throughThickness") {
43889
+ return {
43890
+ channel,
43891
+ objects,
43892
+ throughThickness: {
43893
+ maxSamplesPerObject
43894
+ }
43895
+ };
43896
+ }
41389
43897
  if (channel === "roughness") {
41390
43898
  return {
41391
43899
  channel,
@@ -41413,6 +43921,10 @@ function resultToState(channel, result) {
41413
43921
  normals: object.normals,
41414
43922
  index: object.index,
41415
43923
  aValue: object.aValue,
43924
+ uvs: object.uvs,
43925
+ textureValues: object.textureValues,
43926
+ textureWidth: object.textureWidth,
43927
+ textureHeight: object.textureHeight,
41416
43928
  valueMin: object.valueMin,
41417
43929
  valueMax: object.valueMax,
41418
43930
  capped: object.capped,
@@ -41743,6 +44255,24 @@ function panelNumber(value) {
41743
44255
  function vec3Label(value) {
41744
44256
  return `[${value.map(panelNumber).join(", ")}]`;
41745
44257
  }
44258
+ function localPointToWorld(point, matrix) {
44259
+ const v = new Vector3(point[0], point[1], point[2]).applyMatrix4(matrix);
44260
+ return [v.x, v.y, v.z];
44261
+ }
44262
+ function localDirToWorld(dir, matrix) {
44263
+ const normal = new Matrix3().getNormalMatrix(matrix);
44264
+ const v = new Vector3(dir[0], dir[1], dir[2]).applyMatrix3(normal).normalize();
44265
+ return [v.x, v.y, v.z];
44266
+ }
44267
+ function worldScaleFactor(matrix) {
44268
+ const det = matrix.determinant();
44269
+ const scale = Math.cbrt(Math.abs(det));
44270
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
44271
+ }
44272
+ function copyNumberToClipboard(value) {
44273
+ var _a3;
44274
+ void ((_a3 = navigator.clipboard) == null ? void 0 : _a3.writeText(panelNumber(value)));
44275
+ }
41746
44276
  function edgeCurveLabel(edge) {
41747
44277
  var _a3;
41748
44278
  switch ((_a3 = edge.curve) == null ? void 0 : _a3.kind) {
@@ -41778,6 +44308,8 @@ function inspectChannelLabel(channel) {
41778
44308
  return "Comparison";
41779
44309
  case "thickness":
41780
44310
  return "Thickness";
44311
+ case "throughThickness":
44312
+ return "Minimum Solid Span";
41781
44313
  case "roughness":
41782
44314
  return "Roughness";
41783
44315
  default:
@@ -41993,6 +44525,11 @@ function Viewport() {
41993
44525
  const runQuality = useForgeStore((s) => s.runQuality);
41994
44526
  const buildLedgerEvents = useForgeStore((s) => s.buildLedgerEvents);
41995
44527
  const measureSelections = useForgeStore((s) => s.measureSelections);
44528
+ const selectedFace = useForgeStore((s) => s.selectedFace);
44529
+ const selectedEdge = useForgeStore((s) => s.selectedEdge);
44530
+ const selectedVertex = useForgeStore((s) => s.selectedVertex);
44531
+ const setSelectedEdge = useForgeStore((s) => s.setSelectedEdge);
44532
+ const setSelectedVertex = useForgeStore((s) => s.setSelectedVertex);
41996
44533
  const meshPreviewFile = useForgeStore((s) => s.meshPreviewFile);
41997
44534
  const voxelIntentMode = useForgeStore((s) => s.voxelIntentMode);
41998
44535
  const voxelIntentTool = useForgeStore((s) => s.voxelIntentTool);
@@ -42053,6 +44590,7 @@ function Viewport() {
42053
44590
  renderLabels,
42054
44591
  debugHighlights3D,
42055
44592
  dimensionsVisible,
44593
+ paramAnchorsVisible,
42056
44594
  attachmentsVisible,
42057
44595
  attachmentPoints,
42058
44596
  sectionPlaneGuidesEnabled,
@@ -42176,15 +44714,35 @@ function Viewport() {
42176
44714
  groundZ: inspectGroundZ
42177
44715
  });
42178
44716
  const scalarChannelInfo = reactExports.useMemo(() => {
42179
- if (inspectChannel === "thickness") {
44717
+ if (inspectChannel === "thickness" || inspectChannel === "throughThickness") {
42180
44718
  return { unitLabel: "mm", criticalThreshold: DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness };
42181
44719
  }
42182
44720
  if (inspectChannel === "roughness") return { unitLabel: "deg", criticalThreshold: null };
42183
44721
  return null;
42184
44722
  }, [inspectChannel]);
44723
+ const measuredThicknessMax = reactExports.useMemo(() => {
44724
+ if (inspectChannel !== "thickness" && inspectChannel !== "throughThickness") return null;
44725
+ let max2 = -Infinity;
44726
+ for (const surface of Object.values(inspectAnalysis.scalarSurfaces)) {
44727
+ if (Number.isFinite(surface.valueMax)) max2 = Math.max(max2, surface.valueMax);
44728
+ }
44729
+ return Number.isFinite(max2) ? max2 : null;
44730
+ }, [inspectAnalysis.scalarSurfaces, inspectChannel]);
44731
+ const effectiveThicknessColorRange = reactExports.useMemo(() => {
44732
+ const isDefaultRange = thicknessColorRange.min === DEFAULT_THICKNESS_COLOR_RANGE.min && thicknessColorRange.max === DEFAULT_THICKNESS_COLOR_RANGE.max;
44733
+ if (isDefaultRange && measuredThicknessMax != null && measuredThicknessMax > thicknessColorRange.max) {
44734
+ return { min: thicknessColorRange.min, max: measuredThicknessMax };
44735
+ }
44736
+ return thicknessColorRange;
44737
+ }, [measuredThicknessMax, thicknessColorRange]);
42185
44738
  const inspectColorScale = reactExports.useMemo(
42186
- () => ({ colormap: inspectColormap, domainMin: thicknessColorRange.min, domainMax: thicknessColorRange.max }),
42187
- [inspectColormap, thicknessColorRange.min, thicknessColorRange.max]
44739
+ () => ({
44740
+ colormap: inspectColormap,
44741
+ domainMin: effectiveThicknessColorRange.min,
44742
+ domainMax: effectiveThicknessColorRange.max,
44743
+ ...(inspectChannel === "thickness" || inspectChannel === "throughThickness") && inspectColormap !== "thickness-classic" ? { reversed: true } : {}
44744
+ }),
44745
+ [effectiveThicknessColorRange.max, effectiveThicknessColorRange.min, inspectChannel, inspectColormap]
42188
44746
  );
42189
44747
  const inspectScalarParams = reactExports.useMemo(() => {
42190
44748
  if (!scalarChannelInfo) return void 0;
@@ -42195,11 +44753,12 @@ function Viewport() {
42195
44753
  isolineSpacing: inspectIsolineSpacing,
42196
44754
  criticalEnabled: inspectCriticalLineEnabled,
42197
44755
  criticalThreshold: scalarChannelInfo.criticalThreshold,
42198
- shadingEnabled: true
44756
+ shadingEnabled: inspectChannel !== "thickness" && inspectChannel !== "throughThickness"
42199
44757
  };
42200
44758
  }, [
42201
44759
  scalarChannelInfo,
42202
44760
  inspectColorScale,
44761
+ inspectChannel,
42203
44762
  inspectQuantizeBands,
42204
44763
  inspectIsolinesEnabled,
42205
44764
  inspectIsolineSpacing,
@@ -42440,7 +44999,7 @@ function Viewport() {
42440
44999
  displayMode: inspectDisplayMode,
42441
45000
  warnings: inspectWarnings,
42442
45001
  swatches: inspectLegendSwatches,
42443
- thicknessColorRange,
45002
+ thicknessColorRange: effectiveThicknessColorRange,
42444
45003
  onThicknessColorRangeChange: setThicknessColorRange,
42445
45004
  colorScale: scalarChannelInfo ? inspectColorScale : void 0,
42446
45005
  unitLabel: scalarChannelInfo == null ? void 0 : scalarChannelInfo.unitLabel,
@@ -42674,6 +45233,9 @@ function Viewport() {
42674
45233
  sectionPlanes: objectSectionPlanesById[obj.id] ?? EMPTY_SECTION_PLANES,
42675
45234
  sectionPreviewRenderOrderBase: 2e3 + objIndex * 64,
42676
45235
  debugHighlightColor: shapeHl == null ? void 0 : shapeHl.color,
45236
+ selectedFaceTriangleIndices: (selectedFace == null ? void 0 : selectedFace.objectId) === obj.id ? selectedFace.triangleIndices : null,
45237
+ selectedEdgePoints: (selectedEdge == null ? void 0 : selectedEdge.objectId) === obj.id ? selectedEdge.points : null,
45238
+ selectedVertexPoint: (selectedVertex == null ? void 0 : selectedVertex.objectId) === obj.id ? selectedVertex.point : null,
42677
45239
  onPointerEnter: (event) => updateHoverLabel(obj, event),
42678
45240
  onPointerMove: (event) => updateHoverLabel(obj, event),
42679
45241
  onPointerLeave: (event) => clearHoverLabel(obj, event),
@@ -42808,6 +45370,7 @@ function Viewport() {
42808
45370
  !rigInspectActive && hoveredJointOverlay && /* @__PURE__ */ jsxRuntimeExports.jsx(HoveredJointOverlay, { state: hoveredJointOverlay, config: jointOverlayConfig }),
42809
45371
  !rigInspectActive && dimensionsVisible && dimensions.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx(DimensionAnnotation, { def: d, lengthUnit }, d.id)),
42810
45372
  !rigInspectActive && /* @__PURE__ */ jsxRuntimeExports.jsx(RenderLabelsOverlay, { labels: renderLabels }),
45373
+ !rigInspectActive && paramAnchorsVisible && /* @__PURE__ */ jsxRuntimeExports.jsx(SpatialParamAnchorsOverlay, {}),
42811
45374
  !rigInspectActive && attachmentsVisible !== "none" && attachmentPoints.map((ap) => {
42812
45375
  const matrix = objectMatrices[ap.objectId];
42813
45376
  return matrix ? /* @__PURE__ */ jsxRuntimeExports.jsx("group", { matrixAutoUpdate: false, matrix, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ConnectorAttachmentAnnotation, { def: ap }) }, `${ap.objectId}:${ap.name}`) : /* @__PURE__ */ jsxRuntimeExports.jsx(ConnectorAttachmentAnnotation, { def: ap }, `${ap.objectId}:${ap.name}`);
@@ -43018,6 +45581,7 @@ function Viewport() {
43018
45581
  }
43019
45582
  ),
43020
45583
  /* @__PURE__ */ jsxRuntimeExports.jsx(ViewportOverlayHost, { entries: overlayEntries }),
45584
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SpatialParamExpandedSheetOverlay, {}),
43021
45585
  viewportDisabledMessage && /* @__PURE__ */ jsxRuntimeExports.jsx(ViewportDisabledOverlay, { title: viewportDisabledMessage.title, body: viewportDisabledMessage.body }),
43022
45586
  drawFlagEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx(DrawToolbar, {}),
43023
45587
  /* @__PURE__ */ jsxRuntimeExports.jsx(HoverTooltipLayer, { ref: hoverTooltipRef, enabled: objectPickSyncEnabled && !measureMode && !voxelIntentMode }),
@@ -43129,6 +45693,7 @@ function Viewport() {
43129
45693
  ),
43130
45694
  viewportPortalHost && faceInfoPanel && reactDomExports.createPortal(
43131
45695
  (() => {
45696
+ var _a4;
43132
45697
  const obj = objects.find((o2) => o2.id === faceInfoPanel.objectId);
43133
45698
  if (!obj) return null;
43134
45699
  const activeFaceName = faceInfoPanel.faceName;
@@ -43139,8 +45704,8 @@ function Viewport() {
43139
45704
  const history = activeFaceName ? (faceInfoData == null ? void 0 : faceInfoData.faceHistories[activeFaceName]) ?? null : null;
43140
45705
  const faceNames = (faceInfoData == null ? void 0 : faceInfoData.faceNames) ?? [];
43141
45706
  const activeEdges = activeFaceName && faceInfoData ? (faceInfoData.edgeNames ?? []).map((name) => {
43142
- var _a4;
43143
- return (_a4 = faceInfoData.edges) == null ? void 0 : _a4[name];
45707
+ var _a5;
45708
+ return (_a5 = faceInfoData.edges) == null ? void 0 : _a5[name];
43144
45709
  }).filter((edge) => Boolean(edge) && edgeBelongsToFace(edge, activeFaceName)) : [];
43145
45710
  const visibleEdges = activeEdges.slice(0, 12);
43146
45711
  const viewportMaxRight = Math.min(viewportScreenRight, viewportWindowWidth);
@@ -43171,23 +45736,47 @@ function Viewport() {
43171
45736
  children: [
43172
45737
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }, children: [
43173
45738
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontWeight: 600, fontSize: 13 }, children: "Surface History" }),
43174
- /* @__PURE__ */ jsxRuntimeExports.jsx(
43175
- "button",
43176
- {
43177
- type: "button",
43178
- onClick: () => setFaceInfoPanel(null),
43179
- style: {
43180
- border: "none",
43181
- background: "transparent",
43182
- color: "var(--fc-textMuted)",
43183
- cursor: "pointer",
43184
- fontSize: 16,
43185
- lineHeight: 1,
43186
- padding: 0
43187
- },
43188
- children: "×"
43189
- }
43190
- )
45739
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
45740
+ activeFaceName && activeFace && /* @__PURE__ */ jsxRuntimeExports.jsx(
45741
+ "button",
45742
+ {
45743
+ type: "button",
45744
+ title: `Copy face("${activeFaceName}") selector to clipboard`,
45745
+ onClick: () => {
45746
+ var _a5;
45747
+ void ((_a5 = navigator.clipboard) == null ? void 0 : _a5.writeText(`face(${JSON.stringify(activeFaceName)})`));
45748
+ },
45749
+ style: {
45750
+ border: "1px solid var(--fc-border)",
45751
+ background: "transparent",
45752
+ color: "var(--fc-textMuted)",
45753
+ cursor: "pointer",
45754
+ fontSize: 11,
45755
+ lineHeight: 1,
45756
+ borderRadius: 4,
45757
+ padding: "3px 6px"
45758
+ },
45759
+ children: "Copy selector"
45760
+ }
45761
+ ),
45762
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
45763
+ "button",
45764
+ {
45765
+ type: "button",
45766
+ onClick: () => setFaceInfoPanel(null),
45767
+ style: {
45768
+ border: "none",
45769
+ background: "transparent",
45770
+ color: "var(--fc-textMuted)",
45771
+ cursor: "pointer",
45772
+ fontSize: 16,
45773
+ lineHeight: 1,
45774
+ padding: 0
45775
+ },
45776
+ children: "×"
45777
+ }
45778
+ )
45779
+ ] })
43191
45780
  ] }),
43192
45781
  /* @__PURE__ */ jsxRuntimeExports.jsx(
43193
45782
  "div",
@@ -43274,6 +45863,33 @@ function Viewport() {
43274
45863
  ]
43275
45864
  }
43276
45865
  ),
45866
+ ((_a4 = history == null ? void 0 : history.origin) == null ? void 0 : _a4.sourceSpan) && /* @__PURE__ */ jsxRuntimeExports.jsxs(
45867
+ "div",
45868
+ {
45869
+ style: {
45870
+ marginBottom: 10,
45871
+ padding: 8,
45872
+ border: "1px solid var(--fc-border)",
45873
+ borderRadius: 6,
45874
+ background: "color-mix(in srgb, var(--fc-bgInput) 70%, transparent)"
45875
+ },
45876
+ children: [
45877
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 11, fontWeight: 600, marginBottom: 3 }, children: "Source" }),
45878
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
45879
+ "div",
45880
+ {
45881
+ style: {
45882
+ fontSize: 10,
45883
+ color: "var(--fc-textMuted)",
45884
+ fontFamily: "monospace",
45885
+ wordBreak: "break-all"
45886
+ },
45887
+ children: `${history.origin.sourceSpan.fileName}:${history.origin.sourceSpan.line}:${history.origin.sourceSpan.column}`
45888
+ }
45889
+ )
45890
+ ]
45891
+ }
45892
+ ),
43277
45893
  history && history.timeline.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: history.timeline.map((entry, i) => {
43278
45894
  const isFirst = i === 0;
43279
45895
  const isLast = i === history.timeline.length - 1;
@@ -43489,12 +46105,164 @@ function Viewport() {
43489
46105
  ]
43490
46106
  }
43491
46107
  );
46108
+ })(),
46109
+ (selectedEdge || selectedVertex) && (() => {
46110
+ const objectId = (selectedEdge == null ? void 0 : selectedEdge.objectId) ?? (selectedVertex == null ? void 0 : selectedVertex.objectId) ?? "";
46111
+ const obj = objects.find((o2) => o2.id === objectId);
46112
+ const matrix = objectMatrices[objectId] ?? new Matrix4();
46113
+ const scale = worldScaleFactor(matrix);
46114
+ let title = "";
46115
+ const rows = [];
46116
+ let note = null;
46117
+ if (selectedVertex) {
46118
+ title = "Vertex";
46119
+ const world = localPointToWorld(selectedVertex.point, matrix);
46120
+ rows.push(["Coordinate", formatCoord(world, lengthUnit)]);
46121
+ rows.push(["X", formatLength(world[0], lengthUnit, 3), world[0]]);
46122
+ rows.push(["Y", formatLength(world[1], lengthUnit, 3), world[1]]);
46123
+ rows.push(["Z", formatLength(world[2], lengthUnit, 3), world[2]]);
46124
+ } else if (selectedEdge) {
46125
+ const { curve } = selectedEdge;
46126
+ if (curve.kind === "circle") {
46127
+ title = "Edge — Circle";
46128
+ const radius = curve.radius * scale;
46129
+ const center = localPointToWorld(curve.center, matrix);
46130
+ const axis = localDirToWorld(curve.axis, matrix);
46131
+ rows.push(["Radius", formatLength(radius, lengthUnit, 3), radius]);
46132
+ rows.push(["Diameter", formatLength(radius * 2, lengthUnit, 3), radius * 2]);
46133
+ rows.push(["Center", formatCoord(center, lengthUnit)]);
46134
+ rows.push(["Axis", vec3Label(axis)]);
46135
+ } else if (curve.kind === "line") {
46136
+ title = "Edge — Line";
46137
+ const start = localPointToWorld(curve.start, matrix);
46138
+ const end = localPointToWorld(curve.end, matrix);
46139
+ const dir = new Vector3(end[0] - start[0], end[1] - start[1], end[2] - start[2]).normalize();
46140
+ const length = selectedEdge.length * scale;
46141
+ rows.push(["Length", formatLength(length, lengthUnit, 3), length]);
46142
+ rows.push(["Direction", vec3Label([dir.x, dir.y, dir.z])]);
46143
+ rows.push(["Start", formatCoord(start, lengthUnit)]);
46144
+ rows.push(["End", formatCoord(end, lengthUnit)]);
46145
+ } else {
46146
+ title = "Edge — Non-analytic";
46147
+ const length = selectedEdge.length * scale;
46148
+ rows.push(["Length", formatLength(length, lengthUnit, 3), length]);
46149
+ note = "Non-analytic edge: length only (polyline). No analytic radius/axis.";
46150
+ }
46151
+ }
46152
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
46153
+ "div",
46154
+ {
46155
+ className: "fc-viewport-floating-panel fc-edge-info-panel",
46156
+ style: {
46157
+ position: "absolute",
46158
+ left: 16,
46159
+ bottom: 16,
46160
+ width: 248,
46161
+ background: "var(--fc-floating-panel-bg, var(--fc-bgPanel))",
46162
+ border: "1px solid var(--fc-floating-panel-border, var(--fc-border))",
46163
+ borderRadius: "var(--fc-floating-panel-radius, 8px)",
46164
+ boxShadow: "var(--fc-floating-panel-shadow, 0 12px 28px rgba(0, 0, 0, 0.28))",
46165
+ padding: 12,
46166
+ zIndex: 20,
46167
+ fontSize: 12,
46168
+ color: "var(--fc-text)"
46169
+ },
46170
+ onPointerDown: (e2) => e2.stopPropagation(),
46171
+ onContextMenu: (e2) => e2.preventDefault(),
46172
+ children: [
46173
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [
46174
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontWeight: 600, fontSize: 13 }, children: title }),
46175
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
46176
+ "button",
46177
+ {
46178
+ type: "button",
46179
+ onClick: () => {
46180
+ setSelectedEdge(null);
46181
+ setSelectedVertex(null);
46182
+ },
46183
+ style: {
46184
+ border: "none",
46185
+ background: "transparent",
46186
+ color: "var(--fc-textMuted)",
46187
+ cursor: "pointer",
46188
+ fontSize: 16,
46189
+ lineHeight: 1,
46190
+ padding: 0
46191
+ },
46192
+ children: "×"
46193
+ }
46194
+ )
46195
+ ] }),
46196
+ obj && /* @__PURE__ */ jsxRuntimeExports.jsx(
46197
+ "div",
46198
+ {
46199
+ style: {
46200
+ fontSize: 11,
46201
+ color: "var(--fc-textMuted)",
46202
+ marginBottom: 8,
46203
+ overflow: "hidden",
46204
+ textOverflow: "ellipsis",
46205
+ whiteSpace: "nowrap"
46206
+ },
46207
+ children: obj.treePath && obj.treePath.length > 0 ? obj.treePath.join(" / ") : obj.name
46208
+ }
46209
+ ),
46210
+ rows.map(([label, value, raw]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
46211
+ "div",
46212
+ {
46213
+ style: { display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8, marginBottom: 5 },
46214
+ children: [
46215
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "var(--fc-textMuted)", fontSize: 11 }, children: label }),
46216
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { display: "flex", alignItems: "baseline", gap: 6, minWidth: 0 }, children: [
46217
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontFamily: "monospace", fontSize: 11, textAlign: "right", wordBreak: "break-word" }, children: value }),
46218
+ raw !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
46219
+ "button",
46220
+ {
46221
+ type: "button",
46222
+ title: `Copy ${label.toLowerCase()} value (${panelNumber(raw)} mm) to clipboard`,
46223
+ onClick: () => copyNumberToClipboard(raw),
46224
+ style: {
46225
+ flexShrink: 0,
46226
+ border: "1px solid var(--fc-border)",
46227
+ background: "transparent",
46228
+ color: "var(--fc-textMuted)",
46229
+ cursor: "pointer",
46230
+ fontSize: 9,
46231
+ lineHeight: 1,
46232
+ borderRadius: 3,
46233
+ padding: "2px 4px"
46234
+ },
46235
+ children: "Copy"
46236
+ }
46237
+ )
46238
+ ] })
46239
+ ]
46240
+ },
46241
+ label
46242
+ )),
46243
+ note && /* @__PURE__ */ jsxRuntimeExports.jsx(
46244
+ "div",
46245
+ {
46246
+ style: {
46247
+ marginTop: 6,
46248
+ paddingTop: 6,
46249
+ borderTop: "1px solid var(--fc-border)",
46250
+ fontSize: 10,
46251
+ color: "var(--fc-textMuted)",
46252
+ lineHeight: 1.35
46253
+ },
46254
+ children: note
46255
+ }
46256
+ )
46257
+ ]
46258
+ }
46259
+ );
43492
46260
  })()
43493
46261
  ]
43494
46262
  }
43495
46263
  );
43496
46264
  }
43497
- const EditorApp$1 = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-BWUGCdD5.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
46265
+ const EditorApp$1 = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-CYBDvSyT.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
43498
46266
  const PENDING_SHARE_COPY_KEY = "fc-pending-share-copy";
43499
46267
  function storePendingShareCopy(shareId) {
43500
46268
  sessionStorage.setItem(PENDING_SHARE_COPY_KEY, shareId);
@@ -43760,17 +46528,17 @@ function SeoMetadata() {
43760
46528
  }, [location.pathname]);
43761
46529
  return null;
43762
46530
  }
43763
- reactExports.lazy(() => __vitePreload(() => import("./LandingPageProofDriven-BoVE7JGY.js"), true ? __vite__mapDeps([1]) : void 0).then((m2) => ({ default: m2.LandingPageProofDriven })));
43764
- const DocsPage = reactExports.lazy(() => __vitePreload(() => import("./DocsPage-BPGGwht1.js"), true ? [] : void 0).then((m2) => ({ default: m2.DocsPage })));
43765
- reactExports.lazy(() => __vitePreload(() => import("./BlogPage-B7BWxOCg.js"), true ? [] : void 0).then((m2) => ({ default: m2.BlogPage })));
43766
- reactExports.lazy(() => __vitePreload(() => import("./BenchmarkPage-DXKVXMrJ.js"), true ? __vite__mapDeps([1,2]) : void 0).then((m2) => ({ default: m2.BenchmarkPage })));
43767
- reactExports.lazy(() => __vitePreload(() => import("./AdminPage-B3L3W1Uo.js"), true ? [] : void 0).then((m2) => ({ default: m2.AdminPage })));
46531
+ reactExports.lazy(() => __vitePreload(() => import("./LandingPageProofDriven-XYTiYxfM.js"), true ? __vite__mapDeps([1]) : void 0).then((m2) => ({ default: m2.LandingPageProofDriven })));
46532
+ const DocsPage = reactExports.lazy(() => __vitePreload(() => import("./DocsPage-ClL6X1hR.js"), true ? [] : void 0).then((m2) => ({ default: m2.DocsPage })));
46533
+ reactExports.lazy(() => __vitePreload(() => import("./BlogPage-DIWRApKS.js"), true ? [] : void 0).then((m2) => ({ default: m2.BlogPage })));
46534
+ reactExports.lazy(() => __vitePreload(() => import("./BenchmarkPage-YZJbw5nd.js"), true ? __vite__mapDeps([1,2]) : void 0).then((m2) => ({ default: m2.BenchmarkPage })));
46535
+ reactExports.lazy(() => __vitePreload(() => import("./AdminPage-B1nIvqLS.js"), true ? [] : void 0).then((m2) => ({ default: m2.AdminPage })));
43768
46536
  reactExports.lazy(() => __vitePreload(() => Promise.resolve().then(() => PublishedModelPage$1), true ? void 0 : void 0).then((m2) => ({ default: m2.PublishedModelPage })));
43769
- reactExports.lazy(() => __vitePreload(() => import("./SettingsPage-BlJDCRe8.js"), true ? [] : void 0).then((m2) => ({ default: m2.SettingsPage })));
43770
- reactExports.lazy(() => __vitePreload(() => import("./PricingPage-C2PMzmDc.js"), true ? __vite__mapDeps([1,3]) : void 0).then((m2) => ({ default: m2.PricingPage })));
43771
- reactExports.lazy(() => __vitePreload(() => import("./LegalPage-Din8wv8d.js"), true ? __vite__mapDeps([1,4]) : void 0).then((m2) => ({ default: m2.LegalPage })));
43772
- const EditorApp = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-BWUGCdD5.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
43773
- const EmbedViewer = reactExports.lazy(() => __vitePreload(() => import("./EmbedViewer-DygByZS2.js"), true ? [] : void 0).then((m2) => ({ default: m2.EmbedViewer })));
46537
+ reactExports.lazy(() => __vitePreload(() => import("./SettingsPage-D3bcPBsC.js"), true ? [] : void 0).then((m2) => ({ default: m2.SettingsPage })));
46538
+ reactExports.lazy(() => __vitePreload(() => import("./PricingPage-BP4lIGio.js"), true ? __vite__mapDeps([1,3]) : void 0).then((m2) => ({ default: m2.PricingPage })));
46539
+ reactExports.lazy(() => __vitePreload(() => import("./LegalPage-D5Z3CscF.js"), true ? __vite__mapDeps([1,4]) : void 0).then((m2) => ({ default: m2.LegalPage })));
46540
+ const EditorApp = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-CYBDvSyT.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
46541
+ const EmbedViewer = reactExports.lazy(() => __vitePreload(() => import("./EmbedViewer-Dmfu_LIw.js"), true ? [] : void 0).then((m2) => ({ default: m2.EmbedViewer })));
43774
46542
  const embedMode = isEmbedMode() && !window.location.pathname.startsWith("/m/");
43775
46543
  const EDITABLE_CRASH_FILE = /\.(?:forge\.js|[cm]?[jt]sx?|json|md|txt|svg|dxf)$/i;
43776
46544
  function firstMeaningfulLine(text) {
@@ -43990,73 +46758,81 @@ function App() {
43990
46758
  applyTheme(localStorage.getItem("fc-theme") || "dark");
43991
46759
  clientExports.createRoot(document.getElementById("root")).render(/* @__PURE__ */ jsxRuntimeExports.jsx(App, {}));
43992
46760
  export {
43993
- VOXEL_INTENT_TOOL_ORDER as $,
46761
+ formatArea as $,
43994
46762
  AuthApiError as A,
43995
46763
  BrandMark as B,
43996
- captureViewportImageBlobFromStore as C,
43997
- exportExactFromStore as D,
43998
- storageQuotaUpgradeMessage as E,
46764
+ sanitizeExportStem as C,
46765
+ captureViewportImageBlobFromStore as D,
46766
+ ESCAPE_PRIORITY as E,
43999
46767
  FLAG_DEFINITIONS as F,
44000
- isImportableProjectMeshFile as G,
44001
- isImportableProjectBinaryFile as H,
44002
- hasExternalFiles as I,
44003
- isImportableProjectExactFile as J,
44004
- resolvePreviewFile as K,
44005
- countParamSnapshotDiff as L,
44006
- buildProjectShareUrl as M,
44007
- buildEmbedSnippet as N,
44008
- publishProjectShare as O,
44009
- unpublishProjectShare as P,
44010
- formatComputeBackendLabel as Q,
44011
- themes as R,
44012
- computeBackendFromParts as S,
44013
- formatArea as T,
44014
- sliderToAnimationSpeed as U,
44015
- animationSpeedToSlider as V,
44016
- formatAnimationSpeed as W,
44017
- resolveJointRange as X,
44018
- availableKernels as Y,
44019
- useJointsConfig as Z,
44020
- useJointAnimationValues as _,
46768
+ exportExactFromStore as G,
46769
+ storageQuotaUpgradeMessage as H,
46770
+ isImportableProjectMeshFile as I,
46771
+ isImportableProjectBinaryFile as J,
46772
+ hasExternalFiles as K,
46773
+ isImportableProjectExactFile as L,
46774
+ currentPath2DPoints as M,
46775
+ currentSpline2DPoints as N,
46776
+ currentPlacement2DItems as O,
46777
+ resolvePreviewFile as P,
46778
+ Path2DParamEditor as Q,
46779
+ Placement2DParamEditor as R,
46780
+ Spline2DParamEditor as S,
46781
+ countParamSnapshotDiff as T,
46782
+ buildProjectShareUrl as U,
46783
+ buildEmbedSnippet as V,
46784
+ publishProjectShare as W,
46785
+ unpublishProjectShare as X,
46786
+ formatComputeBackendLabel as Y,
46787
+ themes as Z,
46788
+ computeBackendFromParts as _,
44021
46789
  applyTheme as a,
44022
- VOXEL_INTENT_TOOL_LABELS as a0,
44023
- VOXEL_INTENT_TOOL_COLORS as a1,
44024
- VOXEL_INTENT_PLACEMENT_ORDER as a2,
44025
- VOXEL_INTENT_PLACEMENT_LABELS as a3,
44026
- highlightLanguageForProjectFile as a4,
44027
- hasProjectTextFileExtension as a5,
44028
- expandBoundsByTransformedAabb as a6,
44029
- Canvas as a7,
44030
- ActiveCameraBridge as a8,
44031
- PerspectiveCamera as a9,
44032
- buildShareUrl as aA,
44033
- share as aB,
44034
- ControlsInteractionBridge as aa,
44035
- SceneConfigurator as ab,
44036
- LocalEnvironment as ac,
44037
- ForgeObject as ad,
44038
- RenderLabelsOverlay as ae,
44039
- Grid as af,
44040
- OrbitControls2 as ag,
44041
- TOUCH_GESTURES_3D as ah,
44042
- MOUSE_BUTTONS_3D as ai,
44043
- ViewController as aj,
44044
- ModelJourneyBar as ak,
44045
- useJointAnimationLoop as al,
44046
- computeJointNodeMatrices as am,
44047
- computeObjectJointMatrices as an,
44048
- readLastActiveFileForUser as ao,
44049
- ToastContainer as ap,
44050
- isMobile as aq,
44051
- useFeatureFlag as ar,
44052
- decodeSharedHash as as,
44053
- decodeSharedBundle as at,
44054
- getExternalUrl as au,
44055
- getGistId as av,
44056
- Viewport as aw,
44057
- shouldBlockBrowserShortcut as ax,
44058
- useDrawStore as ay,
44059
- storePendingShareCopy as az,
46790
+ sliderToAnimationSpeed as a0,
46791
+ animationSpeedToSlider as a1,
46792
+ formatAnimationSpeed as a2,
46793
+ resolveJointRange as a3,
46794
+ availableKernels as a4,
46795
+ useJointsConfig as a5,
46796
+ useJointAnimationValues as a6,
46797
+ VOXEL_INTENT_TOOL_ORDER as a7,
46798
+ VOXEL_INTENT_TOOL_LABELS as a8,
46799
+ VOXEL_INTENT_TOOL_COLORS as a9,
46800
+ decodeSharedHash as aA,
46801
+ decodeSharedBundle as aB,
46802
+ getExternalUrl as aC,
46803
+ getGistId as aD,
46804
+ Viewport as aE,
46805
+ shouldBlockBrowserShortcut as aF,
46806
+ useDrawStore as aG,
46807
+ storePendingShareCopy as aH,
46808
+ buildShareUrl as aI,
46809
+ share as aJ,
46810
+ VOXEL_INTENT_PLACEMENT_ORDER as aa,
46811
+ VOXEL_INTENT_PLACEMENT_LABELS as ab,
46812
+ highlightLanguageForProjectFile as ac,
46813
+ hasProjectTextFileExtension as ad,
46814
+ expandBoundsByTransformedAabb as ae,
46815
+ Canvas as af,
46816
+ ActiveCameraBridge as ag,
46817
+ PerspectiveCamera as ah,
46818
+ ControlsInteractionBridge as ai,
46819
+ SceneConfigurator as aj,
46820
+ LocalEnvironment as ak,
46821
+ ForgeObject as al,
46822
+ RenderLabelsOverlay as am,
46823
+ Grid as an,
46824
+ OrbitControls2 as ao,
46825
+ TOUCH_GESTURES_3D as ap,
46826
+ MOUSE_BUTTONS_3D as aq,
46827
+ ViewController as ar,
46828
+ ModelJourneyBar as as,
46829
+ useJointAnimationLoop as at,
46830
+ computeJointNodeMatrices as au,
46831
+ computeObjectJointMatrices as av,
46832
+ readLastActiveFileForUser as aw,
46833
+ ToastContainer as ax,
46834
+ isMobile as ay,
46835
+ useFeatureFlag as az,
44060
46836
  authFetch as b,
44061
46837
  authApi as c,
44062
46838
  showToast as d,
@@ -44070,16 +46846,16 @@ export {
44070
46846
  fetchGistModel as l,
44071
46847
  monacoLanguageForProjectFile as m,
44072
46848
  fetchUrlModel as n,
44073
- exportMeshFromStore as o,
44074
- exportReportFromStore as p,
44075
- exportViewportImageFromStore as q,
46849
+ useEscapeAction as o,
46850
+ exportMeshFromStore as p,
46851
+ exportReportFromStore as q,
44076
46852
  readProjectFilesFromDataTransfer as r,
44077
46853
  storageUsagePercent as s,
44078
46854
  triggerDownload as t,
44079
46855
  useAuthStore as u,
44080
- exportOrbitVideoFromStore as v,
44081
- exportSketchFromStore as w,
44082
- buildGistShareUrl as x,
44083
- deriveExportStem as y,
44084
- sanitizeExportStem as z
46856
+ exportViewportImageFromStore as v,
46857
+ exportOrbitVideoFromStore as w,
46858
+ exportSketchFromStore as x,
46859
+ buildGistShareUrl as y,
46860
+ deriveExportStem as z
44085
46861
  };