forgecad 0.10.4 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/assets/{AdminPage-B3L3W1Uo.js → AdminPage-raksfnNA.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DXKVXMrJ.js → BenchmarkPage-DP3RxhPs.js} +2 -2
  3. package/dist/assets/{BlogPage-B7BWxOCg.js → BlogPage-D7Dos-vl.js} +1 -1
  4. package/dist/assets/{DocsPage-BPGGwht1.js → DocsPage-DO1kvBns.js} +7 -1
  5. package/dist/assets/{EditorApp-BWUGCdD5.js → EditorApp-DQJmcmRT.js} +9 -8
  6. package/dist/assets/{EmbedViewer-DygByZS2.js → EmbedViewer-DFDUhOma.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-BoVE7JGY.js → LandingPageProofDriven-DbE_tp8-.js} +2 -2
  8. package/dist/assets/{LegalPage-Din8wv8d.js → LegalPage-CominSso.js} +2 -2
  9. package/dist/assets/{PricingPage-C2PMzmDc.js → PricingPage-CcVIN9yj.js} +2 -2
  10. package/dist/assets/{SettingsPage-BlJDCRe8.js → SettingsPage-DLWcP289.js} +1 -1
  11. package/dist/assets/{app-BsRYSfxY.js → app-xW3hOdq9.js} +1135 -320
  12. package/dist/assets/{backendInit-6C0DLgH0.js → backendInit-mDHk97u7.js} +6630 -2493
  13. package/dist/assets/cli/{render-XXol_ET7.js → render--SIU27W_.js} +1263 -112
  14. package/dist/assets/{constructionHistoryWorker-cTHWRJEi.js → constructionHistoryWorker-uEe_Q7Kg.js} +1861 -610
  15. package/dist/assets/{evalWorker-BssDYW9u.js → evalWorker-BqyDHDcI.js} +6254 -2177
  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-UXMxlcR8.js} +2738 -742
  19. package/dist/assets/{jointPose-B0blBj9A.js → jointPose-bYMlwU3v.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-B_7QXpGB.js → manifold-BR7UYI4P.js} +1 -1
  22. package/dist/assets/{manifold-CYlIm-M6.js → manifold-CyOV5B9S.js} +2 -2
  23. package/dist/assets/{manifold-CNShmpEJ.js → manifold-D4d5NQst.js} +1 -1
  24. package/dist/assets/{reportWorker-Cb5eyM7D.js → reportWorker-DsaICZsn.js} +6010 -2032
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/CLI.md +4 -2
  28. package/dist/docs-raw/generated/assembly.md +76 -3
  29. package/dist/docs-raw/generated/concepts.md +31 -4
  30. package/dist/docs-raw/generated/core.md +159 -21
  31. package/dist/docs-raw/generated/curves.md +344 -6
  32. package/dist/docs-raw/generated/runtime-names.md +12 -12
  33. package/dist/docs-raw/generated/sketch.md +16 -3
  34. package/dist/docs-raw/guides/inspection-bundles.md +4 -2
  35. package/dist/docs-raw/guides/structural-fea.md +224 -0
  36. package/dist/docs-raw/skills/forgecad.md +1 -0
  37. package/dist/index.html +1 -1
  38. package/dist/sitemap.xml +15 -15
  39. package/dist-cli/{check-compiler-4RPB6SB5.js → check-compiler-7YAHVXYM.js} +1 -1
  40. package/dist-cli/{check-query-propagation-KN3DFQTX.js → check-query-propagation-ZRR6IOJW.js} +1 -1
  41. package/dist-cli/{chunk-UHBRMYA6.js → chunk-VNM67DIV.js} +6489 -2333
  42. package/dist-cli/forgecad.js +5258 -717
  43. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  44. package/dist-skill/CONTEXT.md +827 -45
  45. package/dist-skill/SKILL.md +1 -0
  46. package/dist-skill/docs/CLI.md +4 -2
  47. package/dist-skill/docs/generated/assembly.md +73 -3
  48. package/dist-skill/docs/generated/core.md +159 -21
  49. package/dist-skill/docs/generated/curves.md +343 -6
  50. package/dist-skill/docs/generated/runtime-names.md +12 -12
  51. package/dist-skill/docs/generated/sketch.md +16 -3
  52. package/dist-skill/docs/guides/inspection-bundles.md +4 -2
  53. package/dist-skill/docs/guides/structural-fea.md +224 -0
  54. package/dist-skill/website/skills/forgecad.md +1 -0
  55. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  56. package/examples/api/blend-full-round.forge.js +37 -0
  57. package/examples/api/blend-variable-radius.forge.js +51 -0
  58. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  59. package/examples/api/extrude-up-to-face.forge.js +47 -0
  60. package/examples/api/spoon-full-tang-handle.forge.js +148 -0
  61. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  62. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  63. package/package.json +4 -1
  64. /package/dist/assets/{landing-proof-driven-BxZZh5r5.js → landing-proof-driven-DNPRKL_p.js} +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 triangleSoupFromMeshes, cS as compareTriangleSoups, cT as buildGeometryComparisonPointCloud, cU as aabbOverlaps, cV as aabbOverlapVolume, cW as DEFAULT_COLLISION_INSPECTION_OPTIONS, cX as resolveScalarSceneSampleBudget, cY as INSPECT_POINT_SAMPLE_COUNT_MIN, cZ as FOCUS_MODE_DIM_OPACITY, c_ as DEFAULT_THICKNESS_INSPECTION_OPTIONS, c$ as comparisonCandidateContextOpacity, d0 as Matrix3, d1 as initBackendForEvaluation } from "./backendInit-mDHk97u7.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];
@@ -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
  };
