@pascal-app/nodes 1.0.0-beta.4 → 1.0.0-beta.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 (56) hide show
  1. package/dist/cabinet/flame-index.d.ts +3 -0
  2. package/dist/cabinet/flame-index.d.ts.map +1 -0
  3. package/dist/cabinet/flame-index.js +13 -0
  4. package/dist/cabinet/system.d.ts +2 -0
  5. package/dist/cabinet/system.d.ts.map +1 -1
  6. package/dist/cabinet/system.js +28 -8
  7. package/dist/cabinet/tool.d.ts.map +1 -1
  8. package/dist/cabinet/tool.js +2 -1
  9. package/dist/ceiling/panel.d.ts.map +1 -1
  10. package/dist/ceiling/panel.js +8 -2
  11. package/dist/ceiling/parametrics.js +1 -1
  12. package/dist/column/panel.js +1 -1
  13. package/dist/column/parametrics.js +1 -1
  14. package/dist/dormer/placement-guides.d.ts.map +1 -1
  15. package/dist/dormer/placement-guides.js +5 -4
  16. package/dist/elevator/definition.d.ts.map +1 -1
  17. package/dist/elevator/definition.js +2 -0
  18. package/dist/fence/curve-tool.d.ts.map +1 -1
  19. package/dist/fence/curve-tool.js +14 -9
  20. package/dist/fence/move-control-point-tool.d.ts.map +1 -1
  21. package/dist/fence/move-control-point-tool.js +13 -8
  22. package/dist/fence/move-tangent-tool.d.ts.map +1 -1
  23. package/dist/fence/move-tangent-tool.js +13 -8
  24. package/dist/fence/tool.d.ts.map +1 -1
  25. package/dist/fence/tool.js +8 -11
  26. package/dist/measurement/renderer.d.ts.map +1 -1
  27. package/dist/measurement/renderer.js +6 -5
  28. package/dist/measurement/surface-query.js +5 -5
  29. package/dist/measurement/tool.d.ts.map +1 -1
  30. package/dist/measurement/tool.js +7 -5
  31. package/dist/ridge-vent/renderer.d.ts.map +1 -1
  32. package/dist/ridge-vent/renderer.js +2 -2
  33. package/dist/shared/mep-ghost.d.ts.map +1 -1
  34. package/dist/shared/mep-ghost.js +19 -1
  35. package/dist/slab/panel.d.ts.map +1 -1
  36. package/dist/slab/panel.js +1 -1
  37. package/dist/wall/curve-tool.d.ts +3 -4
  38. package/dist/wall/curve-tool.d.ts.map +1 -1
  39. package/dist/wall/curve-tool.js +17 -13
  40. package/dist/wall/panel.d.ts.map +1 -1
  41. package/dist/wall/panel.js +56 -14
  42. package/dist/wall/parametrics.js +1 -1
  43. package/dist/wall/system.d.ts +3 -0
  44. package/dist/wall/system.d.ts.map +1 -1
  45. package/dist/wall/system.js +5 -1
  46. package/dist/wall/tool.d.ts.map +1 -1
  47. package/dist/wall/tool.js +24 -27
  48. package/dist/wall/wall-batch-system.d.ts +23 -0
  49. package/dist/wall/wall-batch-system.d.ts.map +1 -0
  50. package/dist/wall/wall-batch-system.js +389 -0
  51. package/dist/wall/wall-batch.d.ts +66 -0
  52. package/dist/wall/wall-batch.d.ts.map +1 -0
  53. package/dist/wall/wall-batch.js +169 -0
  54. package/dist/zone/quantities-panel.d.ts.map +1 -1
  55. package/dist/zone/quantities-panel.js +4 -3
  56. package/package.json +7 -7
@@ -0,0 +1,3 @@
1
+ import type { Object3D } from 'three';
2
+ export declare function collectCabinetFlameObjects(root: Object3D): Object3D[];
3
+ //# sourceMappingURL=flame-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flame-index.d.ts","sourceRoot":"","sources":["../../src/cabinet/flame-index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAUrC,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAMrE"}
@@ -0,0 +1,13 @@
1
+ function isFlameObject(object) {
2
+ return Boolean(object.userData.cabinetFlameJet ||
3
+ object.userData.cabinetFlamePulse ||
4
+ object.userData.cabinetFlameMaterialPulse);
5
+ }
6
+ export function collectCabinetFlameObjects(root) {
7
+ const objects = [];
8
+ root.traverse((object) => {
9
+ if (isFlameObject(object))
10
+ objects.push(object);
11
+ });
12
+ return objects;
13
+ }
@@ -1,4 +1,6 @@
1
1
  import { type SceneApi } from '@pascal-app/core';