@@ -15976,7 +15976,7 @@ const CRASH_COOLDOWN_MS = 2e3;
15976
15976
  class EvalWorkerClient {
15977
15977
  constructor(workerFactory = () => new Worker(new URL(
15978
15978
  /* @vite-ignore */
15979
- "/assets/evalWorker-BssDYW9u.js",
15979
+ "/assets/evalWorker-BqyDHDcI.js",
15980
15980
  import.meta.url
15981
15981
  ), { type: "module" })) {
15982
15982
  __publicField(this, "worker", null);
@@ -17636,6 +17636,7 @@ const VIEW_INSPECT_CHANNELS = /* @__PURE__ */ new Set([
17636
17636
  "distance",
17637
17637
  "collisions",
17638
17638
  "thickness",
17639
+ "throughThickness",
17639
17640
  "roughness"
17640
17641
  ]);
17641
17642
  const INSPECT_DISPLAY_MODES = /* @__PURE__ */ new Set(["heatmap", "points", "both", "scan"]);
@@ -17658,7 +17659,7 @@ function resolveInspectDisplayMode(value) {
17658
17659
  return typeof value === "string" && INSPECT_DISPLAY_MODES.has(value) ? value : "heatmap";
17659
17660
  }
17660
17661
  function isScalarInspectChannel(channel) {
17661
- return channel === "thickness" || channel === "roughness";
17662
+ return channel === "thickness" || channel === "throughThickness" || channel === "roughness";
17662
17663
  }
17663
17664
  function shouldUseScanRenderStyle(channel, displayMode) {
17664
17665
  return isScalarInspectChannel(channel) && displayMode === "scan";
@@ -18978,7 +18979,17 @@ const useForgeStore = create((set, get) => ({
18978
18979
  };
18979
18980
  }),
18980
18981
  selectedObjectId: null,
18981
- selectObject: (id) => set({ selectedObjectId: id, constructionGhost: null, selectedConstraintId: null }),
18982
+ selectObject: (id) => set((state2) => ({
18983
+ selectedObjectId: id,
18984
+ constructionGhost: null,
18985
+ selectedConstraintId: null,
18986
+ // Face selection is a sub-selection of an object; drop it when the active
18987
+ // object changes (the click handler re-populates it for the new object).
18988
+ selectedFace: state2.selectedFace && state2.selectedFace.objectId === id ? state2.selectedFace : null,
18989
+ // Edge/vertex sub-selections are likewise scoped to the active object.
18990
+ selectedEdge: state2.selectedEdge && state2.selectedEdge.objectId === id ? state2.selectedEdge : null,
18991
+ selectedVertex: state2.selectedVertex && state2.selectedVertex.objectId === id ? state2.selectedVertex : null
18992
+ })),
18982
18993
  constructionGhost: null,
18983
18994
  setConstructionGhost: (ghost) => set({ constructionGhost: ghost }),
18984
18995
  // ── Construction history replay ──
@@ -19177,6 +19188,30 @@ const useForgeStore = create((set, get) => ({
19177
19188
  setHoveredSurfaceIndex: (index) => set((state2) => state2.hoveredSurfaceIndex === index ? state2 : { hoveredSurfaceIndex: index }),
19178
19189
  selectedSurfaceIndex: null,
19179
19190
  setSelectedSurfaceIndex: (index) => set((state2) => state2.selectedSurfaceIndex === index ? { selectedSurfaceIndex: null } : { selectedSurfaceIndex: index }),
19191
+ selectedFace: null,
19192
+ setSelectedFace: (face) => set((state2) => {
19193
+ const prev = state2.selectedFace;
19194
+ if (face && prev && prev.objectId === face.objectId && prev.carrierName !== null && prev.carrierName === face.carrierName) {
19195
+ return { selectedFace: null };
19196
+ }
19197
+ return { selectedFace: face };
19198
+ }),
19199
+ selectedEdge: null,
19200
+ setSelectedEdge: (edge) => set((state2) => {
19201
+ const prev = state2.selectedEdge;
19202
+ if (edge && prev && prev.objectId === edge.objectId && prev.faceA === edge.faceA && prev.faceB === edge.faceB) {
19203
+ return { selectedEdge: null };
19204
+ }
19205
+ return { selectedEdge: edge };
19206
+ }),
19207
+ selectedVertex: null,
19208
+ setSelectedVertex: (vertex) => set((state2) => {
19209
+ const prev = state2.selectedVertex;
19210
+ 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]) {
19211
+ return { selectedVertex: null };
19212
+ }
19213
+ return { selectedVertex: vertex };
19214
+ }),
19180
19215
  selectedSketchEntityId: null,
19181
19216
  setSelectedSketchEntityId: (id) => set((state2) => state2.selectedSketchEntityId === id ? { selectedSketchEntityId: null } : { selectedSketchEntityId: id }),
19182
19217
  hoveredJointName: null,
@@ -19214,6 +19249,13 @@ const useForgeStore = create((set, get) => ({
19214
19249
  });
19215
19250
  },
19216
19251
  clearMeasureSelections: () => set({ measureSelections: [] }),
19252
+ replaceLastMeasureSelection: (entity) => {
19253
+ set((s) => {
19254
+ const sels = s.measureSelections;
19255
+ if (sels.length === 0) return { measureSelections: [entity] };
19256
+ return { measureSelections: [...sels.slice(0, -1), entity] };
19257
+ });
19258
+ },
19217
19259
  measurements: [],
19218
19260
  addMeasurePoint: (pt2) => {
19219
19261
  const measurements = get().measurements;
@@ -23499,7 +23541,7 @@ function moveOrbitTargetToPoint(camera, controls, target) {
23499
23541
  }
23500
23542
  const MIDDLE_BUTTON = 1;
23501
23543
  const DOUBLE_CLICK_MAX_MS = 500;
23502
- const CLICK_DRAG_TOLERANCE_PX = 5;
23544
+ const CLICK_DRAG_TOLERANCE_PX$1 = 5;
23503
23545
  const DOUBLE_CLICK_DISTANCE_TOLERANCE_PX = 8;
23504
23546
  function distanceSq(a2, b2) {
23505
23547
  return (a2.clientX - b2.clientX) ** 2 + (a2.clientY - b2.clientY) ** 2;
@@ -23535,7 +23577,7 @@ function OrbitTargetPicker({ active, controlsRef }) {
23535
23577
  previousClickRef.current = null;
23536
23578
  if (!active) return;
23537
23579
  const element = gl.domElement;
23538
- const dragToleranceSq = CLICK_DRAG_TOLERANCE_PX ** 2;
23580
+ const dragToleranceSq = CLICK_DRAG_TOLERANCE_PX$1 ** 2;
23539
23581
  const doubleDistanceToleranceSq = DOUBLE_CLICK_DISTANCE_TOLERANCE_PX ** 2;
23540
23582
  const onPointerDown = (event) => {
23541
23583
  if (event.button !== MIDDLE_BUTTON) return;
@@ -23700,7 +23742,7 @@ function OrbitTargetPulseLayer() {
23700
23742
  class ConstructionHistoryWorkerClient {
23701
23743
  constructor(workerFactory = () => new Worker(new URL(
23702
23744
  /* @vite-ignore */
23703
- "/assets/constructionHistoryWorker-cTHWRJEi.js",
23745
+ "/assets/constructionHistoryWorker-uEe_Q7Kg.js",
23704
23746
  import.meta.url
23705
23747
  ), { type: "module" })) {
23706
23748
  __publicField(this, "worker", null);
@@ -26212,7 +26254,7 @@ function generateReportInWorker(options) {
26212
26254
  return new Promise((resolve2, reject) => {
26213
26255
  const worker = new Worker(new URL(
26214
26256
  /* @vite-ignore */
26215
- "/assets/reportWorker-Cb5eyM7D.js",
26257
+ "/assets/reportWorker-DsaICZsn.js",
26216
26258
  import.meta.url
26217
26259
  ), { type: "module" });
26218
26260
  const cleanup = () => {
@@ -29980,6 +30022,185 @@ function LedgerMetric({ label, value }) {
29980
30022
  }
29981
30023
  );
29982
30024
  }
30025
+ const QUANT = 1e4;
30026
+ const q = (v) => Math.round(v * QUANT);
30027
+ const vertKey = (pos, i) => `${q(pos.getX(i))},${q(pos.getY(i))},${q(pos.getZ(i))}`;
30028
+ const edgeKey = (a2, b2) => a2 < b2 ? `${a2}|${b2}` : `${b2}|${a2}`;
30029
+ function buildEdgeAdjacency(positions, triCount) {
30030
+ const edgeToTris = /* @__PURE__ */ new Map();
30031
+ for (let t2 = 0; t2 < triCount; t2++) {
30032
+ const base = t2 * 3;
30033
+ const v0 = vertKey(positions, base);
30034
+ const v12 = vertKey(positions, base + 1);
30035
+ const v22 = vertKey(positions, base + 2);
30036
+ for (const ek of [edgeKey(v0, v12), edgeKey(v12, v22), edgeKey(v22, v0)]) {
30037
+ let list = edgeToTris.get(ek);
30038
+ if (!list) {
30039
+ list = [];
30040
+ edgeToTris.set(ek, list);
30041
+ }
30042
+ list.push(t2);
30043
+ }
30044
+ }
30045
+ return edgeToTris;
30046
+ }
30047
+ function floodComponent(positions, edgeToTris, startTriIndex, accept) {
30048
+ const visited = /* @__PURE__ */ new Set();
30049
+ const queue = [startTriIndex];
30050
+ visited.add(startTriIndex);
30051
+ while (queue.length > 0) {
30052
+ const t2 = queue.pop();
30053
+ const base = t2 * 3;
30054
+ const v0 = vertKey(positions, base);
30055
+ const v12 = vertKey(positions, base + 1);
30056
+ const v22 = vertKey(positions, base + 2);
30057
+ for (const ek of [edgeKey(v0, v12), edgeKey(v12, v22), edgeKey(v22, v0)]) {
30058
+ const neighbors = edgeToTris.get(ek);
30059
+ if (!neighbors) continue;
30060
+ for (const n of neighbors) {
30061
+ if (visited.has(n)) continue;
30062
+ if (!accept(n)) continue;
30063
+ visited.add(n);
30064
+ queue.push(n);
30065
+ }
30066
+ }
30067
+ }
30068
+ return Array.from(visited);
30069
+ }
30070
+ function summarizeRegion(positions, normals, indices) {
30071
+ let totalArea = 0;
30072
+ const centroid = new Vector3();
30073
+ const weightedNormal = new Vector3();
30074
+ const tmpA = new Vector3();
30075
+ const tmpB = new Vector3();
30076
+ const tmpC = new Vector3();
30077
+ for (const t2 of indices) {
30078
+ const base = t2 * 3;
30079
+ tmpA.set(positions.getX(base), positions.getY(base), positions.getZ(base));
30080
+ tmpB.set(positions.getX(base + 1), positions.getY(base + 1), positions.getZ(base + 1));
30081
+ tmpC.set(positions.getX(base + 2), positions.getY(base + 2), positions.getZ(base + 2));
30082
+ const ab = tmpB.clone().sub(tmpA);
30083
+ const ac = tmpC.clone().sub(tmpA);
30084
+ const cross = ab.cross(ac);
30085
+ const triArea = cross.length() * 0.5;
30086
+ totalArea += triArea;
30087
+ const triCenter = tmpA.clone().add(tmpB).add(tmpC).multiplyScalar(1 / 3);
30088
+ centroid.add(triCenter.multiplyScalar(triArea));
30089
+ if (cross.lengthSq() > 0) weightedNormal.add(cross.multiplyScalar(0.5));
30090
+ }
30091
+ if (totalArea > 0) centroid.multiplyScalar(1 / totalArea);
30092
+ let normal;
30093
+ if (weightedNormal.lengthSq() > 1e-12) {
30094
+ normal = weightedNormal.normalize();
30095
+ } else if (normals) {
30096
+ const si = indices[0] * 3;
30097
+ normal = new Vector3(normals.getX(si), normals.getY(si), normals.getZ(si)).normalize();
30098
+ } else {
30099
+ normal = new Vector3(0, 0, 1);
30100
+ }
30101
+ return { normal, center: centroid, area: totalArea };
30102
+ }
30103
+ function getFaceRegion(geometry, startTriIndex, options = {}) {
30104
+ const positions = geometry.getAttribute("position");
30105
+ const normals = geometry.getAttribute("normal") ?? null;
30106
+ const triCount = positions.count / 3;
30107
+ const edgeToTris = buildEdgeAdjacency(positions, triCount);
30108
+ const userData = geometry.userData;
30109
+ const faceIds = userData.forgeTriangleFaceIds;
30110
+ const faceIdNames = userData.forgeFaceIdNames;
30111
+ if (faceIds && faceIdNames && startTriIndex < faceIds.length) {
30112
+ const hitFaceId = faceIds[startTriIndex];
30113
+ if (hitFaceId >= 0) {
30114
+ const indices2 = floodComponent(positions, edgeToTris, startTriIndex, (n) => faceIds[n] === hitFaceId);
30115
+ const summary2 = summarizeRegion(positions, normals, indices2);
30116
+ const rawName = faceIdNames[hitFaceId] ?? null;
30117
+ const carrierName = rawName && rawName !== UNIDENTIFIED_FACE_NAME ? rawName : null;
30118
+ return { triangleIndices: indices2, ...summary2, carrierName };
30119
+ }
30120
+ }
30121
+ const normalTolerance = options.normalTolerance ?? 0.9995;
30122
+ const si = startTriIndex * 3;
30123
+ const startNormal = normals ? new Vector3(normals.getX(si), normals.getY(si), normals.getZ(si)) : new Vector3(0, 0, 1);
30124
+ const indices = floodComponent(positions, edgeToTris, startTriIndex, (n) => {
30125
+ if (!normals) return false;
30126
+ const ni = n * 3;
30127
+ const nNormal = new Vector3(normals.getX(ni), normals.getY(ni), normals.getZ(ni));
30128
+ return startNormal.dot(nNormal) >= normalTolerance;
30129
+ });
30130
+ const summary = summarizeRegion(positions, normals, indices);
30131
+ return { triangleIndices: indices, normal: startNormal.clone().normalize(), center: summary.center, area: summary.area, carrierName: null };
30132
+ }
30133
+ function buildFaceHighlightGeometry(sourceGeometry, triangleIndices) {
30134
+ const srcPos = sourceGeometry.getAttribute("position");
30135
+ const count = triangleIndices.length * 9;
30136
+ const positions = new Float32Array(count);
30137
+ for (let i = 0; i < triangleIndices.length; i++) {
30138
+ const base = triangleIndices[i] * 3;
30139
+ const out = i * 9;
30140
+ for (let v = 0; v < 3; v++) {
30141
+ positions[out + v * 3] = srcPos.getX(base + v);
30142
+ positions[out + v * 3 + 1] = srcPos.getY(base + v);
30143
+ positions[out + v * 3 + 2] = srcPos.getZ(base + v);
30144
+ }
30145
+ }
30146
+ const geo = new BufferGeometry();
30147
+ geo.setAttribute("position", new BufferAttribute(positions, 3));
30148
+ return geo;
30149
+ }
30150
+ function closestOnSegment(p2, a2, b2, out) {
30151
+ const abx = b2.x - a2.x;
30152
+ const aby = b2.y - a2.y;
30153
+ const abz = b2.z - a2.z;
30154
+ const abLenSq = abx * abx + aby * aby + abz * abz;
30155
+ let t2 = abLenSq > 0 ? ((p2.x - a2.x) * abx + (p2.y - a2.y) * aby + (p2.z - a2.z) * abz) / abLenSq : 0;
30156
+ if (t2 < 0) t2 = 0;
30157
+ else if (t2 > 1) t2 = 1;
30158
+ out.set(a2.x + abx * t2, a2.y + aby * t2, a2.z + abz * t2);
30159
+ return out.distanceToSquared(p2);
30160
+ }
30161
+ function getNearestEdge(geometry, localPoint, maxDistance) {
30162
+ const edges = getBakedEdges(geometry);
30163
+ if (edges.length === 0) return null;
30164
+ const maxSq = maxDistance * maxDistance;
30165
+ let best = null;
30166
+ let bestSq = maxSq;
30167
+ const a2 = new Vector3();
30168
+ const b2 = new Vector3();
30169
+ const closest = new Vector3();
30170
+ for (const edge of edges) {
30171
+ const pts = edge.points;
30172
+ for (let i = 0; i + 5 < pts.length; i += 3) {
30173
+ a2.set(pts[i], pts[i + 1], pts[i + 2]);
30174
+ b2.set(pts[i + 3], pts[i + 4], pts[i + 5]);
30175
+ const dSq = closestOnSegment(localPoint, a2, b2, closest);
30176
+ if (dSq < bestSq) {
30177
+ bestSq = dSq;
30178
+ best = { edge, point: closest.clone(), distance: Math.sqrt(dSq) };
30179
+ }
30180
+ }
30181
+ }
30182
+ return best;
30183
+ }
30184
+ function getNearestVertex(geometry, localPoint, maxDistance) {
30185
+ const edges = getBakedEdges(geometry);
30186
+ if (edges.length === 0) return null;
30187
+ const maxSq = maxDistance * maxDistance;
30188
+ let best = null;
30189
+ let bestSq = maxSq;
30190
+ const v = new Vector3();
30191
+ for (const edge of edges) {
30192
+ const pts = edge.points;
30193
+ for (let i = 0; i + 2 < pts.length; i += 3) {
30194
+ v.set(pts[i], pts[i + 1], pts[i + 2]);
30195
+ const dSq = v.distanceToSquared(localPoint);
30196
+ if (dSq < bestSq) {
30197
+ bestSq = dSq;
30198
+ best = { point: v.clone(), distance: Math.sqrt(dSq) };
30199
+ }
30200
+ }
30201
+ }
30202
+ return best;
30203
+ }
29983
30204
  const PREVIEW_RENDER_ORDER_STEP = 1;
29984
30205
  const HATCH_DIRECTION_A = new Vector2(Math.cos(MathUtils.degToRad(35)), Math.sin(MathUtils.degToRad(35)));
29985
30206
  const HATCH_DIRECTION_B = new Vector2(Math.cos(MathUtils.degToRad(125)), Math.sin(MathUtils.degToRad(125)));
@@ -31315,6 +31536,9 @@ function ZebraInspectionMaterial({ clippingPlanes }) {
31315
31536
  }
31316
31537
  const EMPTY_CLIPPING_PLANES$1 = [];
31317
31538
  const EMPTY_SECTION_PLANES$1 = [];
31539
+ const SELECTED_FACE_HIGHLIGHT_COLOR = "#ffa040";
31540
+ const SELECTED_EDGE_HIGHLIGHT_COLOR = "#3fd8ff";
31541
+ const SELECTED_VERTEX_HIGHLIGHT_COLOR = "#ffffff";
31318
31542
  function scanCellKey(ix, iy, iz) {
31319
31543
  return `${ix}:${iy}:${iz}`;
31320
31544
  }
@@ -31505,6 +31729,9 @@ function ForgeObject({
31505
31729
  sectionPlanes,
31506
31730
  sectionPreviewRenderOrderBase,
31507
31731
  debugHighlightColor,
31732
+ selectedFaceTriangleIndices,
31733
+ selectedEdgePoints,
31734
+ selectedVertexPoint,
31508
31735
  onPointerEnter,
31509
31736
  onPointerMove,
31510
31737
  onPointerLeave,
@@ -31546,7 +31773,7 @@ function ForgeObject({
31546
31773
  };
31547
31774
  }
31548
31775
  }, [obj.shape, wantsDirectSdf]);
31549
- const isScalarInspect = inspectChannel === "thickness" || inspectChannel === "roughness";
31776
+ const isScalarInspect = inspectChannel === "thickness" || inspectChannel === "throughThickness" || inspectChannel === "roughness";
31550
31777
  const isScalarScan = isScalarInspect && inspectDisplayMode === "scan";
31551
31778
  const inspectPointGeo = reactExports.useMemo(() => {
31552
31779
  if (!inspectPointCloud) return null;
@@ -31577,6 +31804,7 @@ function ForgeObject({
31577
31804
  geometry.setAttribute("position", new BufferAttribute(inspectScalarSurface.positions, 3));
31578
31805
  geometry.setAttribute("normal", new BufferAttribute(inspectScalarSurface.normals, 3));
31579
31806
  geometry.setAttribute("aValue", new BufferAttribute(inspectScalarSurface.aValue, 1));
31807
+ geometry.setAttribute("uv", new BufferAttribute(inspectScalarSurface.uvs ?? new Float32Array(vertexCount * 2), 2));
31580
31808
  geometry.setIndex(new BufferAttribute(inspectScalarSurface.index, 1));
31581
31809
  try {
31582
31810
  geometry.boundsTree = MeshBVH.deserialize(
@@ -31597,14 +31825,24 @@ function ForgeObject({
31597
31825
  if (!inspectScalarSurface || !effectiveColorScale) return null;
31598
31826
  return makeColorScaleTexture(colorScaleLUT(effectiveColorScale.colormap));
31599
31827
  }, [inspectScalarSurface, effectiveColorScale == null ? void 0 : effectiveColorScale.colormap]);
31828
+ const inspectScalarValueTexture = reactExports.useMemo(() => {
31829
+ if (!(inspectScalarSurface == null ? void 0 : inspectScalarSurface.textureValues) || !inspectScalarSurface.textureWidth || !inspectScalarSurface.textureHeight) return null;
31830
+ return makeScalarValueTexture(
31831
+ inspectScalarSurface.textureValues,
31832
+ inspectScalarSurface.textureWidth,
31833
+ inspectScalarSurface.textureHeight
31834
+ );
31835
+ }, [inspectScalarSurface]);
31600
31836
  const inspectScalarMaterialRef = reactExports.useRef(null);
31601
31837
  const invalidate2 = useThree((s) => s.invalidate);
31602
31838
  const inspectScalarUniforms = reactExports.useMemo(() => {
31603
31839
  if (!inspectColormapTexture || !effectiveColorScale) return null;
31604
31840
  return makeInspectScalarUniforms({
31605
31841
  colorScale: inspectColormapTexture,
31842
+ scalarTexture: inspectScalarValueTexture,
31606
31843
  domainMin: effectiveColorScale.domainMin,
31607
31844
  domainMax: effectiveColorScale.domainMax,
31845
+ colorScaleReversed: effectiveColorScale.reversed === true,
31608
31846
  quantizeBands: (inspectScalarParams == null ? void 0 : inspectScalarParams.quantizeBands) ?? 0,
31609
31847
  isoEnabled: (inspectScalarParams == null ? void 0 : inspectScalarParams.isolinesEnabled) ?? false,
31610
31848
  isoSpacing: (inspectScalarParams == null ? void 0 : inspectScalarParams.isolineSpacing) ?? 0,
@@ -31612,7 +31850,7 @@ function ForgeObject({
31612
31850
  criticalThreshold: (inspectScalarParams == null ? void 0 : inspectScalarParams.criticalThreshold) ?? 0,
31613
31851
  shadingEnabled: (inspectScalarParams == null ? void 0 : inspectScalarParams.shadingEnabled) ?? true
31614
31852
  });
31615
- }, [inspectColormapTexture]);
31853
+ }, [inspectColormapTexture, inspectScalarValueTexture]);
31616
31854
  const wantsScanProxy = Boolean(
31617
31855
  settings.visible && (inspectChannel === "none" && (renderStyle === "scan" || renderStyle === "matrix") || isScalarScan)
31618
31856
  );
@@ -31623,6 +31861,40 @@ function ForgeObject({
31623
31861
  const shell = createScanAnalysisColorGeometry(scanProxy.geometries.shell, inspectPointCloud, scanProxy.grid, matrix);
31624
31862
  return shell ? { shell } : null;
31625
31863
  }, [inspectPointCloud, isScalarScan, matrix, scanProxy]);
31864
+ const selectedFaceHighlightGeo = reactExports.useMemo(() => {
31865
+ if (!solidGeo || !selectedFaceTriangleIndices || selectedFaceTriangleIndices.length === 0) return null;
31866
+ return buildFaceHighlightGeometry(solidGeo, selectedFaceTriangleIndices);
31867
+ }, [solidGeo, selectedFaceTriangleIndices]);
31868
+ reactExports.useEffect(() => {
31869
+ return () => {
31870
+ selectedFaceHighlightGeo == null ? void 0 : selectedFaceHighlightGeo.dispose();
31871
+ };
31872
+ }, [selectedFaceHighlightGeo]);
31873
+ const selectedEdgeGeo = reactExports.useMemo(() => {
31874
+ if (!selectedEdgePoints || selectedEdgePoints.length < 6) return null;
31875
+ const segCount = selectedEdgePoints.length / 3 - 1;
31876
+ const positions = new Float32Array(segCount * 6);
31877
+ for (let i = 0; i < segCount; i++) {
31878
+ const a2 = i * 3;
31879
+ positions.set(selectedEdgePoints.slice(a2, a2 + 3), i * 6);
31880
+ positions.set(selectedEdgePoints.slice(a2 + 3, a2 + 6), i * 6 + 3);
31881
+ }
31882
+ const geo = new BufferGeometry();
31883
+ geo.setAttribute("position", new BufferAttribute(positions, 3));
31884
+ return geo;
31885
+ }, [selectedEdgePoints]);
31886
+ reactExports.useEffect(() => {
31887
+ return () => {
31888
+ selectedEdgeGeo == null ? void 0 : selectedEdgeGeo.dispose();
31889
+ };
31890
+ }, [selectedEdgeGeo]);
31891
+ const selectedVertexMarkerRadius = reactExports.useMemo(() => {
31892
+ var _a4;
31893
+ if (!selectedVertexPoint || !solidGeo) return 0;
31894
+ if (!solidGeo.boundingSphere) solidGeo.computeBoundingSphere();
31895
+ const r2 = ((_a4 = solidGeo.boundingSphere) == null ? void 0 : _a4.radius) ?? 1;
31896
+ return r2 * 0.02;
31897
+ }, [selectedVertexPoint, solidGeo]);
31626
31898
  reactExports.useEffect(() => {
31627
31899
  return () => {
31628
31900
  solidGeo == null ? void 0 : solidGeo.dispose();
@@ -31649,11 +31921,17 @@ function ForgeObject({
31649
31921
  inspectColormapTexture == null ? void 0 : inspectColormapTexture.dispose();
31650
31922
  };
31651
31923
  }, [inspectColormapTexture]);
31924
+ reactExports.useEffect(() => {
31925
+ return () => {
31926
+ inspectScalarValueTexture == null ? void 0 : inspectScalarValueTexture.dispose();
31927
+ };
31928
+ }, [inspectScalarValueTexture]);
31652
31929
  reactExports.useEffect(() => {
31653
31930
  if (!inspectScalarUniforms || !effectiveColorScale || !inspectScalarParams) return;
31654
31931
  updateInspectScalarUniforms(inspectScalarUniforms, {
31655
31932
  domainMin: effectiveColorScale.domainMin,
31656
31933
  domainMax: effectiveColorScale.domainMax,
31934
+ colorScaleReversed: effectiveColorScale.reversed === true,
31657
31935
  quantizeBands: inspectScalarParams.quantizeBands,
31658
31936
  isoEnabled: inspectScalarParams.isolinesEnabled,
31659
31937
  isoSpacing: inspectScalarParams.isolineSpacing,
@@ -32057,7 +32335,25 @@ function ForgeObject({
32057
32335
  polygonOffsetUnits: -1
32058
32336
  }
32059
32337
  ) }),
32060
- debugHighlightColor && edgesGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: edgesGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx("lineBasicMaterial", { color: debugHighlightColor, linewidth: 2, depthTest: false }) })
32338
+ debugHighlightColor && edgesGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("lineSegments", { geometry: edgesGeo, raycast: () => null, children: /* @__PURE__ */ jsxRuntimeExports.jsx("lineBasicMaterial", { color: debugHighlightColor, linewidth: 2, depthTest: false }) }),
32339
+ selectedFaceHighlightGeo && /* @__PURE__ */ jsxRuntimeExports.jsx("mesh", { geometry: selectedFaceHighlightGeo, raycast: () => null, renderOrder: 11, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
32340
+ "meshBasicMaterial",
32341
+ {
32342
+ color: SELECTED_FACE_HIGHLIGHT_COLOR,
32343
+ transparent: true,
32344
+ opacity: 0.45,
32345
+ side: DoubleSide,
32346
+ depthTest: false,
32347
+ polygonOffset: true,
32348
+ polygonOffsetFactor: -2,
32349
+ polygonOffsetUnits: -2
32350
+ }
32351
+ ) }),
32352
+ 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 }) }),
32353
+ selectedVertexPoint && selectedVertexMarkerRadius > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("mesh", { position: selectedVertexPoint, raycast: () => null, renderOrder: 13, children: [
32354
+ /* @__PURE__ */ jsxRuntimeExports.jsx("sphereGeometry", { args: [selectedVertexMarkerRadius, 12, 12] }),
32355
+ /* @__PURE__ */ jsxRuntimeExports.jsx("meshBasicMaterial", { color: SELECTED_VERTEX_HIGHLIGHT_COLOR, depthTest: false, toneMapped: false })
32356
+ ] })
32061
32357
  ]
32062
32358
  }
32063
32359
  );
@@ -32344,7 +32640,7 @@ function HoveredJointOverlay({ state: state2, config }) {
32344
32640
  ] });
32345
32641
  }
32346
32642
  function scalarRamp(options, domainMin, domainMax, unit) {
32347
- var _a3;
32643
+ var _a3, _b2;
32348
32644
  const colormap = ((_a3 = options.colorScale) == null ? void 0 : _a3.colormap) ?? DEFAULT_COLORMAP;
32349
32645
  const midpoint = domainMin + (domainMax - domainMin) / 2;
32350
32646
  const unitSuffix = unit ? ` ${unit}` : "";
@@ -32352,7 +32648,7 @@ function scalarRamp(options, domainMin, domainMax, unit) {
32352
32648
  const critical = options.criticalValue;
32353
32649
  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
32650
  return {
32355
- colors: colorScaleHexStops(colormap),
32651
+ colors: colorScaleHexStops(colormap, 8, ((_b2 = options.colorScale) == null ? void 0 : _b2.reversed) === true),
32356
32652
  leftLabel: `${fmt(domainMin)}${unitSuffix}`,
32357
32653
  centerLabel: `${fmt(midpoint)}${unitSuffix}`,
32358
32654
  rightLabel: `${fmt(domainMax)}${unitSuffix}`,
@@ -32366,7 +32662,7 @@ function fmt(value) {
32366
32662
  return fixed.replace(/\.?0+$/, "");
32367
32663
  }
32368
32664
  function inspectionLegendDefinitionFor(channel, displayMode, legendOptions = {}) {
32369
- var _a3, _b2, _c, _d;
32665
+ var _a3, _b2, _c, _d, _e, _f;
32370
32666
  switch (channel) {
32371
32667
  case "mask":
32372
32668
  return {
@@ -32468,13 +32764,32 @@ function inspectionLegendDefinitionFor(channel, displayMode, legendOptions = {})
32468
32764
  }
32469
32765
  return {
32470
32766
  title: "Wall Thickness",
32471
- summary: "Color maps continuously from thinner to thicker material across the selected range.",
32767
+ summary: "Warm colors mark thinner material; cool colors mark thicker material.",
32768
+ ramp
32769
+ };
32770
+ }
32771
+ case "throughThickness": {
32772
+ const colorRange = legendOptions.thicknessColorRange ?? DEFAULT_THICKNESS_COLOR_RANGE;
32773
+ const domainMin = ((_c = legendOptions.colorScale) == null ? void 0 : _c.domainMin) ?? colorRange.min;
32774
+ const domainMax = ((_d = legendOptions.colorScale) == null ? void 0 : _d.domainMax) ?? colorRange.max;
32775
+ const unit = legendOptions.unitLabel ?? "mm";
32776
+ const ramp = scalarRamp(legendOptions, domainMin, domainMax, unit);
32777
+ if (displayMode === "scan") {
32778
+ return {
32779
+ title: "Minimum Solid Span Scan",
32780
+ summary: "Each scan box uses the same minimum solid span ramp as the heatmap.",
32781
+ ramp
32782
+ };
32783
+ }
32784
+ return {
32785
+ title: "Minimum Solid Span",
32786
+ summary: "Warm colors mark short through-material spans; cool colors mark more surrounding material.",
32472
32787
  ramp
32473
32788
  };
32474
32789
  }
32475
32790
  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;
32791
+ const domainMin = ((_e = legendOptions.colorScale) == null ? void 0 : _e.domainMin) ?? 0;
32792
+ const domainMax = ((_f = legendOptions.colorScale) == null ? void 0 : _f.domainMax) ?? 1;
32478
32793
  const unit = legendOptions.unitLabel ?? "deg";
32479
32794
  const ramp = scalarRamp(legendOptions, domainMin, domainMax, unit);
32480
32795
  if (displayMode === "scan") {
@@ -32960,7 +33275,7 @@ function InspectionLegend({
32960
33275
  criticalValue
32961
33276
  });
32962
33277
  const swatches = liveSwatches && liveSwatches.length > 0 ? liveSwatches : definition == null ? void 0 : definition.swatches;
32963
- const sliderGradient = colorScaleHexStops((colorScale == null ? void 0 : colorScale.colormap) ?? "viridis");
33278
+ const sliderGradient = colorScaleHexStops((colorScale == null ? void 0 : colorScale.colormap) ?? "viridis", 8, (colorScale == null ? void 0 : colorScale.reversed) === true);
32964
33279
  reactExports.useEffect(() => {
32965
33280
  var _a3;
32966
33281
  const parent = (_a3 = panelRef.current) == null ? void 0 : _a3.parentElement;
@@ -32973,7 +33288,7 @@ function InspectionLegend({
32973
33288
  }, [channel]);
32974
33289
  if (!definition) return null;
32975
33290
  const warning = warnings[0];
32976
- const showThicknessControls = channel === "thickness" && thicknessColorRange !== void 0 && onThicknessColorRangeChange !== void 0;
33291
+ const showThicknessControls = (channel === "thickness" || channel === "throughThickness") && thicknessColorRange !== void 0 && onThicknessColorRangeChange !== void 0;
32977
33292
  const swatchCount = (swatches == null ? void 0 : swatches.length) ?? 0;
32978
33293
  const hasScrollableSwatches = swatchCount > 10;
32979
33294
  const effectivePanelStyle = {
@@ -33066,212 +33381,263 @@ function LabeledAxes({ size = 50 }) {
33066
33381
  /* @__PURE__ */ jsxRuntimeExports.jsx(Html, { position: [0, 0, size + 3], center: true, style: labelStyle("#4488ff"), children: "Z" })
33067
33382
  ] });
33068
33383
  }
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) {
33384
+ const SHARP_DOT = 0.9995;
33385
+ const CORNER_TURN_DEG = 35;
33386
+ const LEN_RATIO_SPLIT = 4;
33387
+ const CONTINUITY_LIMIT_DEG = 60;
33388
+ const CORNER_TURN_COS = Math.cos(CORNER_TURN_DEG * Math.PI / 180);
33389
+ const CONTINUITY_LIMIT_COS = Math.cos(CONTINUITY_LIMIT_DEG * Math.PI / 180);
33390
+ function extractFeatureEdges(geometry) {
33087
33391
  const positions = geometry.getAttribute("position");
33088
33392
  const normals = geometry.getAttribute("normal");
33393
+ if (!positions) return [];
33089
33394
  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();
33395
+ const data = /* @__PURE__ */ new Map();
33093
33396
  for (let t2 = 0; t2 < triCount; t2++) {
33094
33397
  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
- }
33398
+ const tn = normals ? new Vector3(normals.getX(base), normals.getY(base), normals.getZ(base)) : new Vector3(0, 0, 1);
33399
+ for (let e2 = 0; e2 < 3; e2++) {
33400
+ const i0 = base + e2;
33401
+ const i1 = base + (e2 + 1) % 3;
33402
+ const ek = edgeKey(vertKey(positions, i0), vertKey(positions, i1));
33403
+ let d = data.get(ek);
33404
+ if (!d) {
33405
+ d = {
33406
+ a: new Vector3().fromBufferAttribute(positions, i0),
33407
+ b: new Vector3().fromBufferAttribute(positions, i1),
33408
+ normals: []
33409
+ };
33410
+ data.set(ek, d);
33127
33411
  }
33412
+ d.normals.push(tn);
33128
33413
  }
33129
33414
  }
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));
33415
+ const out = [];
33416
+ for (const d of data.values()) {
33417
+ const sharp = d.normals.length === 2 && d.normals[0].dot(d.normals[1]) < SHARP_DOT;
33418
+ const boundary = d.normals.length === 1;
33419
+ if (sharp || boundary) out.push({ a: d.a, b: d.b });
33147
33420
  }
33148
- if (totalArea > 0) centroid.multiplyScalar(1 / totalArea);
33149
- return { triangleIndices: indices, normal: startNormal.clone(), center: centroid, area: totalArea };
33421
+ return out;
33150
33422
  }
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);
33423
+ function buildGraph(edges) {
33424
+ const point = /* @__PURE__ */ new Map();
33425
+ const adj = /* @__PURE__ */ new Map();
33426
+ const key = (p2) => `${q(p2.x)},${q(p2.y)},${q(p2.z)}`;
33427
+ for (const { a: a2, b: b2 } of edges) {
33428
+ const ka = key(a2);
33429
+ const kb = key(b2);
33430
+ if (ka === kb) continue;
33431
+ point.set(ka, a2);
33432
+ point.set(kb, b2);
33433
+ (adj.get(ka) ?? adj.set(ka, /* @__PURE__ */ new Set()).get(ka)).add(kb);
33434
+ (adj.get(kb) ?? adj.set(kb, /* @__PURE__ */ new Set()).get(kb)).add(ka);
33435
+ }
33436
+ return { point, adj: new Map(Array.from(adj, ([k2, v]) => [k2, Array.from(v)])) };
33437
+ }
33438
+ function nearestSegment(graph, localPoint) {
33439
+ let best = null;
33440
+ let bestSq = Infinity;
33441
+ const closest = new Vector3();
33442
+ const seen = /* @__PURE__ */ new Set();
33443
+ for (const [u, neighbors] of graph.adj) {
33444
+ const a2 = graph.point.get(u);
33445
+ for (const v of neighbors) {
33446
+ const ek = edgeKey(u, v);
33447
+ if (seen.has(ek)) continue;
33448
+ seen.add(ek);
33449
+ const b2 = graph.point.get(v);
33450
+ const dSq = closestOnSegment(localPoint, a2, b2, closest);
33451
+ if (dSq < bestSq) {
33452
+ bestSq = dSq;
33453
+ best = { u, v };
33454
+ }
33162
33455
  }
33163
33456
  }
33164
- const geo = new BufferGeometry();
33165
- geo.setAttribute("position", new BufferAttribute(positions, 3));
33166
- return geo;
33457
+ return best;
33167
33458
  }
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);
33459
+ function walk(graph, fromKey, curKey, mode, stopAtKey) {
33460
+ const out = [];
33461
+ const visited = /* @__PURE__ */ new Set([fromKey, curKey]);
33462
+ let prev = fromKey;
33463
+ let cur = curKey;
33464
+ for (let guard = 0; guard < 1e5; guard++) {
33465
+ const prevP = graph.point.get(prev);
33466
+ const curP = graph.point.get(cur);
33467
+ const dIn = curP.clone().sub(prevP);
33468
+ const lenIn = dIn.length();
33469
+ if (lenIn === 0) break;
33470
+ dIn.multiplyScalar(1 / lenIn);
33471
+ let bestKey = null;
33472
+ let bestDot = -Infinity;
33473
+ let bestLen = 0;
33474
+ for (const w of graph.adj.get(cur) ?? []) {
33475
+ if (w === prev) continue;
33476
+ const wp = graph.point.get(w);
33477
+ const dOut = wp.clone().sub(curP);
33478
+ const lenOut = dOut.length();
33479
+ if (lenOut === 0) continue;
33480
+ const dot = dIn.dot(dOut) / lenOut;
33481
+ if (dot > bestDot) {
33482
+ bestDot = dot;
33483
+ bestKey = w;
33484
+ bestLen = lenOut;
33485
+ }
33486
+ }
33487
+ if (!bestKey) break;
33488
+ if (bestDot < CONTINUITY_LIMIT_COS) break;
33489
+ if (mode === "single") {
33490
+ if (bestDot < CORNER_TURN_COS) break;
33491
+ const ratio = bestLen >= lenIn ? bestLen / lenIn : lenIn / bestLen;
33492
+ if (ratio > LEN_RATIO_SPLIT) break;
33493
+ }
33494
+ if (bestKey === stopAtKey) {
33495
+ out.push(bestKey);
33496
+ break;
33229
33497
  }
33498
+ if (visited.has(bestKey)) break;
33499
+ visited.add(bestKey);
33500
+ out.push(bestKey);
33501
+ prev = cur;
33502
+ cur = bestKey;
33230
33503
  }
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
- }
33504
+ return out;
33505
+ }
33506
+ function chainFromSeed(graph, seed, mode) {
33507
+ const forward = walk(graph, seed.u, seed.v, mode, seed.u);
33508
+ const closed = forward.length > 0 && forward[forward.length - 1] === seed.u;
33509
+ let keys;
33510
+ if (closed) {
33511
+ keys = [seed.u, seed.v, ...forward];
33512
+ } else {
33513
+ const backward = walk(graph, seed.v, seed.u, mode, seed.v);
33514
+ keys = [...backward.slice().reverse(), seed.u, seed.v, ...forward];
33515
+ }
33516
+ return keys.map((k2) => graph.point.get(k2).clone());
33517
+ }
33518
+ function getNearestEdgeChain(geometry, localPoint) {
33519
+ const graph = buildGraph(extractFeatureEdges(geometry));
33520
+ const seed = nearestSegment(graph, localPoint);
33521
+ if (!seed) return null;
33522
+ return chainFromSeed(graph, seed, "single");
33523
+ }
33524
+ function getWholeEdgePath(geometry, localPoint) {
33525
+ const graph = buildGraph(extractFeatureEdges(geometry));
33526
+ const seed = nearestSegment(graph, localPoint);
33527
+ if (!seed) return null;
33528
+ return chainFromSeed(graph, seed, "whole");
33529
+ }
33530
+ function circleMeasurement(curve) {
33531
+ if (!curve || curve.kind !== "circle") return null;
33532
+ const radius = curve.radius;
33533
+ return {
33534
+ radius,
33535
+ diameter: radius * 2,
33536
+ circumference: 2 * Math.PI * radius,
33537
+ center: [curve.center[0], curve.center[1], curve.center[2]],
33538
+ axis: [curve.axis[0], curve.axis[1], curve.axis[2]]
33539
+ };
33540
+ }
33541
+ function worldScale(m2) {
33542
+ const s = new Vector3();
33543
+ m2.decompose(new Vector3(), new Quaternion(), s);
33544
+ return (Math.abs(s.x) + Math.abs(s.y) + Math.abs(s.z)) / 3;
33545
+ }
33546
+ function applyPoint(p2, m2) {
33547
+ const v = new Vector3(p2[0], p2[1], p2[2]).applyMatrix4(m2);
33548
+ return [v.x, v.y, v.z];
33549
+ }
33550
+ function edgeEntityFromBakedEdge(edge, meshMatrixWorld, meshUuid) {
33551
+ const scale = worldScale(meshMatrixWorld);
33552
+ const pts = edge.points;
33553
+ const n = pts.length / 3;
33554
+ const firstLocal = [pts[0], pts[1], pts[2]];
33555
+ const lastLocal = n > 0 ? [pts[(n - 1) * 3], pts[(n - 1) * 3 + 1], pts[(n - 1) * 3 + 2]] : firstLocal;
33556
+ const start = applyPoint(firstLocal, meshMatrixWorld);
33557
+ const end = applyPoint(lastLocal, meshMatrixWorld);
33558
+ 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));
33559
+ if (edge.curve.kind === "line") {
33560
+ const s = new Vector3(...edge.curve.start).applyMatrix4(meshMatrixWorld);
33561
+ const e2 = new Vector3(...edge.curve.end).applyMatrix4(meshMatrixWorld);
33562
+ const dir2 = e2.clone().sub(s);
33563
+ const length = dir2.length();
33564
+ const direction2 = length > 0 ? dir2.normalize() : new Vector3(1, 0, 0);
33565
+ return {
33566
+ kind: "edge",
33567
+ start: [s.x, s.y, s.z],
33568
+ end: [e2.x, e2.y, e2.z],
33569
+ length,
33570
+ direction: [direction2.x, direction2.y, direction2.z],
33571
+ meshUuid,
33572
+ curve: { kind: "line", start: [s.x, s.y, s.z], end: [e2.x, e2.y, e2.z], faceName: edge.curve.faceName }
33573
+ };
33253
33574
  }
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
- }
33575
+ if (edge.curve.kind === "circle") {
33576
+ const center = new Vector3(...edge.curve.center).applyMatrix4(meshMatrixWorld);
33577
+ const axis = new Vector3(...edge.curve.axis).transformDirection(meshMatrixWorld).normalize();
33578
+ const radius = edge.curve.radius * scale;
33579
+ return {
33580
+ kind: "edge",
33581
+ start,
33582
+ end,
33583
+ length: 2 * Math.PI * radius,
33584
+ // circumference
33585
+ direction: [axis.x, axis.y, axis.z],
33586
+ meshUuid,
33587
+ curve: { kind: "circle", center: [center.x, center.y, center.z], axis: [axis.x, axis.y, axis.z], radius, faceName: edge.curve.faceName },
33588
+ polyline
33589
+ };
33269
33590
  }
33591
+ const dir = new Vector3(end[0] - start[0], end[1] - start[1], end[2] - start[2]);
33592
+ const direction = dir.lengthSq() > 0 ? dir.normalize() : new Vector3(1, 0, 0);
33270
33593
  return {
33271
- start: startPt.applyMatrix4(mesh.matrixWorld),
33272
- end: endPt.applyMatrix4(mesh.matrixWorld),
33273
- segments
33594
+ kind: "edge",
33595
+ start,
33596
+ end,
33597
+ length: edge.length * scale,
33598
+ direction: [direction.x, direction.y, direction.z],
33599
+ meshUuid,
33600
+ curve: { kind: "unidentified" },
33601
+ polyline
33602
+ };
33603
+ }
33604
+ const MEASURE_COLORS = {
33605
+ face: "#60b8ff",
33606
+ // hover face — gentle blue
33607
+ edge: "#ffffff",
33608
+ // hover edge — white
33609
+ vertex: "#ffffff",
33610
+ // hover vertex — white
33611
+ selection: "#ffa040",
33612
+ // both selections — warm amber
33613
+ line: "#ffa040",
33614
+ panelLabel: "#888",
33615
+ panelValue: "#ffd060"
33616
+ };
33617
+ function edgeEntityFromLocalPolyline(localPts, meshMatrixWorld, meshUuid) {
33618
+ const world = localPts.map((p2) => p2.clone().applyMatrix4(meshMatrixWorld));
33619
+ const start = world[0];
33620
+ const end = world[world.length - 1];
33621
+ let length = 0;
33622
+ const segments = [];
33623
+ for (let i = 1; i < world.length; i++) {
33624
+ length += world[i].distanceTo(world[i - 1]);
33625
+ segments.push([world[i - 1], world[i]]);
33626
+ }
33627
+ const dirVec = end.clone().sub(start);
33628
+ const direction = dirVec.lengthSq() > 0 ? dirVec.normalize() : new Vector3(1, 0, 0);
33629
+ const isLine = world.length <= 2;
33630
+ const entity = {
33631
+ kind: "edge",
33632
+ start: [start.x, start.y, start.z],
33633
+ end: [end.x, end.y, end.z],
33634
+ length,
33635
+ direction: [direction.x, direction.y, direction.z],
33636
+ meshUuid,
33637
+ curve: isLine ? { kind: "line", start: [start.x, start.y, start.z], end: [end.x, end.y, end.z] } : { kind: "unidentified" },
33638
+ polyline: isLine ? void 0 : world.map((p2) => [p2.x, p2.y, p2.z])
33274
33639
  };
33640
+ return { entity, preview: { kind: "edge", edgeSegments: segments, meshUuid } };
33275
33641
  }
33276
33642
  function computeMeasureResult(a2, b2) {
33277
33643
  const v32 = (xyz) => new Vector3(...xyz);
@@ -33413,9 +33779,11 @@ function MeasureTool() {
33413
33779
  const measureMode = useForgeStore((s) => s.measureMode);
33414
33780
  const measureSelections = useForgeStore((s) => s.measureSelections);
33415
33781
  const addMeasureSelection = useForgeStore((s) => s.addMeasureSelection);
33782
+ const replaceLastMeasureSelection = useForgeStore((s) => s.replaceLastMeasureSelection);
33416
33783
  const { camera, raycaster, scene, gl } = useThree();
33417
33784
  const [hover, setHover] = reactExports.useState(null);
33418
33785
  const pointerDownRef = reactExports.useRef(null);
33786
+ const lastSelectUpRef = reactExports.useRef(null);
33419
33787
  const [selectionVisuals, setSelectionVisuals] = reactExports.useState({ geos: [], matrices: [], edgeSegments: [], vertexPositions: [] });
33420
33788
  reactExports.useEffect(() => {
33421
33789
  const geos = [];
@@ -33443,7 +33811,15 @@ function MeasureTool() {
33443
33811
  } else if (sel.kind === "edge") {
33444
33812
  geos.push(null);
33445
33813
  matrices.push(null);
33446
- edgeSegs.push([[new Vector3(...sel.start), new Vector3(...sel.end)]]);
33814
+ if (sel.polyline && sel.polyline.length >= 2) {
33815
+ const segs = [];
33816
+ for (let i = 1; i < sel.polyline.length; i++) {
33817
+ segs.push([new Vector3(...sel.polyline[i - 1]), new Vector3(...sel.polyline[i])]);
33818
+ }
33819
+ edgeSegs.push(segs);
33820
+ } else {
33821
+ edgeSegs.push([[new Vector3(...sel.start), new Vector3(...sel.end)]]);
33822
+ }
33447
33823
  vertexPos.push(null);
33448
33824
  } else {
33449
33825
  geos.push(null);
@@ -33491,7 +33867,7 @@ function MeasureTool() {
33491
33867
  [gl.domElement]
33492
33868
  );
33493
33869
  const detectEntity = reactExports.useCallback(
33494
- (event) => {
33870
+ (event, wholePath = false) => {
33495
33871
  if (!measureMode) return null;
33496
33872
  const pointer = getPointerNDC(event);
33497
33873
  raycaster.setFromCamera(new Vector2(pointer.x, pointer.y), camera);
@@ -33521,58 +33897,89 @@ function MeasureTool() {
33521
33897
  const vA = new Vector3().fromBufferAttribute(positions, ia).applyMatrix4(mesh.matrixWorld);
33522
33898
  const vB = new Vector3().fromBufferAttribute(positions, ib).applyMatrix4(mesh.matrixWorld);
33523
33899
  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();
33900
+ const invWorld = new Matrix4().copy(mesh.matrixWorld).invert();
33901
+ const localHit = hit.point.clone().applyMatrix4(invWorld);
33902
+ const bakedEdges = getBakedEdges(geometry);
33903
+ if (bakedEdges.length > 0) {
33904
+ if (!wholePath) {
33905
+ let cornerWorld = null;
33906
+ let cornerDist = SNAP_PX;
33907
+ for (const e2 of bakedEdges) {
33908
+ if (e2.curve.kind !== "line") continue;
33909
+ for (const pt2 of [e2.curve.start, e2.curve.end]) {
33910
+ const w = new Vector3(pt2[0], pt2[1], pt2[2]).applyMatrix4(mesh.matrixWorld);
33911
+ const s = worldToScreen2D(w);
33912
+ const d = Math.hypot(screenX - s.x, screenY - s.y);
33913
+ if (d < cornerDist) {
33914
+ cornerDist = d;
33915
+ cornerWorld = w;
33916
+ }
33917
+ }
33918
+ }
33919
+ if (cornerWorld) {
33920
+ const entity2 = {
33921
+ kind: "vertex",
33922
+ position: [cornerWorld.x, cornerWorld.y, cornerWorld.z],
33923
+ meshUuid: mesh.uuid
33924
+ };
33925
+ return { entity: entity2, preview: { kind: "vertex", vertexPosition: cornerWorld.clone() } };
33926
+ }
33927
+ }
33928
+ const carrierEdge = getNearestEdge(geometry, localHit, Number.POSITIVE_INFINITY);
33929
+ if (carrierEdge && (!wholePath || carrierEdge.edge.curve.kind === "circle")) {
33930
+ const worldPt = carrierEdge.point.clone().applyMatrix4(mesh.matrixWorld);
33931
+ const sp = worldToScreen2D(worldPt);
33932
+ if (Math.hypot(screenX - sp.x, screenY - sp.y) < SNAP_PX * 1.5) {
33933
+ const entity2 = edgeEntityFromBakedEdge(carrierEdge.edge, mesh.matrixWorld, mesh.uuid);
33934
+ const segs = [];
33935
+ const p2 = carrierEdge.edge.points;
33936
+ for (let i = 0; i + 5 < p2.length; i += 3) {
33937
+ segs.push([
33938
+ new Vector3(p2[i], p2[i + 1], p2[i + 2]).applyMatrix4(mesh.matrixWorld),
33939
+ new Vector3(p2[i + 3], p2[i + 4], p2[i + 5]).applyMatrix4(mesh.matrixWorld)
33940
+ ]);
33941
+ }
33942
+ return { entity: entity2, preview: { kind: "edge", edgeSegments: segs, meshUuid: mesh.uuid } };
33943
+ }
33944
+ }
33945
+ } else if (!wholePath) {
33946
+ let closestVertexDist = Infinity;
33947
+ let closestVertex = null;
33948
+ for (const v of [vA, vB, vC]) {
33949
+ const s = worldToScreen2D(v);
33950
+ const d = Math.hypot(screenX - s.x, screenY - s.y);
33951
+ if (d < closestVertexDist && d < SNAP_PX) {
33952
+ closestVertexDist = d;
33953
+ closestVertex = v;
33954
+ }
33955
+ }
33956
+ if (closestVertex) {
33558
33957
  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],
33958
+ kind: "vertex",
33959
+ position: [closestVertex.x, closestVertex.y, closestVertex.z],
33564
33960
  meshUuid: mesh.uuid
33565
33961
  };
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
- };
33962
+ return { entity: entity2, preview: { kind: "vertex", vertexPosition: closestVertex.clone() } };
33963
+ }
33964
+ }
33965
+ const chain = wholePath ? getWholeEdgePath(geometry, localHit) : getNearestEdgeChain(geometry, localHit);
33966
+ if (chain && chain.length >= 2) {
33967
+ const closest = new Vector3();
33968
+ const tmp = new Vector3();
33969
+ let nearestSq = Infinity;
33970
+ for (let i = 1; i < chain.length; i++) {
33971
+ const dSq = closestOnSegment(localHit, chain[i - 1], chain[i], tmp);
33972
+ if (dSq < nearestSq) {
33973
+ nearestSq = dSq;
33974
+ closest.copy(tmp);
33975
+ }
33976
+ }
33977
+ const screenPt = worldToScreen2D(closest.clone().applyMatrix4(mesh.matrixWorld));
33978
+ if (Math.hypot(screenX - screenPt.x, screenY - screenPt.y) < SNAP_PX * 1.5) {
33979
+ return edgeEntityFromLocalPolyline(chain, mesh.matrixWorld, mesh.uuid);
33573
33980
  }
33574
33981
  }
33575
- const ffResult = floodFillFace(geometry, faceIndex);
33982
+ const ffResult = getFaceRegion(geometry, faceIndex);
33576
33983
  const worldNormal = ffResult.normal.clone().transformDirection(mesh.matrixWorld).normalize();
33577
33984
  const worldCenter = ffResult.center.clone().applyMatrix4(mesh.matrixWorld);
33578
33985
  const highlightGeo = buildFaceHighlightGeometry(geometry, ffResult.triangleIndices);
@@ -33636,17 +34043,31 @@ function MeasureTool() {
33636
34043
  const down = pointerDownRef.current;
33637
34044
  pointerDownRef.current = null;
33638
34045
  if (!down || down.moved) return;
34046
+ const clearHover = () => setHover((prev) => {
34047
+ var _a4;
34048
+ (_a4 = prev == null ? void 0 : prev.faceHighlightGeo) == null ? void 0 : _a4.dispose();
34049
+ return null;
34050
+ });
34051
+ const now = performance.now();
34052
+ const last2 = lastSelectUpRef.current;
34053
+ const isDouble = !!last2 && last2.added && now - last2.t < 300 && Math.hypot(event.clientX - last2.x, event.clientY - last2.y) < 6;
34054
+ if (isDouble) {
34055
+ const whole = detectEntity(event, true);
34056
+ if (whole) {
34057
+ replaceLastMeasureSelection(whole.entity);
34058
+ clearHover();
34059
+ }
34060
+ lastSelectUpRef.current = null;
34061
+ return;
34062
+ }
33639
34063
  const result = detectEntity(event);
33640
34064
  if (result) {
33641
34065
  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
- });
34066
+ clearHover();
33647
34067
  }
34068
+ lastSelectUpRef.current = { t: now, x: event.clientX, y: event.clientY, added: !!result };
33648
34069
  },
33649
- [addMeasureSelection, detectEntity, measureMode]
34070
+ [addMeasureSelection, detectEntity, measureMode, replaceLastMeasureSelection]
33650
34071
  );
33651
34072
  reactExports.useEffect(() => {
33652
34073
  if (!measureMode) {
@@ -33859,15 +34280,31 @@ function MeasureInfoPanel() {
33859
34280
  ] });
33860
34281
  }
33861
34282
  if (sel.kind === "edge") {
34283
+ const circle = circleMeasurement(sel.curve);
33862
34284
  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) })
34285
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, marginBottom: 6, color: MEASURE_COLORS.selection }, children: circle ? "Circular Edge" : "Edge" }),
34286
+ circle ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
34287
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34288
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Radius" }),
34289
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue, fontWeight: 600, fontSize: 14 }, children: formatLength(circle.radius, lengthUnit) })
34290
+ ] }),
34291
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34292
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Diameter" }),
34293
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue, fontWeight: 600, fontSize: 14 }, children: formatLength(circle.diameter, lengthUnit) })
34294
+ ] }),
34295
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34296
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Circumference" }),
34297
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue }, children: formatLength(circle.circumference, lengthUnit) })
34298
+ ] })
34299
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
34300
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34301
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Length" }),
34302
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelValue }, children: formatLength(sel.length, lengthUnit) })
34303
+ ] }),
34304
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: ROW_STYLE, children: [
34305
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: MEASURE_COLORS.panelLabel }, children: "Direction" }),
34306
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: fmtNormal(sel.direction) })
34307
+ ] })
33871
34308
  ] }),