2
+ import type { Object3D } from 'three';
3
+ export declare function animateCabinetFlames(objects: Object3D[], elapsedTime: number, updateTubes: boolean): void;
2
4
  /**
3
5
  * Poses door hinges / drawer slides stamped with `userData.cabinetPose`
4
6
  * directly, so `operationState` changes never trigger a geometry rebuild
@@ -1 +1 @@
1
- {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/cabinet/system.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,QAAQ,EAAiB,MAAM,kBAAkB,CAAA;AAmDjG;;;;;;GAMG;AACH,QAAA,MAAM,sBAAsB,GAAI,cAAc;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,SAkEnE,CAAA;AAED,eAAe,sBAAsB,CAAA"}
1
+ {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/cabinet/system.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,QAAQ,EAAiB,MAAM,kBAAkB,CAAA;AAGjG,OAAO,KAAK,EAAmC,QAAQ,EAAE,MAAM,OAAO,CAAA;AAgBtE,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,QAAQ,EAAE,EACnB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,OAAO,QAgCrB;AAED;;;;;;GAMG;AACH,QAAA,MAAM,sBAAsB,GAAI,cAAc;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,SAuFnE,CAAA;AAED,eAAe,sBAAsB,CAAA"}
@@ -5,22 +5,23 @@ import { useRef } from 'react';
5
5
  import { poseCabinetMovingParts } from './animation';
6
6
  import { updateCooktopFlameTube } from './cooktop-flame';
7
7
  import { bumpCabinetRunsNear, cabinetRunFootprint, cabinetRunNeighborSignature, } from './definition';
8
+ import { collectCabinetFlameObjects } from './flame-index';
8
9
  function materialWithOpacity(material) {
9
10
  if (!material)
10
11
  return null;
11
12
  return Array.isArray(material) ? (material[0] ?? null) : material;
12
13
  }
13
- function animateCabinetFlames(root, elapsedTime, updateTubes) {
14
- root.traverse((obj) => {
14
+ export function animateCabinetFlames(objects, elapsedTime, updateTubes) {
15
+ for (const obj of objects) {
15
16
  const jet = obj.userData.cabinetFlameJet;
16
17
  if (jet) {
17
18
  if (!updateTubes)
18
- return;
19
+ continue;
19
20
  const mesh = obj;
20
21
  const position = mesh.geometry.getAttribute('position');
21
22
  updateCooktopFlameTube(position.array, elapsedTime, jet.seed, jet.burnerR);
22
23
  position.needsUpdate = true;
23
- return;
24
+ continue;
24
25
  }
25
26
  const pulse = obj.userData.cabinetFlamePulse;
26
27
  if (pulse) {
@@ -28,14 +29,14 @@ function animateCabinetFlames(root, elapsedTime, updateTubes) {
28
29
  }
29
30
  const materialPulse = obj.userData.cabinetFlameMaterialPulse;
30
31
  if (!materialPulse)
31
- return;
32
+ continue;
32
33
  const material = materialWithOpacity(obj.material);
33
34
  if (!material || !('opacity' in material))
34
- return;
35
+ continue;
35
36
  material.opacity =
36
37
  materialPulse.base +
37
38
  materialPulse.amplitude * Math.sin(elapsedTime * 2.3 + materialPulse.phase);
38
- });
39
+ }
39
40
  }
40
41
  /**
41
42
  * Poses door hinges / drawer slides stamped with `userData.cabinetPose`
@@ -47,6 +48,7 @@ function animateCabinetFlames(root, elapsedTime, updateTubes) {
47
48
  const CabinetAnimationSystem = ({ sceneApi }) => {
48
49
  const appliedRef = useRef(new Map());
49
50
  const lastTubeUpdateRef = useRef(0);
51
+ const flameObjectsRef = useRef(new Map());
50
52
  // Last-seen neighbor-affecting signature per run. A run whose countertop
51
53
  // overhang trims against sibling runs never sees a neighbor's move in its
52
54
  // own geometryKey, so when a run's signature changes here we bump the
@@ -107,9 +109,27 @@ const CabinetAnimationSystem = ({ sceneApi }) => {
107
109
  poseCabinetMovingParts(obj, value);
108
110
  applied.set(id, value);
109
111
  }
110
- animateCabinetFlames(obj, clock.elapsedTime, updateTubes);
112
+ let flameEntry = flameObjectsRef.current.get(id);
113
+ const childrenChanged = !flameEntry ||
114
+ flameEntry.children.length !== obj.children.length ||
115
+ flameEntry.children.some((child, index) => child !== obj.children[index]);
116
+ if (flameEntry?.root !== obj || childrenChanged) {
117
+ flameEntry = {
118
+ root: obj,
119
+ children: [...obj.children],
120
+ objects: collectCabinetFlameObjects(obj),
121
+ };
122
+ flameObjectsRef.current.set(id, flameEntry);
123
+ }
124
+ if (flameEntry.objects.length > 0) {
125
+ animateCabinetFlames(flameEntry.objects, clock.elapsedTime, updateTubes);
126
+ }
111
127
  }
112
128
  }
129
+ for (const id of flameObjectsRef.current.keys()) {
130
+ if (!sceneRegistry.nodes.has(id))
131
+ flameObjectsRef.current.delete(id);
132
+ }
113
133
  }, 2);
114
134
  return null;
115
135
  };
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/cabinet/tool.tsx"],"names":[],"mappings":"AA+PA,QAAA,MAAM,WAAW,0CA29BhB,CAAA;AAED,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/cabinet/tool.tsx"],"names":[],"mappings":"AA+PA,QAAA,MAAM,WAAW,0CA49BhB,CAAA;AAED,eAAe,WAAW,CAAA"}
@@ -122,6 +122,7 @@ function wallHitFromWallEvent(event) {
122
122
  const CabinetTool = () => {
123
123
  const activeLevelId = useViewer((s) => s.selection.levelId);
124
124
  const unit = useViewer((s) => s.unit);
125
+ const metricNotation = useViewer((s) => s.metricNotation);
125
126
  const [placement, setPlacement] = useState(null);
126
127
  const [draftSegments, setDraftSegments] = useState([]);
127
128
  const [yaw, setYaw] = useState(0);
@@ -917,7 +918,7 @@ const CabinetTool = () => {
917
918
  visualPosition[1],
918
919
  placementBoxPlanPosition[2],
919
920
  ];
920
- return (_jsxs(LevelOffsetGroup, { children: [placement.guide && _jsx(WallSnapGuide, { blocked: !placement.valid, guide: placement.guide }), _jsx(PlacementBox, { dimensions: placementBoxDimensions, measurements: { unit }, position: placementBoxPosition, rotationY: placementRotationY, valid: placement.valid }), draftSegments.map((segment, segmentIndex) => (_jsx("group", { position: segment.anchor.position, rotation: [0, segment.anchor.yaw, 0], children: segment.stretch.modules.map((module, index) => (_jsx("group", { position: [module.x, 0, 0], scale: [module.width / previewNode.width, 1, 1], children: _jsx("primitive", { object: ghostForIndex(draftModuleOffsets[segmentIndex] + index) }) }, `${segmentIndex}-${index}`))) }, `draft-${segmentIndex}`))), _jsx("group", { ref: activeGhostRef, position: visualPosition, rotation: [0, placementRotationY, 0], children: stretch ? (stretch.modules.map((module, index) => (_jsx("group", { position: [module.x, 0, 0], scale: [module.width / previewNode.width, 1, 1], children: _jsx("primitive", { object: ghostForIndex(activeStretchGhostOffset + index) }) }, index)))) : (_jsx("primitive", { object: ghost })) }), placementLabel ? (_jsx(Html, { center: true, position: [
921
+ return (_jsxs(LevelOffsetGroup, { children: [placement.guide && _jsx(WallSnapGuide, { blocked: !placement.valid, guide: placement.guide }), _jsx(PlacementBox, { dimensions: placementBoxDimensions, measurements: { unit, metricNotation }, position: placementBoxPosition, rotationY: placementRotationY, valid: placement.valid }), draftSegments.map((segment, segmentIndex) => (_jsx("group", { position: segment.anchor.position, rotation: [0, segment.anchor.yaw, 0], children: segment.stretch.modules.map((module, index) => (_jsx("group", { position: [module.x, 0, 0], scale: [module.width / previewNode.width, 1, 1], children: _jsx("primitive", { object: ghostForIndex(draftModuleOffsets[segmentIndex] + index) }) }, `${segmentIndex}-${index}`))) }, `draft-${segmentIndex}`))), _jsx("group", { ref: activeGhostRef, position: visualPosition, rotation: [0, placementRotationY, 0], children: stretch ? (stretch.modules.map((module, index) => (_jsx("group", { position: [module.x, 0, 0], scale: [module.width / previewNode.width, 1, 1], children: _jsx("primitive", { object: ghostForIndex(activeStretchGhostOffset + index) }) }, index)))) : (_jsx("primitive", { object: ghost })) }), placementLabel ? (_jsx(Html, { center: true, position: [
921
922
  labelPosition[0],
922
923
  previewNode.plinthHeight + previewNode.carcassHeight + 0.35,
923
924
  labelPosition[2],
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/ceiling/panel.tsx"],"names":[],"mappings":"AA2BA;;;;;;;GAOG;AACH,wBAAgB,YAAY,uCA0V3B;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/ceiling/panel.tsx"],"names":[],"mappings":"AA2BA;;;;;;;GAOG;AACH,wBAAgB,YAAY,uCA6W3B;AAED,eAAe,YAAY,CAAA"}
@@ -16,6 +16,7 @@ import { useCallback, useEffect, useRef } from 'react';
16
16
  export function CeilingPanel() {
17
17
  const selectedId = useViewer((s) => s.selection.selectedIds[0]);
18
18
  const unit = useViewer((s) => s.unit);
19
+ const metricNotation = useViewer((s) => s.metricNotation);
19
20
  const setSelection = useViewer((s) => s.setSelection);
20
21
  const editingHole = useEditingHole();
21
22
  const setMovingNode = useEditor((s) => s.setMovingNode);
@@ -30,7 +31,7 @@ export function CeilingPanel() {
30
31
  const parent = node?.parentId ? s.nodes[node.parentId] : undefined;
31
32
  return parent?.type === 'level'
32
33
  ? getCeilingClampBound(parent.id, s.nodes, node?.polygon ?? [])
33
- : 6;
34
+ : Number.POSITIVE_INFINITY;
34
35
  });
35
36
  // Effective height: the stored custom height, or — for follows-mode
36
37
  // ceilings (absent `height`) — the live level-top bound. Primitive
@@ -184,7 +185,12 @@ export function CeilingPanel() {
184
185
  return (_jsxs(PanelWrapper, { icon: "/icons/ceiling.webp", onClose: handleClose, title: node.name || 'Ceiling', width: 320, children: [_jsxs(PanelSection, { title: "Height", children: [_jsx(SegmentedControl, { onChange: handleTopModeChange, options: [
185
186
  { label: 'Follows level', value: 'storey' },
186
187
  { label: 'Custom height', value: 'custom' },
187
- ], value: isFollows ? 'storey' : 'custom' }), isFollows ? (_jsxs("div", { className: "px-1 text-[11px] text-muted-foreground", children: ["Currently ", formatLinearMeasurement(resolvedHeight, unit)] })) : (_jsx(SliderControl, { label: "Height", max: Math.min(6, maxHeight), min: 0, onChange: handleHeightChange, precision: 3, step: 0.01, unit: "m", value: Math.round((node.height ?? resolvedHeight) * 1000) / 1000 })), _jsx("div", { className: "mt-2 grid grid-cols-3 gap-1.5 px-1 pb-1", children: heightPresets.map((preset) => (_jsx(ActionButton, { label: preset.label, onClick: () => handleHeightChange(preset.height) }, preset.label))) })] }), _jsx(PanelSection, { title: "Info", children: _jsxs("div", { className: "flex items-center justify-between px-2 py-1 text-muted-foreground text-sm", children: [_jsx("span", { children: "Area" }), _jsxs("span", { className: "font-mono text-white", children: [area.toFixed(2), " m\u00B2"] })] }) }), _jsxs(PanelSection, { title: "Holes", children: [node.holes && node.holes.length > 0 ? (_jsx("div", { className: "flex flex-col gap-1 pb-2", children: node.holes.map((hole, index) => {
188
+ ], value: isFollows ? 'storey' : 'custom' }), isFollows ? (_jsxs("div", { className: "px-1 text-[11px] text-muted-foreground", children: ["Currently ", formatLinearMeasurement(resolvedHeight, unit, metricNotation)] })) : (_jsx(SliderControl, { label: "Height", max: Math.min(20, maxHeight), min: 0, onChange: handleHeightChange, precision: 3, step: 0.01, unit: "m", value: Math.round((node.height ?? resolvedHeight) * 1000) / 1000 })), _jsx("div", { className: "mt-2 grid grid-cols-3 gap-1.5 px-1 pb-1", children: heightPresets.map((preset) => {
189
+ const fits = preset.height <= maxHeight;
190
+ return (_jsx(ActionButton, { className: fits ? undefined : 'cursor-not-allowed opacity-40', disabled: !fits, label: preset.label, onClick: () => handleHeightChange(preset.height), title: fits
191
+ ? undefined
192
+ : `Taller than this level (${formatLinearMeasurement(maxHeight, unit, metricNotation)} available). Raise the level height first.` }, preset.label));
193
+ }) }), Number.isFinite(maxHeight) && (_jsxs("div", { className: "px-1 pb-1 text-[11px] text-muted-foreground", children: ["Limited by the level to ", formatLinearMeasurement(maxHeight, unit, metricNotation), " \u2014 raise the level height for a taller ceiling."] }))] }), _jsx(PanelSection, { title: "Info", children: _jsxs("div", { className: "flex items-center justify-between px-2 py-1 text-muted-foreground text-sm", children: [_jsx("span", { children: "Area" }), _jsxs("span", { className: "font-mono text-white", children: [area.toFixed(2), " m\u00B2"] })] }) }), _jsxs(PanelSection, { title: "Holes", children: [node.holes && node.holes.length > 0 ? (_jsx("div", { className: "flex flex-col gap-1 pb-2", children: node.holes.map((hole, index) => {
188
194
  const holeArea = calculateArea(hole);
189
195
  const isEditing = editingHole?.nodeId === selectedId && editingHole?.holeIndex === index;
190
196
  const source = node.holeMetadata?.[index]?.source ?? 'manual';
@@ -9,7 +9,7 @@ export const ceilingParametrics = {
9
9
  groups: [
10
10
  {
11
11
  label: 'Dimensions',
12
- fields: [{ key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 6, step: 0.05 }],
12
+ fields: [{ key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 20, step: 0.05 }],
13
13
  },
14
14
  ],
15
15
  customPanel: () => import('./panel'),
@@ -313,7 +313,7 @@ export default function ColumnPanel() {
313
313
  if (!event.target.value)
314
314
  return;
315
315
  handleUpdate(proportionUpdates(node, event.target.value));
316
- }, value: "", children: [_jsx("option", { value: "", children: "Apply proportion..." }), COLUMN_PROPORTION_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })), _jsx(SliderControl, { label: "Height", max: 6, min: 0.8, onChange: (value) => handleUpdate({ height: value }), precision: 2, step: 0.05, unit: "m", value: node.height }), isBraceSupport ? (_jsxs(_Fragment, { children: [(supportStyle === 'a-frame' ||
316
+ }, value: "", children: [_jsx("option", { value: "", children: "Apply proportion..." }), COLUMN_PROPORTION_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })), _jsx(SliderControl, { label: "Height", max: 20, min: 0.8, onChange: (value) => handleUpdate({ height: value }), precision: 2, step: 0.05, unit: "m", value: node.height }), isBraceSupport ? (_jsxs(_Fragment, { children: [(supportStyle === 'a-frame' ||
317
317
  supportStyle === 'x-brace' ||
318
318
  supportStyle === 'k-brace' ||
319
319
  supportStyle === 'single-strut' ||
@@ -11,7 +11,7 @@ export const columnParametrics = {
11
11
  {
12
12
  label: 'Dimensions',
13
13
  fields: [
14
- { key: 'height', kind: 'number', unit: 'm', min: 0.5, max: 6, step: 0.05 },
14
+ { key: 'height', kind: 'number', unit: 'm', min: 0.5, max: 20, step: 0.05 },
15
15
  { key: 'width', kind: 'number', unit: 'm', min: 0.1, max: 2, step: 0.01 },
16
16
  { key: 'depth', kind: 'number', unit: 'm', min: 0.1, max: 2, step: 0.01 },
17
17
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"placement-guides.d.ts","sourceRoot":"","sources":["../../src/dormer/placement-guides.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AA0CvD,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAgBpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,EACP,MAAM,EACN,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,eAAe,CAAA;IACxB,MAAM,EAAE,IAAI,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,+BAiGA"}
1
+ {"version":3,"file":"placement-guides.d.ts","sourceRoot":"","sources":["../../src/dormer/placement-guides.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AA0CvD,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAgBpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,EACP,MAAM,EACN,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,eAAe,CAAA;IACxB,MAAM,EAAE,IAAI,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,+BAkGA"}
@@ -52,6 +52,7 @@ const alignMaterial = new LineBasicNodeMaterial({
52
52
  */
53
53
  export function DormerPlacementGuides({ segment, center, width, depth, rotation, movingId, }) {
54
54
  const unit = useViewer((s) => s.unit);
55
+ const metricNotation = useViewer((s) => s.metricNotation);
55
56
  const [cx, , cz] = center;
56
57
  const faceBounds = getRoofSurfaceFaceBoundsAt(segment, cx, cz);
57
58
  const halfW = Math.max(0, width) / 2;
@@ -139,13 +140,13 @@ export function DormerPlacementGuides({ segment, center, width, depth, rotation,
139
140
  }
140
141
  }
141
142
  guides.push(...siblingSpacing.guides);
142
- return (_jsx(_Fragment, { children: guides.map((g) => (_jsx(Guide, { guide: g, unit: unit }, g.id))) }));
143
+ return (_jsx(_Fragment, { children: guides.map((g) => (_jsx(Guide, { guide: g, metricNotation: metricNotation, unit: unit }, g.id))) }));
143
144
  }
144
- function Guide({ guide, unit }) {
145
+ function Guide({ guide, metricNotation, unit, }) {
145
146
  if (guide.kind === 'badge') {
146
- return _jsx(GuideBadge, { at: guide.at, pill: `= ${formatMeasurement(guide.value, unit)}` });
147
+ return (_jsx(GuideBadge, { at: guide.at, pill: `= ${formatMeasurement(guide.value, unit, metricNotation)}` }));
147
148
  }
148
- return (_jsx(GuideLine, { from: guide.from, kind: guide.kind, pill: guide.value === undefined ? undefined : formatMeasurement(guide.value, unit), to: guide.to }));
149
+ return (_jsx(GuideLine, { from: guide.from, kind: guide.kind, pill: guide.value === undefined ? undefined : formatMeasurement(guide.value, unit, metricNotation), to: guide.to }));
149
150
  }
150
151
  function GuideBadge({ at, pill }) {
151
152
  return (_jsx(Html, { center: true, position: at, style: { pointerEvents: 'none', userSelect: 'none' }, zIndexRange: [20, 0], children: _jsx("div", { className: "whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-semibold font-sans text-[11px] text-white", style: { backgroundColor: BADGE_BG }, children: pill }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/elevator/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAsJvC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,YAAY,CA2GlE,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/elevator/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAsJvC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,YAAY,CA8GlE,CAAA"}
@@ -218,6 +218,8 @@ export const elevatorDefinition = {
218
218
  },
219
219
  parametrics: elevatorParametrics,
220
220
  handles: elevatorHandles,
221
+ // No dirty consumer rebuilds this kind — see NodeDefinition.dirtyTracking.
222
+ dirtyTracking: false,
221
223
  renderer: {
222
224
  kind: 'parametric',
223
225
  module: () => import('./renderer'),
@@ -1 +1 @@
1
- {"version":3,"file":"curve-tool.d.ts","sourceRoot":"","sources":["../../src/fence/curve-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,SAAS,EAUf,MAAM,kBAAkB,CAAA;AAYzB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAsIxD,CAAA;AAED,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"curve-tool.d.ts","sourceRoot":"","sources":["../../src/fence/curve-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,SAAS,EAQf,MAAM,kBAAkB,CAAA;AAYzB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAyIxD,CAAA;AAED,eAAe,cAAc,CAAA"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { emitter, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallChordFrame, getWallMidpointHandlePoint, normalizeWallCurveOffset, pauseSceneHistory, resumeSceneHistory, useScene, } from '@pascal-app/core';
3
+ import { acquireSceneHistoryPause, emitter, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallChordFrame, getWallMidpointHandlePoint, normalizeWallCurveOffset, useScene, } from '@pascal-app/core';
4
4
  import { CursorSphere, getSegmentGridStep, markToolCancelConsumed, snapScalarToGrid, triggerSFX, useInteractionScope, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { useCallback, useEffect, useRef, useState } from 'react';
@@ -36,8 +36,8 @@ export const CurveFenceTool = ({ node }) => {
36
36
  const originalCurveOffset = originalCurveOffsetRef.current;
37
37
  const chord = getWallChordFrame(node);
38
38
  const maxCurveOffset = getMaxWallCurveOffset(node);
39
- pauseSceneHistory(useScene);
40
- let wasCommitted = false;
39
+ let releaseHistory = acquireSceneHistoryPause(useScene);
40
+ let wasFinalized = false;
41
41
  const applyPreview = (curveOffset) => {
42
42
  if (previewOffsetRef.current === curveOffset) {
43
43
  return;
@@ -76,21 +76,23 @@ export const CurveFenceTool = ({ node }) => {
76
76
  applyPreview(nextCurveOffset);
77
77
  };
78
78
  const onGridClick = (event) => {
79
+ if (wasFinalized)
80
+ return;
79
81
  if (Date.now() - activatedAtRef.current < 150) {
80
82
  event.nativeEvent?.stopPropagation?.();
81
83
  return;
82
84
  }
83
85
  const curveOffset = previewOffsetRef.current;
84
- wasCommitted = true;
86
+ wasFinalized = true;
85
87
  if (curveOffset !== originalCurveOffset) {
86
88
  // Restore original baseline while paused so the next resume+update
87
89
  // registers as a single tracked change (undo reverts to original).
88
90
  useScene.getState().updateNode(nodeId, { curveOffset: originalCurveOffset });
89
91
  useScene.getState().markDirty(nodeId);
90
- resumeSceneHistory(useScene);
92
+ releaseHistory();
91
93
  useScene.getState().updateNode(nodeId, { curveOffset });
92
94
  useScene.getState().markDirty(nodeId);
93
- pauseSceneHistory(useScene);
95
+ releaseHistory = acquireSceneHistoryPause(useScene);
94
96
  }
95
97
  triggerSFX('sfx:item-place');
96
98
  useViewer.getState().setSelection({ selectedIds: [nodeId] });
@@ -98,9 +100,12 @@ export const CurveFenceTool = ({ node }) => {
98
100
  event.nativeEvent?.stopPropagation?.();
99
101
  };
100
102
  const onCancel = () => {
103
+ if (wasFinalized)
104
+ return;
101
105
  restoreOriginal();
106
+ wasFinalized = true;
102
107
  useViewer.getState().setSelection({ selectedIds: [nodeId] });
103
- resumeSceneHistory(useScene);
108
+ releaseHistory();
104
109
  markToolCancelConsumed();
105
110
  exitCurveMode();
106
111
  };
@@ -108,10 +113,10 @@ export const CurveFenceTool = ({ node }) => {
108
113
  emitter.on('grid:click', onGridClick);
109
114
  emitter.on('tool:cancel', onCancel);
110
115
  return () => {
111
- if (!wasCommitted) {
116
+ if (!wasFinalized) {
112
117
  restoreOriginal();
113
118
  }
114
- resumeSceneHistory(useScene);
119
+ releaseHistory();
115
120
  emitter.off('grid:move', onGridMove);
116
121
  emitter.off('grid:click', onGridClick);
117
122
  emitter.off('tool:cancel', onCancel);
@@ -1 +1 @@
1
- {"version":3,"file":"move-control-point-tool.d.ts","sourceRoot":"","sources":["../../src/fence/move-control-point-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,SAAS,EAMf,MAAM,kBAAkB,CAAA;AAazB,eAAO,MAAM,yBAAyB,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/C,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5C,CAoGA,CAAA;AAED,eAAe,yBAAyB,CAAA"}
1
+ {"version":3,"file":"move-control-point-tool.d.ts","sourceRoot":"","sources":["../../src/fence/move-control-point-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,SAAS,EAIf,MAAM,kBAAkB,CAAA;AAazB,eAAO,MAAM,yBAAyB,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/C,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5C,CAuGA,CAAA;AAED,eAAe,yBAAyB,CAAA"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { emitter, pauseSceneHistory, resumeSceneHistory, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
3
+ import { acquireSceneHistoryPause, emitter, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
4
4
  import { CursorSphere, getSegmentGridStep, isGridSnapActive, markToolCancelConsumed, snapScalarToGrid, triggerSFX, useInteractionScope, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { useEffect, useState } from 'react';
@@ -15,8 +15,8 @@ export const MoveFenceControlPointTool = ({ target }) => {
15
15
  originalPoint[1],
16
16
  ]);
17
17
  useEffect(() => {
18
- pauseSceneHistory(useScene);
19
- let committed = false;
18
+ const releaseHistory = acquireSceneHistoryPause(useScene);
19
+ let finalized = false;
20
20
  let lastPoint = [originalPoint[0], originalPoint[1]];
21
21
  const buildPatch = (point) => {
22
22
  const nextPath = originalPath.map((pathPoint, pathIndex) => pathIndex === index ? point : pathPoint);
@@ -59,8 +59,10 @@ export const MoveFenceControlPointTool = ({ target }) => {
59
59
  }
60
60
  };
61
61
  const onGridClick = (event) => {
62
- committed = true;
63
- resumeSceneHistory(useScene);
62
+ if (finalized)
63
+ return;
64
+ finalized = true;
65
+ releaseHistory();
64
66
  useScene.getState().updateNode(fenceId, buildPatch(lastPoint));
65
67
  useLiveNodeOverrides.getState().clear(fenceId);
66
68
  useScene.getState().markDirty(fenceId);
@@ -68,8 +70,11 @@ export const MoveFenceControlPointTool = ({ target }) => {
68
70
  event.nativeEvent?.stopPropagation?.();
69
71
  };
70
72
  const onCancel = () => {
73
+ if (finalized)
74
+ return;
71
75
  restore();
72
- resumeSceneHistory(useScene);
76
+ finalized = true;
77
+ releaseHistory();
73
78
  markToolCancelConsumed();
74
79
  exit(false);
75
80
  };
@@ -77,9 +82,9 @@ export const MoveFenceControlPointTool = ({ target }) => {
77
82
  emitter.on('grid:click', onGridClick);
78
83
  emitter.on('tool:cancel', onCancel);
79
84
  return () => {
80
- if (!committed) {
85
+ if (!finalized) {
81
86
  restore();
82
- resumeSceneHistory(useScene);
87
+ releaseHistory();
83
88
  }
84
89
  emitter.off('grid:move', onGridMove);
85
90
  emitter.off('grid:click', onGridClick);
@@ -1 +1 @@
1
- {"version":3,"file":"move-tangent-tool.d.ts","sourceRoot":"","sources":["../../src/fence/move-tangent-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,SAAS,EAMf,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAE,CAAA;CAChE,CAqGA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"move-tangent-tool.d.ts","sourceRoot":"","sources":["../../src/fence/move-tangent-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,SAAS,EAIf,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAE,CAAA;CAChE,CAwGA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { emitter, pauseSceneHistory, resumeSceneHistory, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
3
+ import { acquireSceneHistoryPause, emitter, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
4
4
  import { CursorSphere, getSegmentGridStep, isGridSnapActive, markToolCancelConsumed, snapScalarToGrid, triggerSFX, useInteractionScope, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { useEffect, useState } from 'react';
@@ -11,8 +11,8 @@ export const MoveFenceTangentTool = ({ target }) => {
11
11
  const anchor = target.fence.path?.[index] ?? target.fence.start;
12
12
  const [cursor, setCursor] = useState([anchor[0], 0, anchor[1]]);
13
13
  useEffect(() => {
14
- pauseSceneHistory(useScene);
15
- let committed = false;
14
+ const releaseHistory = acquireSceneHistoryPause(useScene);
15
+ let finalized = false;
16
16
  const originalTangents = (target.fence.tangents ?? []).map((t) => (t ? [t[0], t[1]] : null));
17
17
  let lastTangents = originalTangents;
18
18
  const writeTangent = (vector) => {
@@ -54,9 +54,11 @@ export const MoveFenceTangentTool = ({ target }) => {
54
54
  writeTangent([armX / TANGENT_HANDLE_ARM_SCALE, armZ / TANGENT_HANDLE_ARM_SCALE]);
55
55
  };
56
56
  const onGridClick = (event) => {
57
- committed = true;
57
+ if (finalized)
58
+ return;
59
+ finalized = true;
58
60
  const finalTangents = lastTangents;
59
- resumeSceneHistory(useScene);
61
+ releaseHistory();
60
62
  useScene.getState().updateNode(fenceId, { tangents: finalTangents });
61
63
  useLiveNodeOverrides.getState().clear(fenceId);
62
64
  useScene.getState().markDirty(fenceId);
@@ -65,8 +67,11 @@ export const MoveFenceTangentTool = ({ target }) => {
65
67
  event.nativeEvent?.stopPropagation?.();
66
68
  };
67
69
  const onCancel = () => {
70
+ if (finalized)
71
+ return;
68
72
  restore();
69
- resumeSceneHistory(useScene);
73
+ finalized = true;
74
+ releaseHistory();
70
75
  markToolCancelConsumed();
71
76
  exit(false);
72
77
  };
@@ -74,9 +79,9 @@ export const MoveFenceTangentTool = ({ target }) => {
74
79
  emitter.on('grid:click', onGridClick);
75
80
  emitter.on('tool:cancel', onCancel);
76
81
  return () => {
77
- if (!committed) {
82
+ if (!finalized) {
78
83
  restore();
79
- resumeSceneHistory(useScene);
84
+ releaseHistory();
80
85
  }
81
86
  emitter.off('grid:move', onGridMove);
82
87
  emitter.off('grid:click', onGridClick);
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/fence/tool.tsx"],"names":[],"mappings":"AAycA,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAM7B,CAAA;AAweD,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/fence/tool.tsx"],"names":[],"mappings":"AA6bA,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAM7B,CAAA;AA0eD,eAAe,SAAS,CAAA"}
@@ -5,7 +5,7 @@ import { CursorSphere, clearPlacementSurface, createFenceOnCurrentLevel, createS
5
5
  import { getSceneTheme, useViewer } from '@pascal-app/viewer';
6
6
  import { useThree } from '@react-three/fiber';
7
7
  import { useEffect, useMemo, useRef, useState } from 'react';
8
- import { BoxGeometry, BufferGeometry, DoubleSide, Vector3, } from 'three';
8
+ import { BufferGeometry, DoubleSide, Vector3 } from 'three';
9
9
  import { DraftAngleArc, DraftAxisGuides, DraftMeasurementLabel, getNearestAxisAngleLabel, } from '../shared/draft-axis-guides';
10
10
  const FENCE_PREVIEW_HEIGHT = 1.8;
11
11
  const FENCE_PREVIEW_THICKNESS = 0.08;
@@ -216,14 +216,14 @@ function getDraftAngleLabels(start, end, segments, baseY, previewHeight, preview
216
216
  }
217
217
  return labels;
218
218
  }
219
- function getDraftMeasurementState(start, end, segments, unit, baseY, previewHeight, previewThickness) {
219
+ function getDraftMeasurementState(start, end, segments, unit, metricNotation, baseY, previewHeight, previewThickness) {
220
220
  const dx = end[0] - start[0];
221
221
  const dz = end[1] - start[1];
222
222
  const length = Math.hypot(dx, dz);
223
223
  if (length < 0.01)
224
224
  return null;
225
225
  return {
226
- lengthLabel: formatLinearMeasurement(length, unit),
226
+ lengthLabel: formatLinearMeasurement(length, unit, metricNotation),
227
227
  lengthPosition: [
228
228
  (start[0] + end[0]) / 2,
229
229
  baseY + previewHeight + DRAFT_LABEL_Y_OFFSET,
@@ -259,14 +259,10 @@ function updateFencePreview(mesh, start, end, previewHeight, previewThickness) {
259
259
  }
260
260
  mesh.visible = true;
261
261
  direction.normalize();
262
- const geometry = new BoxGeometry(length, previewHeight, previewThickness);
263
262
  const angle = Math.atan2(direction.z, direction.x);
264
263
  mesh.position.set((start.x + end.x) / 2, start.y + previewHeight / 2, (start.z + end.z) / 2);
265
264
  mesh.rotation.y = -angle;
266
- if (mesh.geometry) {
267
- mesh.geometry.dispose();
268
- }
269
- mesh.geometry = geometry;
265
+ mesh.scale.set(length, previewHeight, previewThickness);
270
266
  }
271
267
  function getCurrentLevelElements() {
272
268
  const currentLevelId = useViewer.getState().selection.levelId;
@@ -291,6 +287,7 @@ export const FenceTool = () => {
291
287
  };
292
288
  const StraightFenceTool = () => {
293
289
  const unit = useViewer((state) => state.unit);
290
+ const metricNotation = useViewer((state) => state.metricNotation);
294
291
  const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
295
292
  // A placed preset seeds `toolDefaults.fence` before the tool mounts, so
296
293
  // the draft preview is drawn at the preset's height / thickness rather
@@ -406,7 +403,7 @@ const StraightFenceTool = () => {
406
403
  }
407
404
  previousFenceEnd = currentFenceEnd;
408
405
  updateFencePreview(previewRef.current, startingPoint.current, endingPoint.current, previewHeightRef.current, previewThicknessRef.current);
409
- setDraftMeasurement(getDraftMeasurementState([startingPoint.current.x, startingPoint.current.z], snappedLocal, getReferenceSegments(walls, fences), unit, startingPoint.current.y, previewHeightRef.current, previewThicknessRef.current));
406
+ setDraftMeasurement(getDraftMeasurementState([startingPoint.current.x, startingPoint.current.z], snappedLocal, getReferenceSegments(walls, fences), unit, metricNotation, startingPoint.current.y, previewHeightRef.current, previewThicknessRef.current));
410
407
  }
411
408
  else {
412
409
  const snappedPoint = alignPoint(snapFenceDraftPoint({
@@ -523,8 +520,8 @@ const StraightFenceTool = () => {
523
520
  draftPreview.setFenceDraftStart(null);
524
521
  draftPreview.setFenceDraftEnd(null);
525
522
  };
526
- }, [unit]);
527
- return (_jsxs("group", { children: [_jsx(DraftAxisGuides, { guide: axisGuide, labelColor: measurementColor, labelShadowColor: measurementShadowColor }), _jsx(CursorSphere, { height: previewHeight, ref: cursorRef }), _jsxs("mesh", { layers: EDITOR_LAYER, ref: previewRef, renderOrder: 1, visible: false, children: [_jsx("shapeGeometry", {}), _jsx("meshBasicMaterial", { color: "#ffffff", depthTest: false, depthWrite: false, opacity: 0.45, side: DoubleSide, transparent: true })] }), draftMeasurement && (_jsxs(_Fragment, { children: [_jsx(DraftMeasurementLabel, { color: measurementColor, label: draftMeasurement.lengthLabel, position: draftMeasurement.lengthPosition, shadowColor: measurementShadowColor }), draftMeasurement.angleLabels.map((angleLabel) => (_jsxs("group", { children: [_jsx(DraftAngleArc, { arc: angleLabel.arc, color: measurementColor }), _jsx(DraftMeasurementLabel, { color: measurementColor, label: angleLabel.label, position: angleLabel.position, shadowColor: measurementShadowColor })] }, angleLabel.id)))] }))] }));
523
+ }, [unit, metricNotation]);
524
+ return (_jsxs("group", { children: [_jsx(DraftAxisGuides, { guide: axisGuide, labelColor: measurementColor, labelShadowColor: measurementShadowColor }), _jsx(CursorSphere, { height: previewHeight, ref: cursorRef }), _jsxs("mesh", { layers: EDITOR_LAYER, ref: previewRef, renderOrder: 1, visible: false, children: [_jsx("boxGeometry", {}), _jsx("meshBasicMaterial", { color: "#ffffff", depthTest: false, depthWrite: false, opacity: 0.45, side: DoubleSide, transparent: true })] }), draftMeasurement && (_jsxs(_Fragment, { children: [_jsx(DraftMeasurementLabel, { color: measurementColor, label: draftMeasurement.lengthLabel, position: draftMeasurement.lengthPosition, shadowColor: measurementShadowColor }), draftMeasurement.angleLabels.map((angleLabel) => (_jsxs("group", { children: [_jsx(DraftAngleArc, { arc: angleLabel.arc, color: measurementColor }), _jsx(DraftMeasurementLabel, { color: measurementColor, label: angleLabel.label, position: angleLabel.position, shadowColor: measurementShadowColor })] }, angleLabel.id)))] }))] }));
528
525
  };
529
526
  const SPLINE_PREVIEW_COLOR = '#8381ed';
530
527
  const SPLINE_PREVIEW_SEGMENTS = 40;
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/measurement/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EAWrB,MAAM,kBAAkB,CAAA;AAezB,OAAO,EAML,KAAK,QAAQ,EAKd,MAAM,OAAO,CAAA;AA4Qd,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAO/E;AAED,eAAO,MAAM,mBAAmB,GAAI,UAAU;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,gCAyItE,CAAA;AAED,eAAe,mBAAmB,CAAA"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/measurement/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EAWrB,MAAM,kBAAkB,CAAA;AAezB,OAAO,EAML,KAAK,QAAQ,EAKd,MAAM,OAAO,CAAA;AAiRd,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAO/E;AAED,eAAO,MAAM,mBAAmB,GAAI,UAAU;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,gCA0ItE,CAAA;AAED,eAAe,mBAAmB,CAAA"}
@@ -197,9 +197,9 @@ function buildRenderData(measurement) {
197
197
  markerPoints,
198
198
  };
199
199
  }
200
- function formatMeasurement(measurement, unit) {
200
+ function formatMeasurement(measurement, unit, metricNotation) {
201
201
  if (measurement.kind === 'distance') {
202
- return formatLinearMeasurement(measurementDistance(...measurement.points), unit);
202
+ return formatLinearMeasurement(measurementDistance(...measurement.points), unit, metricNotation);
203
203
  }
204
204
  if (measurement.kind === 'angle') {
205
205
  return formatAngleRadians(measurementAngle(...measurement.points));
@@ -208,7 +208,7 @@ function formatMeasurement(measurement, unit) {
208
208
  return `A ${formatAreaLabel(measurementArea(measurement.base), unit)}`;
209
209
  }
210
210
  if (measurement.kind === 'perimeter') {
211
- return `P ${formatLinearMeasurement(measurementPerimeter(measurement.base), unit)}`;
211
+ return `P ${formatLinearMeasurement(measurementPerimeter(measurement.base), unit, metricNotation)}`;
212
212
  }
213
213
  return `V ${formatVolumeLabel(measurementPrismVolume(measurement.base, measurement.extrusion), unit)}`;
214
214
  }
@@ -229,6 +229,7 @@ export const MeasurementRenderer = ({ node }) => {
229
229
  const handlers = useNodeEvents(node, 'measurement');
230
230
  const showMeasurements = useViewer((state) => state.showMeasurements);
231
231
  const unit = useViewer((state) => state.unit);
232
+ const metricNotation = useViewer((state) => state.metricNotation);
232
233
  const active = useViewer((state) => state.hoveredId === node.id || state.selection.selectedIds.some((id) => id === node.id));
233
234
  const ownOverride = useLiveNodeOverrides((state) => state.overrides.get(node.id));
234
235
  const effectiveNode = useMemo(() => (ownOverride ? { ...node, ...ownOverride } : node), [node, ownOverride]);
@@ -244,9 +245,9 @@ export const MeasurementRenderer = ({ node }) => {
244
245
  });
245
246
  const data = buildRenderData(resolved.payload);
246
247
  const label = useMemo(() => {
247
- const value = formatMeasurement(resolved.payload, unit);
248
+ const value = formatMeasurement(resolved.payload, unit, metricNotation);
248
249
  return resolved.dangling.length > 0 ? `Unlinked · ${value}` : value;
249
- }, [resolved, unit]);
250
+ }, [metricNotation, resolved, unit]);
250
251
  const color = measurementPresentationColor(resolved.dangling.length > 0, active);
251
252
  const lineMaterial = useMemo(() => new LineBasicNodeMaterial({
252
253
  color,
@@ -1,5 +1,5 @@
1
1
  import { measurementDistance, measurementFeatureLength, sceneRegistry, useScene, } from '@pascal-app/core';
2
- import { SCENE_LAYER, ZONE_LAYER } from '@pascal-app/viewer';
2
+ import { setSurfaceRaycastLayers, ZONE_LAYER } from '@pascal-app/viewer';
3
3
  import { Matrix3, Matrix4, Quaternion, Raycaster, Vector2, Vector3, } from 'three';
4
4
  import { matchMeasurementFeatureForNode } from './resolve';
5
5
  const AXIS_SNAP_DISTANCE_PX = 16;
@@ -405,7 +405,7 @@ function collectMeasurementAxisSurfaceIntersections(context, levelObject, anchor
405
405
  const origin = levelObject.localToWorld(new Vector3(...anchor));
406
406
  const levelRotation = levelObject.getWorldQuaternion(new Quaternion());
407
407
  const inverseLevelRotation = levelRotation.clone().invert();
408
- raycaster.layers.set(SCENE_LAYER);
408
+ setSurfaceRaycastLayers(raycaster.layers);
409
409
  raycaster.near = 0;
410
410
  raycaster.far = maxDistance;
411
411
  const intersections = [];
@@ -452,9 +452,9 @@ export function createMeasurementSurfaceQuerySession(scene, options = {}) {
452
452
  const verificationRaycaster = new Raycaster();
453
453
  const axisRaycaster = new Raycaster();
454
454
  const pointer = new Vector2();
455
- pointerRaycaster.layers.set(SCENE_LAYER);
456
- verificationRaycaster.layers.set(SCENE_LAYER);
457
- axisRaycaster.layers.set(SCENE_LAYER);
455
+ setSurfaceRaycastLayers(pointerRaycaster.layers);
456
+ setSurfaceRaycastLayers(verificationRaycaster.layers);
457
+ setSurfaceRaycastLayers(axisRaycaster.layers);
458
458
  if (options.includeZoneLayer)
459
459
  pointerRaycaster.layers.enable(ZONE_LAYER);
460
460
  let context = null;