33872
34309
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { marginTop: 6, fontSize: 10, color: MEASURE_COLORS.panelLabel }, children: "Click another entity to measure" })
33873
34310
  ] });
@@ -38530,7 +38967,7 @@ const computeExplodeTreeOffsets = (root, explodeAmount, explodeConfig) => {
38530
38967
  if (Math.abs(config.amount) <= 1e-8) return {};
38531
38968
  const rootCenter = explodeBoundsCenter(root.bounds) ?? [0, 0, 0];
38532
38969
  const offsets = {};
38533
- const walk = (node, depth, inherited, parentCenter, parentDirection) => {
38970
+ const walk2 = (node, depth, inherited, parentCenter, parentDirection) => {
38534
38971
  const center = explodeBoundsCenter(node.bounds) ?? parentCenter;
38535
38972
  const directive = resolveExplodeDirective([node.path.join("/")], node.label, void 0, config);
38536
38973
  const motion = depth > 1 && node.children.length === 0 && !hasExplodeOverride(directive) ? (() => {
@@ -38554,9 +38991,9 @@ const computeExplodeTreeOffsets = (root, explodeAmount, explodeConfig) => {
38554
38991
  node.objectIds.forEach((objectId) => {
38555
38992
  offsets[objectId] = total;
38556
38993
  });
38557
- node.children.forEach((child) => walk(child, depth + 1, total, center, motion.branchDirection));
38994
+ node.children.forEach((child) => walk2(child, depth + 1, total, center, motion.branchDirection));
38558
38995
  };
38559
- root.children.forEach((child) => walk(child, 1, [0, 0, 0], rootCenter, void 0));
38996
+ root.children.forEach((child) => walk2(child, 1, [0, 0, 0], rootCenter, void 0));
38560
38997
  return offsets;
38561
38998
  };
38562
38999
  const EMPTY_RIG_INSPECTION_OVERLAY_STATE = {
@@ -39474,6 +39911,48 @@ function shouldRequestInitialFit({
39474
39911
  if (initialFitRequested && previousPreviewFile === previewFile) return false;
39475
39912
  return true;
39476
39913
  }
39914
+ const SUBFEATURE_SNAP_PX = 14;
39915
+ const CLICK_DRAG_TOLERANCE_PX = 4;
39916
+ function worldToScreenPx(pt2, camera, rect) {
39917
+ const p2 = pt2.clone().project(camera);
39918
+ return {
39919
+ x: (p2.x * 0.5 + 0.5) * rect.width + rect.left,
39920
+ y: (-p2.y * 0.5 + 0.5) * rect.height + rect.top
39921
+ };
39922
+ }
39923
+ function drillIntoSubFeature(mesh, worldHit, camera, rect, clientX, clientY, objectId, store) {
39924
+ const geometry = mesh.geometry;
39925
+ const inv = new Matrix4().copy(mesh.matrixWorld).invert();
39926
+ const localHit = worldHit.clone().applyMatrix4(inv);
39927
+ const vtx = getNearestVertex(geometry, localHit, Number.POSITIVE_INFINITY);
39928
+ if (vtx) {
39929
+ const worldPt = vtx.point.clone().applyMatrix4(mesh.matrixWorld);
39930
+ const s = worldToScreenPx(worldPt, camera, rect);
39931
+ if (Math.hypot(clientX - s.x, clientY - s.y) <= SUBFEATURE_SNAP_PX) {
39932
+ store.setSelectedEdge(null);
39933
+ store.setSelectedVertex({ objectId, point: [vtx.point.x, vtx.point.y, vtx.point.z] });
39934
+ return true;
39935
+ }
39936
+ }
39937
+ const edge = getNearestEdge(geometry, localHit, Number.POSITIVE_INFINITY);
39938
+ if (edge) {
39939
+ const worldPt = edge.point.clone().applyMatrix4(mesh.matrixWorld);
39940
+ const s = worldToScreenPx(worldPt, camera, rect);
39941
+ if (Math.hypot(clientX - s.x, clientY - s.y) <= SUBFEATURE_SNAP_PX) {
39942
+ store.setSelectedVertex(null);
39943
+ store.setSelectedEdge({
39944
+ objectId,
39945
+ faceA: edge.edge.faceA,
39946
+ faceB: edge.edge.faceB,
39947
+ curve: edge.edge.curve,
39948
+ length: edge.edge.length,
39949
+ points: edge.edge.points
39950
+ });
39951
+ return true;
39952
+ }
39953
+ }
39954
+ return false;
39955
+ }
39477
39956
  function useViewportHandlers({
39478
39957
  containerRef,
39479
39958
  contextMenuRef,
@@ -39505,6 +39984,14 @@ function useViewportHandlers({
39505
39984
  const [faceInfoData, setFaceInfoData] = reactExports.useState(null);
39506
39985
  const [faceInfoLoading, setFaceInfoLoading] = reactExports.useState(false);
39507
39986
  const [sketchEntityInfo, setSketchEntityInfo] = reactExports.useState(null);
39987
+ const pointerDownPosRef = reactExports.useRef(null);
39988
+ reactExports.useEffect(() => {
39989
+ const onDown = (e2) => {
39990
+ pointerDownPosRef.current = { x: e2.clientX, y: e2.clientY };
39991
+ };
39992
+ window.addEventListener("pointerdown", onDown, { capture: true });
39993
+ return () => window.removeEventListener("pointerdown", onDown, { capture: true });
39994
+ }, []);
39508
39995
  const closeObjectContextMenu = reactExports.useCallback(() => {
39509
39996
  setObjectContextMenu(null);
39510
39997
  }, []);
@@ -39565,6 +40052,20 @@ function useViewportHandlers({
39565
40052
  closeObjectContextMenu();
39566
40053
  return;
39567
40054
  }
40055
+ const store = useForgeStore.getState();
40056
+ if (store.selectedVertex) {
40057
+ store.setSelectedVertex(null);
40058
+ return;
40059
+ }
40060
+ if (store.selectedEdge) {
40061
+ store.setSelectedEdge(null);
40062
+ return;
40063
+ }
40064
+ if (store.selectedFace) {
40065
+ store.setSelectedFace(null);
40066
+ setFaceInfoPanel(null);
40067
+ return;
40068
+ }
39568
40069
  if (faceInfoPanel) {
39569
40070
  setFaceInfoPanel(null);
39570
40071
  return;
@@ -39573,7 +40074,6 @@ function useViewportHandlers({
39573
40074
  setSketchEntityInfo(null);
39574
40075
  return;
39575
40076
  }
39576
- const store = useForgeStore.getState();
39577
40077
  if (store.measureMode) {
39578
40078
  if (store.measureSelections.length > 0) {
39579
40079
  store.clearMeasureSelections();
@@ -39586,8 +40086,13 @@ function useViewportHandlers({
39586
40086
  store.setConstructionGhost(null);
39587
40087
  return;
39588
40088
  }
39589
- if (store.focusedObjectIds.length === 0) return;
39590
- clearFocusedObject();
40089
+ if (store.focusedObjectIds.length > 0) {
40090
+ clearFocusedObject();
40091
+ return;
40092
+ }
40093
+ if (store.selectedObjectId) {
40094
+ store.selectObject(null);
40095
+ }
39591
40096
  };
39592
40097
  window.addEventListener("keydown", handleEscape);
39593
40098
  return () => window.removeEventListener("keydown", handleEscape);
@@ -39681,11 +40186,42 @@ function useViewportHandlers({
39681
40186
  );
39682
40187
  const handleObjectClick = reactExports.useCallback(
39683
40188
  (obj, event) => {
40189
+ var _a3;
39684
40190
  if (!objectPickSyncEnabled || measureMode || isViewportInteracting) return;
40191
+ const down = pointerDownPosRef.current;
40192
+ if (down && Math.hypot(event.clientX - down.x, event.clientY - down.y) > CLICK_DRAG_TOLERANCE_PX) return;
39685
40193
  event.stopPropagation();
39686
- selectObject(obj.id);
40194
+ const store = useForgeStore.getState();
40195
+ const alreadySelected = store.selectedObjectId === obj.id;
40196
+ if (!alreadySelected) {
40197
+ selectObject(obj.id);
40198
+ setFaceInfoPanel(null);
40199
+ return;
40200
+ }
40201
+ const rectForPick = (_a3 = containerRef.current) == null ? void 0 : _a3.getBoundingClientRect();
40202
+ if (event.object instanceof Mesh && rectForPick && event.point && drillIntoSubFeature(event.object, event.point, event.camera, rectForPick, event.clientX, event.clientY, obj.id, store)) {
40203
+ store.setSelectedFace(null);
40204
+ setFaceInfoPanel(null);
40205
+ return;
40206
+ }
40207
+ const triangleIndex = event.faceIndex ?? (event.face ? Math.floor(event.face.a / 3) : null);
40208
+ if (event.object instanceof Mesh && triangleIndex !== null && triangleIndex >= 0) {
40209
+ store.setSelectedEdge(null);
40210
+ store.setSelectedVertex(null);
40211
+ const region = getFaceRegion(event.object.geometry, triangleIndex);
40212
+ const worldNormal = region.normal.clone().transformDirection(event.object.matrixWorld).normalize();
40213
+ const worldCenter = region.center.clone().applyMatrix4(event.object.matrixWorld);
40214
+ store.setSelectedFace({
40215
+ objectId: obj.id,
40216
+ carrierName: region.carrierName,
40217
+ triangleIndices: region.triangleIndices,
40218
+ meshUuid: event.object.uuid,
40219
+ normal: [worldNormal.x, worldNormal.y, worldNormal.z],
40220
+ center: [worldCenter.x, worldCenter.y, worldCenter.z]
40221
+ });
40222
+ }
39687
40223
  },
39688
- [isViewportInteracting, measureMode, objectPickSyncEnabled, selectObject]
40224
+ [containerRef, isViewportInteracting, measureMode, objectPickSyncEnabled, selectObject, setFaceInfoPanel]
39689
40225
  );
39690
40226
  const handleObjectDoubleClick = reactExports.useCallback(
39691
40227
  (obj, event) => {
@@ -39693,8 +40229,13 @@ function useViewportHandlers({
39693
40229
  event.stopPropagation();
39694
40230
  const additive = event.shiftKey || event.metaKey || event.ctrlKey;
39695
40231
  focusObject(obj.id, { additive });
40232
+ const store = useForgeStore.getState();
40233
+ store.setSelectedFace(null);
40234
+ store.setSelectedEdge(null);
40235
+ store.setSelectedVertex(null);
40236
+ setFaceInfoPanel(null);
39696
40237
  },
39697
- [focusObject, isViewportInteracting, measureMode]
40238
+ [focusObject, isViewportInteracting, measureMode, setFaceInfoPanel]
39698
40239
  );
39699
40240
  const handleObjectContextMenu = reactExports.useCallback(
39700
40241
  (obj, event) => {
@@ -40965,7 +41506,7 @@ function useGeometryComparison(args) {
40965
41506
  }, [args.activeBackend, args.candidate, args.enabled, args.files, args.previewFile, args.quality]);
40966
41507
  return state2;
40967
41508
  }
40968
- const PARALLEL_CHANNELS = /* @__PURE__ */ new Set(["thickness", "roughness"]);
41509
+ const PARALLEL_CHANNELS = /* @__PURE__ */ new Set(["thickness", "throughThickness", "roughness"]);
40969
41510
  const MAX_INSPECT_WORKERS = Math.max(
40970
41511
  1,
40971
41512
  Math.min((typeof navigator !== "undefined" && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4) - 1, 8)
@@ -41021,7 +41562,7 @@ function mergeResults(results, channel, analysisId) {
41021
41562
  class InspectWorkerClient {
41022
41563
  constructor(workerFactory = () => new Worker(new URL(
41023
41564
  /* @vite-ignore */
41024
- "/assets/inspectWorker-ymhBV4Ll.js",
41565
+ "/assets/inspectWorker-UXMxlcR8.js",
41025
41566
  import.meta.url
41026
41567
  ), { type: "module" })) {
41027
41568
  __publicField(this, "reqId", 0);
@@ -41075,8 +41616,16 @@ class InspectWorkerClient {
41075
41616
  }
41076
41617
  }
41077
41618
  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"]);
41619
+ const WORKER_CHANNELS = /* @__PURE__ */ new Set([
41620
+ "thickness",
41621
+ "throughThickness",
41622
+ "roughness",
41623
+ "connectivity",
41624
+ "floating",
41625
+ "distance",
41626
+ "collisions"
41627
+ ]);
41628
+ const SCALAR_WORKER_CHANNELS = /* @__PURE__ */ new Set(["thickness", "throughThickness", "roughness"]);
41080
41629
  const MESH_COMPONENT_WORKER_CHANNELS = /* @__PURE__ */ new Set(["connectivity", "floating", "distance"]);
41081
41630
  const IDENTITY_MATRIX_ELEMENTS = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
41082
41631
  const INSPECT_BUILD_YIELD_INTERVAL_MS = 8;
@@ -41386,6 +41935,15 @@ function analyzePayloadFor(channel, objects, inspectPointSampleCount, groundZ, w
41386
41935
  }
41387
41936
  };
41388
41937
  }
41938
+ if (channel === "throughThickness") {
41939
+ return {
41940
+ channel,
41941
+ objects,
41942
+ throughThickness: {
41943
+ maxSamplesPerObject
41944
+ }
41945
+ };
41946
+ }
41389
41947
  if (channel === "roughness") {
41390
41948
  return {
41391
41949
  channel,
@@ -41413,6 +41971,10 @@ function resultToState(channel, result) {
41413
41971
  normals: object.normals,
41414
41972
  index: object.index,
41415
41973
  aValue: object.aValue,
41974
+ uvs: object.uvs,
41975
+ textureValues: object.textureValues,
41976
+ textureWidth: object.textureWidth,
41977
+ textureHeight: object.textureHeight,
41416
41978
  valueMin: object.valueMin,
41417
41979
  valueMax: object.valueMax,
41418
41980
  capped: object.capped,
@@ -41743,6 +42305,24 @@ function panelNumber(value) {
41743
42305
  function vec3Label(value) {
41744
42306
  return `[${value.map(panelNumber).join(", ")}]`;
41745
42307
  }
42308
+ function localPointToWorld(point, matrix) {
42309
+ const v = new Vector3(point[0], point[1], point[2]).applyMatrix4(matrix);
42310
+ return [v.x, v.y, v.z];
42311
+ }
42312
+ function localDirToWorld(dir, matrix) {
42313
+ const normal = new Matrix3().getNormalMatrix(matrix);
42314
+ const v = new Vector3(dir[0], dir[1], dir[2]).applyMatrix3(normal).normalize();
42315
+ return [v.x, v.y, v.z];
42316
+ }
42317
+ function worldScaleFactor(matrix) {
42318
+ const det = matrix.determinant();
42319
+ const scale = Math.cbrt(Math.abs(det));
42320
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
42321
+ }
42322
+ function copyNumberToClipboard(value) {
42323
+ var _a3;
42324
+ void ((_a3 = navigator.clipboard) == null ? void 0 : _a3.writeText(panelNumber(value)));
42325
+ }
41746
42326
  function edgeCurveLabel(edge) {
41747
42327
  var _a3;
41748
42328
  switch ((_a3 = edge.curve) == null ? void 0 : _a3.kind) {
@@ -41778,6 +42358,8 @@ function inspectChannelLabel(channel) {
41778
42358
  return "Comparison";
41779
42359
  case "thickness":
41780
42360
  return "Thickness";
42361
+ case "throughThickness":
42362
+ return "Minimum Solid Span";
41781
42363
  case "roughness":
41782
42364
  return "Roughness";
41783
42365
  default:
@@ -41993,6 +42575,11 @@ function Viewport() {
41993
42575
  const runQuality = useForgeStore((s) => s.runQuality);
41994
42576
  const buildLedgerEvents = useForgeStore((s) => s.buildLedgerEvents);
41995
42577
  const measureSelections = useForgeStore((s) => s.measureSelections);
42578
+ const selectedFace = useForgeStore((s) => s.selectedFace);
42579
+ const selectedEdge = useForgeStore((s) => s.selectedEdge);
42580
+ const selectedVertex = useForgeStore((s) => s.selectedVertex);
42581
+ const setSelectedEdge = useForgeStore((s) => s.setSelectedEdge);
42582
+ const setSelectedVertex = useForgeStore((s) => s.setSelectedVertex);
41996
42583
  const meshPreviewFile = useForgeStore((s) => s.meshPreviewFile);
41997
42584
  const voxelIntentMode = useForgeStore((s) => s.voxelIntentMode);
41998
42585
  const voxelIntentTool = useForgeStore((s) => s.voxelIntentTool);
@@ -42176,15 +42763,35 @@ function Viewport() {
42176
42763
  groundZ: inspectGroundZ
42177
42764
  });
42178
42765
  const scalarChannelInfo = reactExports.useMemo(() => {
42179
- if (inspectChannel === "thickness") {
42766
+ if (inspectChannel === "thickness" || inspectChannel === "throughThickness") {
42180
42767
  return { unitLabel: "mm", criticalThreshold: DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness };
42181
42768
  }
42182
42769
  if (inspectChannel === "roughness") return { unitLabel: "deg", criticalThreshold: null };
42183
42770
  return null;
42184
42771
  }, [inspectChannel]);
42772
+ const measuredThicknessMax = reactExports.useMemo(() => {
42773
+ if (inspectChannel !== "thickness" && inspectChannel !== "throughThickness") return null;
42774
+ let max2 = -Infinity;
42775
+ for (const surface of Object.values(inspectAnalysis.scalarSurfaces)) {
42776
+ if (Number.isFinite(surface.valueMax)) max2 = Math.max(max2, surface.valueMax);
42777
+ }
42778
+ return Number.isFinite(max2) ? max2 : null;
42779
+ }, [inspectAnalysis.scalarSurfaces, inspectChannel]);
42780
+ const effectiveThicknessColorRange = reactExports.useMemo(() => {
42781
+ const isDefaultRange = thicknessColorRange.min === DEFAULT_THICKNESS_COLOR_RANGE.min && thicknessColorRange.max === DEFAULT_THICKNESS_COLOR_RANGE.max;
42782
+ if (isDefaultRange && measuredThicknessMax != null && measuredThicknessMax > thicknessColorRange.max) {
42783
+ return { min: thicknessColorRange.min, max: measuredThicknessMax };
42784
+ }
42785
+ return thicknessColorRange;
42786
+ }, [measuredThicknessMax, thicknessColorRange]);
42185
42787
  const inspectColorScale = reactExports.useMemo(
42186
- () => ({ colormap: inspectColormap, domainMin: thicknessColorRange.min, domainMax: thicknessColorRange.max }),
42187
- [inspectColormap, thicknessColorRange.min, thicknessColorRange.max]
42788
+ () => ({
42789
+ colormap: inspectColormap,
42790
+ domainMin: effectiveThicknessColorRange.min,
42791
+ domainMax: effectiveThicknessColorRange.max,
42792
+ ...(inspectChannel === "thickness" || inspectChannel === "throughThickness") && inspectColormap !== "thickness-classic" ? { reversed: true } : {}
42793
+ }),
42794
+ [effectiveThicknessColorRange.max, effectiveThicknessColorRange.min, inspectChannel, inspectColormap]
42188
42795
  );
42189
42796
  const inspectScalarParams = reactExports.useMemo(() => {
42190
42797
  if (!scalarChannelInfo) return void 0;
@@ -42195,11 +42802,12 @@ function Viewport() {
42195
42802
  isolineSpacing: inspectIsolineSpacing,
42196
42803
  criticalEnabled: inspectCriticalLineEnabled,
42197
42804
  criticalThreshold: scalarChannelInfo.criticalThreshold,
42198
- shadingEnabled: true
42805
+ shadingEnabled: inspectChannel !== "thickness" && inspectChannel !== "throughThickness"
42199
42806
  };
42200
42807
  }, [
42201
42808
  scalarChannelInfo,
42202
42809
  inspectColorScale,
42810
+ inspectChannel,
42203
42811
  inspectQuantizeBands,
42204
42812
  inspectIsolinesEnabled,
42205
42813
  inspectIsolineSpacing,
@@ -42440,7 +43048,7 @@ function Viewport() {
42440
43048
  displayMode: inspectDisplayMode,
42441
43049
  warnings: inspectWarnings,
42442
43050
  swatches: inspectLegendSwatches,
42443
- thicknessColorRange,
43051
+ thicknessColorRange: effectiveThicknessColorRange,
42444
43052
  onThicknessColorRangeChange: setThicknessColorRange,
42445
43053
  colorScale: scalarChannelInfo ? inspectColorScale : void 0,
42446
43054
  unitLabel: scalarChannelInfo == null ? void 0 : scalarChannelInfo.unitLabel,
@@ -42674,6 +43282,9 @@ function Viewport() {
42674
43282
  sectionPlanes: objectSectionPlanesById[obj.id] ?? EMPTY_SECTION_PLANES,
42675
43283
  sectionPreviewRenderOrderBase: 2e3 + objIndex * 64,
42676
43284
  debugHighlightColor: shapeHl == null ? void 0 : shapeHl.color,
43285
+ selectedFaceTriangleIndices: (selectedFace == null ? void 0 : selectedFace.objectId) === obj.id ? selectedFace.triangleIndices : null,
43286
+ selectedEdgePoints: (selectedEdge == null ? void 0 : selectedEdge.objectId) === obj.id ? selectedEdge.points : null,
43287
+ selectedVertexPoint: (selectedVertex == null ? void 0 : selectedVertex.objectId) === obj.id ? selectedVertex.point : null,
42677
43288
  onPointerEnter: (event) => updateHoverLabel(obj, event),
42678
43289
  onPointerMove: (event) => updateHoverLabel(obj, event),
42679
43290
  onPointerLeave: (event) => clearHoverLabel(obj, event),
@@ -43129,6 +43740,7 @@ function Viewport() {
43129
43740
  ),
43130
43741
  viewportPortalHost && faceInfoPanel && reactDomExports.createPortal(
43131
43742
  (() => {
43743
+ var _a4;
43132
43744
  const obj = objects.find((o2) => o2.id === faceInfoPanel.objectId);
43133
43745
  if (!obj) return null;
43134
43746
  const activeFaceName = faceInfoPanel.faceName;
@@ -43139,8 +43751,8 @@ function Viewport() {
43139
43751
  const history = activeFaceName ? (faceInfoData == null ? void 0 : faceInfoData.faceHistories[activeFaceName]) ?? null : null;
43140
43752
  const faceNames = (faceInfoData == null ? void 0 : faceInfoData.faceNames) ?? [];
43141
43753
  const activeEdges = activeFaceName && faceInfoData ? (faceInfoData.edgeNames ?? []).map((name) => {
43142
- var _a4;
43143
- return (_a4 = faceInfoData.edges) == null ? void 0 : _a4[name];
43754
+ var _a5;
43755
+ return (_a5 = faceInfoData.edges) == null ? void 0 : _a5[name];
43144
43756
  }).filter((edge) => Boolean(edge) && edgeBelongsToFace(edge, activeFaceName)) : [];
43145
43757
  const visibleEdges = activeEdges.slice(0, 12);
43146
43758
  const viewportMaxRight = Math.min(viewportScreenRight, viewportWindowWidth);
@@ -43171,23 +43783,47 @@ function Viewport() {
43171
43783
  children: [
43172
43784
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }, children: [
43173
43785
  /* @__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
- )
43786
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
43787
+ activeFaceName && activeFace && /* @__PURE__ */ jsxRuntimeExports.jsx(
43788
+ "button",
43789
+ {
43790
+ type: "button",
43791
+ title: `Copy face("${activeFaceName}") selector to clipboard`,
43792
+ onClick: () => {
43793
+ var _a5;
43794
+ void ((_a5 = navigator.clipboard) == null ? void 0 : _a5.writeText(`face(${JSON.stringify(activeFaceName)})`));
43795
+ },
43796
+ style: {
43797
+ border: "1px solid var(--fc-border)",
43798
+ background: "transparent",
43799
+ color: "var(--fc-textMuted)",
43800
+ cursor: "pointer",
43801
+ fontSize: 11,
43802
+ lineHeight: 1,
43803
+ borderRadius: 4,
43804
+ padding: "3px 6px"
43805
+ },
43806
+ children: "Copy selector"
43807
+ }
43808
+ ),
43809
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43810
+ "button",
43811
+ {
43812
+ type: "button",
43813
+ onClick: () => setFaceInfoPanel(null),
43814
+ style: {
43815
+ border: "none",
43816
+ background: "transparent",
43817
+ color: "var(--fc-textMuted)",
43818
+ cursor: "pointer",
43819
+ fontSize: 16,
43820
+ lineHeight: 1,
43821
+ padding: 0
43822
+ },
43823
+ children: "×"
43824
+ }
43825
+ )
43826
+ ] })
43191
43827
  ] }),
43192
43828
  /* @__PURE__ */ jsxRuntimeExports.jsx(
43193
43829
  "div",
@@ -43274,6 +43910,33 @@ function Viewport() {
43274
43910
  ]
43275
43911
  }
43276
43912
  ),
43913
+ ((_a4 = history == null ? void 0 : history.origin) == null ? void 0 : _a4.sourceSpan) && /* @__PURE__ */ jsxRuntimeExports.jsxs(
43914
+ "div",
43915
+ {
43916
+ style: {
43917
+ marginBottom: 10,
43918
+ padding: 8,
43919
+ border: "1px solid var(--fc-border)",
43920
+ borderRadius: 6,
43921
+ background: "color-mix(in srgb, var(--fc-bgInput) 70%, transparent)"
43922
+ },
43923
+ children: [
43924
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { fontSize: 11, fontWeight: 600, marginBottom: 3 }, children: "Source" }),
43925
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
43926
+ "div",
43927
+ {
43928
+ style: {
43929
+ fontSize: 10,
43930
+ color: "var(--fc-textMuted)",
43931
+ fontFamily: "monospace",
43932
+ wordBreak: "break-all"
43933
+ },
43934
+ children: `${history.origin.sourceSpan.fileName}:${history.origin.sourceSpan.line}:${history.origin.sourceSpan.column}`
43935
+ }
43936
+ )
43937
+ ]
43938
+ }
43939
+ ),
43277
43940
  history && history.timeline.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: history.timeline.map((entry, i) => {
43278
43941
  const isFirst = i === 0;
43279
43942
  const isLast = i === history.timeline.length - 1;
@@ -43489,12 +44152,164 @@ function Viewport() {
43489
44152
  ]
43490
44153
  }
43491
44154
  );
44155
+ })(),
44156
+ (selectedEdge || selectedVertex) && (() => {
44157
+ const objectId = (selectedEdge == null ? void 0 : selectedEdge.objectId) ?? (selectedVertex == null ? void 0 : selectedVertex.objectId) ?? "";
44158
+ const obj = objects.find((o2) => o2.id === objectId);
44159
+ const matrix = objectMatrices[objectId] ?? new Matrix4();
44160
+ const scale = worldScaleFactor(matrix);
44161
+ let title = "";
44162
+ const rows = [];
44163
+ let note = null;
44164
+ if (selectedVertex) {
44165
+ title = "Vertex";
44166
+ const world = localPointToWorld(selectedVertex.point, matrix);
44167
+ rows.push(["Coordinate", formatCoord(world, lengthUnit)]);
44168
+ rows.push(["X", formatLength(world[0], lengthUnit, 3), world[0]]);
44169
+ rows.push(["Y", formatLength(world[1], lengthUnit, 3), world[1]]);
44170
+ rows.push(["Z", formatLength(world[2], lengthUnit, 3), world[2]]);
44171
+ } else if (selectedEdge) {
44172
+ const { curve } = selectedEdge;
44173
+ if (curve.kind === "circle") {
44174
+ title = "Edge — Circle";
44175
+ const radius = curve.radius * scale;
44176
+ const center = localPointToWorld(curve.center, matrix);
44177
+ const axis = localDirToWorld(curve.axis, matrix);
44178
+ rows.push(["Radius", formatLength(radius, lengthUnit, 3), radius]);
44179
+ rows.push(["Diameter", formatLength(radius * 2, lengthUnit, 3), radius * 2]);
44180
+ rows.push(["Center", formatCoord(center, lengthUnit)]);
44181
+ rows.push(["Axis", vec3Label(axis)]);
44182
+ } else if (curve.kind === "line") {
44183
+ title = "Edge — Line";
44184
+ const start = localPointToWorld(curve.start, matrix);
44185
+ const end = localPointToWorld(curve.end, matrix);
44186
+ const dir = new Vector3(end[0] - start[0], end[1] - start[1], end[2] - start[2]).normalize();
44187
+ const length = selectedEdge.length * scale;
44188
+ rows.push(["Length", formatLength(length, lengthUnit, 3), length]);
44189
+ rows.push(["Direction", vec3Label([dir.x, dir.y, dir.z])]);
44190
+ rows.push(["Start", formatCoord(start, lengthUnit)]);
44191
+ rows.push(["End", formatCoord(end, lengthUnit)]);
44192
+ } else {
44193
+ title = "Edge — Non-analytic";
44194
+ const length = selectedEdge.length * scale;
44195
+ rows.push(["Length", formatLength(length, lengthUnit, 3), length]);
44196
+ note = "Non-analytic edge: length only (polyline). No analytic radius/axis.";
44197
+ }
44198
+ }
44199
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
44200
+ "div",
44201
+ {
44202
+ className: "fc-viewport-floating-panel fc-edge-info-panel",
44203
+ style: {
44204
+ position: "absolute",
44205
+ left: 16,
44206
+ bottom: 16,
44207
+ width: 248,
44208
+ background: "var(--fc-floating-panel-bg, var(--fc-bgPanel))",
44209
+ border: "1px solid var(--fc-floating-panel-border, var(--fc-border))",
44210
+ borderRadius: "var(--fc-floating-panel-radius, 8px)",
44211
+ boxShadow: "var(--fc-floating-panel-shadow, 0 12px 28px rgba(0, 0, 0, 0.28))",
44212
+ padding: 12,
44213
+ zIndex: 20,
44214
+ fontSize: 12,
44215
+ color: "var(--fc-text)"
44216
+ },
44217
+ onPointerDown: (e2) => e2.stopPropagation(),
44218
+ onContextMenu: (e2) => e2.preventDefault(),
44219
+ children: [
44220
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [
44221
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontWeight: 600, fontSize: 13 }, children: title }),
44222
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44223
+ "button",
44224
+ {
44225
+ type: "button",
44226
+ onClick: () => {
44227
+ setSelectedEdge(null);
44228
+ setSelectedVertex(null);
44229
+ },
44230
+ style: {
44231
+ border: "none",
44232
+ background: "transparent",
44233
+ color: "var(--fc-textMuted)",
44234
+ cursor: "pointer",
44235
+ fontSize: 16,
44236
+ lineHeight: 1,
44237
+ padding: 0
44238
+ },
44239
+ children: "×"
44240
+ }
44241
+ )
44242
+ ] }),
44243
+ obj && /* @__PURE__ */ jsxRuntimeExports.jsx(
44244
+ "div",
44245
+ {
44246
+ style: {
44247
+ fontSize: 11,
44248
+ color: "var(--fc-textMuted)",
44249
+ marginBottom: 8,
44250
+ overflow: "hidden",
44251
+ textOverflow: "ellipsis",
44252
+ whiteSpace: "nowrap"
44253
+ },
44254
+ children: obj.treePath && obj.treePath.length > 0 ? obj.treePath.join(" / ") : obj.name
44255
+ }
44256
+ ),
44257
+ rows.map(([label, value, raw]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
44258
+ "div",
44259
+ {
44260
+ style: { display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8, marginBottom: 5 },
44261
+ children: [
44262
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "var(--fc-textMuted)", fontSize: 11 }, children: label }),
44263
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { style: { display: "flex", alignItems: "baseline", gap: 6, minWidth: 0 }, children: [
44264
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { fontFamily: "monospace", fontSize: 11, textAlign: "right", wordBreak: "break-word" }, children: value }),
44265
+ raw !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
44266
+ "button",
44267
+ {
44268
+ type: "button",
44269
+ title: `Copy ${label.toLowerCase()} value (${panelNumber(raw)} mm) to clipboard`,
44270
+ onClick: () => copyNumberToClipboard(raw),
44271
+ style: {
44272
+ flexShrink: 0,
44273
+ border: "1px solid var(--fc-border)",
44274
+ background: "transparent",
44275
+ color: "var(--fc-textMuted)",
44276
+ cursor: "pointer",
44277
+ fontSize: 9,
44278
+ lineHeight: 1,
44279
+ borderRadius: 3,
44280
+ padding: "2px 4px"
44281
+ },
44282
+ children: "Copy"
44283
+ }
44284
+ )
44285
+ ] })
44286
+ ]
44287
+ },
44288
+ label
44289
+ )),
44290
+ note && /* @__PURE__ */ jsxRuntimeExports.jsx(
44291
+ "div",
44292
+ {
44293
+ style: {
44294
+ marginTop: 6,
44295
+ paddingTop: 6,
44296
+ borderTop: "1px solid var(--fc-border)",
44297
+ fontSize: 10,
44298
+ color: "var(--fc-textMuted)",
44299
+ lineHeight: 1.35
44300
+ },
44301
+ children: note
44302
+ }
44303
+ )
44304
+ ]
44305
+ }
44306
+ );
43492
44307
  })()
43493
44308
  ]
43494
44309
  }
43495
44310
  );
43496
44311
  }
43497
- const EditorApp$1 = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-BWUGCdD5.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
44312
+ const EditorApp$1 = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-DQJmcmRT.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
43498
44313
  const PENDING_SHARE_COPY_KEY = "fc-pending-share-copy";
43499
44314
  function storePendingShareCopy(shareId) {
43500
44315
  sessionStorage.setItem(PENDING_SHARE_COPY_KEY, shareId);
@@ -43760,17 +44575,17 @@ function SeoMetadata() {
43760
44575
  }, [location.pathname]);
43761
44576
  return null;
43762
44577
  }
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 })));
44578
+ reactExports.lazy(() => __vitePreload(() => import("./LandingPageProofDriven-DbE_tp8-.js"), true ? __vite__mapDeps([1]) : void 0).then((m2) => ({ default: m2.LandingPageProofDriven })));
44579
+ const DocsPage = reactExports.lazy(() => __vitePreload(() => import("./DocsPage-DO1kvBns.js"), true ? [] : void 0).then((m2) => ({ default: m2.DocsPage })));
44580
+ reactExports.lazy(() => __vitePreload(() => import("./BlogPage-D7Dos-vl.js"), true ? [] : void 0).then((m2) => ({ default: m2.BlogPage })));
44581
+ reactExports.lazy(() => __vitePreload(() => import("./BenchmarkPage-DP3RxhPs.js"), true ? __vite__mapDeps([1,2]) : void 0).then((m2) => ({ default: m2.BenchmarkPage })));
44582
+ reactExports.lazy(() => __vitePreload(() => import("./AdminPage-raksfnNA.js"), true ? [] : void 0).then((m2) => ({ default: m2.AdminPage })));
43768
44583
  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 })));
44584
+ reactExports.lazy(() => __vitePreload(() => import("./SettingsPage-DLWcP289.js"), true ? [] : void 0).then((m2) => ({ default: m2.SettingsPage })));
44585
+ reactExports.lazy(() => __vitePreload(() => import("./PricingPage-CcVIN9yj.js"), true ? __vite__mapDeps([1,3]) : void 0).then((m2) => ({ default: m2.PricingPage })));
44586
+ reactExports.lazy(() => __vitePreload(() => import("./LegalPage-CominSso.js"), true ? __vite__mapDeps([1,4]) : void 0).then((m2) => ({ default: m2.LegalPage })));
44587
+ const EditorApp = reactExports.lazy(() => __vitePreload(() => import("./EditorApp-DQJmcmRT.js"), true ? __vite__mapDeps([0]) : void 0).then((m2) => ({ default: m2.EditorApp })));
44588
+ const EmbedViewer = reactExports.lazy(() => __vitePreload(() => import("./EmbedViewer-DFDUhOma.js"), true ? [] : void 0).then((m2) => ({ default: m2.EmbedViewer })));
43774
44589
  const embedMode = isEmbedMode() && !window.location.pathname.startsWith("/m/");
43775
44590
  const EDITABLE_CRASH_FILE = /\.(?:forge\.js|[cm]?[jt]sx?|json|md|txt|svg|dxf)$/i;
43776
44591
  function firstMeaningfulLine(text) {