@pascal-app/nodes 1.0.0-beta.3 → 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
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/measurement/tool.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/measurement/tool.tsx"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { collectAlignmentAnchors, emitter, measurementAngle, measurementArea, measurementCentroid, measurementDistance, measurementFeatureLength, measurementPerimeter, measurementPrismVolume, sceneRegistry, useScene, } from '@pascal-app/core';
4
4
  import { buildMeasurementAngleArcPoints, commitMeasurementDraft, EDITOR_LAYER, finishMeasurementDraft, formatAngleRadians, formatAreaLabel, formatLinearMeasurement, formatVolumeLabel, getLinearUnitLabel, handleMeasurementDraftEscape, isAlignmentGuideActive, isGridSnapActive, linearUnitToMeters, MEASUREMENT_ACTIVE_COLOR, markToolCancelConsumed, measurementPolygonLabelAnchor, measurementPolygonMidpoints, metersToLinearUnit, useEditor, useInteractionScope, useMeasurementDraft, } from '@pascal-app/editor';
5
- import { SCENE_LAYER, useViewer } from '@pascal-app/viewer';
5
+ import { setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer';
6
6
  import { Html } from '@react-three/drei';
7
7
  import { useFrame, useThree } from '@react-three/fiber';
8
8
  import { useEffect, useMemo, useRef, useState } from 'react';
@@ -247,7 +247,7 @@ export function collectMeasurementAxisSurfaceIntersections(scene, levelObject, a
247
247
  const origin = levelObject.localToWorld(new Vector3(...anchor));
248
248
  const levelRotation = levelObject.getWorldQuaternion(new Quaternion());
249
249
  const raycaster = new Raycaster();
250
- raycaster.layers.set(SCENE_LAYER);
250
+ setSurfaceRaycastLayers(raycaster.layers);
251
251
  raycaster.near = 0;
252
252
  raycaster.far = maxDistance;
253
253
  const intersections = [];
@@ -766,6 +766,7 @@ const MeasurementDraftPreview = ({ buildingId, levelId, surfaceQuery }) => {
766
766
  const extrusionHeight = useMeasurementDraft((state) => state.extrusionHeight);
767
767
  const error = useMeasurementDraft((state) => state.error);
768
768
  const unit = useViewer((state) => state.unit);
769
+ const metricNotation = useViewer((state) => state.metricNotation);
769
770
  const axisIntersectionCache = useRef(null);
770
771
  const draftPointMaterial = useMemo(() => new MeshBasicNodeMaterial({
771
772
  color: DRAFT_COLOR,
@@ -897,7 +898,7 @@ const MeasurementDraftPreview = ({ buildingId, levelId, surfaceQuery }) => {
897
898
  const end = livePoints[livePoints.length - 1];
898
899
  label = {
899
900
  position: localToPreviewFrame(levelObject, buildingObject, midpoint(start, end)),
900
- text: formatLinearMeasurement(measurementDistance(start, end), unit),
901
+ text: formatLinearMeasurement(measurementDistance(start, end), unit, metricNotation),
901
902
  };
902
903
  }
903
904
  else if (kind === 'angle' && livePoints.length >= 3) {
@@ -915,7 +916,7 @@ const MeasurementDraftPreview = ({ buildingId, levelId, surfaceQuery }) => {
915
916
  position: localToPreviewFrame(levelObject, buildingObject, center),
916
917
  text: kind === 'area'
917
918
  ? `A ${formatAreaLabel(measurementArea(livePoints), unit)}`
918
- : `P ${formatLinearMeasurement(measurementPerimeter(livePoints), unit)}`,
919
+ : `P ${formatLinearMeasurement(measurementPerimeter(livePoints), unit, metricNotation)}`,
919
920
  };
920
921
  }
921
922
  }
@@ -971,6 +972,7 @@ const MeasurementDraftPreview = ({ buildingId, levelId, surfaceQuery }) => {
971
972
  hoverOwner,
972
973
  kind,
973
974
  levelId,
975
+ metricNotation,
974
976
  points,
975
977
  stage,
976
978
  surfaceQuery,
@@ -1005,7 +1007,7 @@ export const MeasurementTool = () => {
1005
1007
  const cancelVertexGesture = useRef(() => { });
1006
1008
  const surfaceQuery = useMemo(() => createMeasurementSurfaceQuerySession(scene), [scene]);
1007
1009
  useEffect(() => {
1008
- raycaster.current.layers.set(SCENE_LAYER);
1010
+ setSurfaceRaycastLayers(raycaster.current.layers);
1009
1011
  }, []);
1010
1012
  useEffect(() => () => surfaceQuery.dispose(), [surfaceQuery]);
1011
1013
  useEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/ridge-vent/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,aAAa,EAMnB,MAAM,kBAAkB,CAAA;AAuDzB;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,iBAAiB,GAAI,qBAAqB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,uCAiMtE,CAAA;AAED,eAAe,iBAAiB,CAAA"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/ridge-vent/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,aAAa,EAMnB,MAAM,kBAAkB,CAAA;AAuDzB;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,iBAAiB,GAAI,qBAAqB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,uCAiMtE,CAAA;AAED,eAAe,iBAAiB,CAAA"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { getEffectiveRoofSurfaceMaterial, getEffectiveSegmentSurfaceMaterial, useLiveNodeOverrides, useRegistry, useScene, } from '@pascal-app/core';
3
+ import { getEffectiveRoofSurfaceMaterial, getEffectiveSegmentSurfaceMaterial, normalizeRoofSegmentTrim, useLiveNodeOverrides, useRegistry, useScene, } from '@pascal-app/core';
4
4
  import { createMaterial, createMaterialFromPresetRef, createSurfaceRoleMaterial, getRoofMaterialArray, useNodeEvents, useViewer, } from '@pascal-app/viewer';
5
5
  import { useEffect, useMemo, useRef } from 'react';
6
6
  import * as THREE from 'three';
@@ -11,7 +11,7 @@ import { buildRidgeVentGeometry } from './geometry';
11
11
  function ridgeVentSegmentGeometryKey(segment) {
12
12
  if (!segment)
13
13
  return 'none';
14
- const trim = segment.trim;
14
+ const trim = normalizeRoofSegmentTrim(segment);
15
15
  return [
16
16
  segment.roofType,
17
17
  segment.width,
@@ -1 +1 @@
1
- {"version":3,"file":"mep-ghost.d.ts","sourceRoot":"","sources":["../../src/shared/mep-ghost.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAUzB,gFAAgF;AAChF,eAAO,MAAM,WAAW,YAAY,CAAA;AACpC,eAAO,MAAM,aAAa,OAAO,CAAA;AAEjC;;;oCAGoC;AACpC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;AAyBvD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,+BAS7F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,+BAO3F;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,IAAI,GACL,EAAE;IACD,OAAO,EAAE,eAAe,CAAA;IACxB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,+BASA;AAED,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,+BAO3F"}
1
+ {"version":3,"file":"mep-ghost.d.ts","sourceRoot":"","sources":["../../src/shared/mep-ghost.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAWzB,gFAAgF;AAChF,eAAO,MAAM,WAAW,YAAY,CAAA;AACpC,eAAO,MAAM,aAAa,OAAO,CAAA;AAEjC;;;oCAGoC;AACpC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;AAmCvD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,+BAU7F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,+BAQ3F;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,IAAI,GACL,EAAE;IACD,OAAO,EAAE,eAAe,CAAA;IACxB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,+BAUA;AAED,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,+BAQ3F"}
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { EDITOR_LAYER } from '@pascal-app/editor';
4
- import { useMemo } from 'react';
4
+ import { disposeObject3DResources } from '@pascal-app/viewer';
5
+ import { useEffect, useMemo } from 'react';
5
6
  import { Mesh, MeshBasicMaterial } from 'three';
6
7
  import { buildDuctFittingGeometry } from '../duct-fitting/geometry';
7
8
  import { buildDuctSegmentGeometry } from '../duct-segment/geometry';
@@ -21,8 +22,17 @@ function ghostColor(tint) {
21
22
  /** Repaint every mesh in `group` as a translucent, depth-test-free preview. */
22
23
  function ghostify(group, tint) {
23
24
  const color = ghostColor(tint);
25
+ const replacedMaterials = new Set();
24
26
  group.traverse((child) => {
25
27
  if (child instanceof Mesh) {
28
+ const previous = child.material;
29
+ if (Array.isArray(previous)) {
30
+ for (const material of previous)
31
+ replacedMaterials.add(material);
32
+ }
33
+ else {
34
+ replacedMaterials.add(previous);
35
+ }
26
36
  child.layers.set(EDITOR_LAYER);
27
37
  child.material = new MeshBasicMaterial({
28
38
  color,
@@ -33,6 +43,10 @@ function ghostify(group, tint) {
33
43
  child.renderOrder = 999;
34
44
  }
35
45
  });
46
+ for (const material of replacedMaterials) {
47
+ if (!material.userData.__pascalCachedMaterial)
48
+ material.dispose();
49
+ }
36
50
  }
37
51
  /**
38
52
  * Translucent ghost of a duct fitting, built from the same geometry the
@@ -48,6 +62,7 @@ export function FittingGhost({ fitting, tint }) {
48
62
  ghostify(group, tint);
49
63
  return group;
50
64
  }, [fitting, tint]);
65
+ useEffect(() => () => disposeObject3DResources(ghost), [ghost]);
51
66
  return _jsx("primitive", { object: ghost });
52
67
  }
53
68
  /**
@@ -61,6 +76,7 @@ export function DuctSegmentGhost({ duct, tint }) {
61
76
  ghostify(group, tint);
62
77
  return group;
63
78
  }, [duct, tint]);
79
+ useEffect(() => () => disposeObject3DResources(ghost), [ghost]);
64
80
  return _jsx("primitive", { object: ghost });
65
81
  }
66
82
  export function PipeFittingGhost({ fitting, tint, }) {
@@ -71,6 +87,7 @@ export function PipeFittingGhost({ fitting, tint, }) {
71
87
  ghostify(group, tint);
72
88
  return group;
73
89
  }, [fitting, tint]);
90
+ useEffect(() => () => disposeObject3DResources(ghost), [ghost]);
74
91
  return _jsx("primitive", { object: ghost });
75
92
  }
76
93
  export function PipeSegmentGhost({ pipe, tint }) {
@@ -79,5 +96,6 @@ export function PipeSegmentGhost({ pipe, tint }) {
79
96
  ghostify(group, tint);
80
97
  return group;
81
98
  }, [pipe, tint]);
99
+ useEffect(() => () => disposeObject3DResources(ghost), [ghost]);
82
100
  return _jsx("primitive", { object: ghost });
83
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/slab/panel.tsx"],"names":[],"mappings":"AA+BA;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,uCA8ZxB;AAED,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/slab/panel.tsx"],"names":[],"mappings":"AA+BA;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,uCAgaxB;AAED,eAAe,SAAS,CAAA"}
@@ -203,7 +203,7 @@ export function SlabPanel() {
203
203
  { label: 'Standard (5cm)', elevation: 0.05 },
204
204
  { label: 'Thick (15cm)', elevation: 0.15 },
205
205
  ];
206
- return (_jsxs(PanelWrapper, { icon: "/icons/floor.webp", onClose: handleClose, title: node.name || 'Slab', width: 320, children: [_jsxs(PanelSection, { title: "Elevation", children: [_jsx(SliderControl, { label: node.recessed ? 'Floor' : 'Surface', max: 6, min: -3, onChange: handleElevationChange, precision: 3, step: 0.01, unit: "m", value: Math.round(node.elevation * 1000) / 1000 }), _jsx(SliderControl, { label: node.recessed ? 'Rim' : 'Base', max: 6, min: -3, onChange: handleAnchorChange, precision: 3, step: 0.01, unit: "m", value: Math.round(getSlabAnchorElevation(node) * 1000) / 1000 }), node.recessed ? (_jsx(SliderControl, { label: "Depth", max: 2, min: MIN_SLAB_THICKNESS, onChange: handleRecessDepthChange, precision: 2, step: 0.01, unit: "m", value: Math.round(getSlabRecessDepth(node) * 100) / 100 })) : (_jsx(SliderControl, { label: "Thickness", max: 0.5, min: MIN_SLAB_THICKNESS, onChange: handleThicknessChange, precision: 2, step: 0.01, unit: "m", value: Math.round((node.thickness ?? 0.05) * 100) / 100 })), !node.recessed && (_jsxs(_Fragment, { children: [_jsx("div", { className: "px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Foundation" }), _jsx(SegmentedControl, { onChange: handleTerrainModeChange, options: [
206
+ return (_jsxs(PanelWrapper, { icon: "/icons/floor.webp", onClose: handleClose, title: node.name || 'Slab', width: 320, children: [_jsxs(PanelSection, { title: "Elevation", children: [_jsx(SliderControl, { label: node.recessed ? 'Floor' : 'Surface', max: 20, min: -3, onChange: handleElevationChange, precision: 3, step: 0.01, unit: "m", value: Math.round(node.elevation * 1000) / 1000 }), _jsx(SliderControl, { label: node.recessed ? 'Rim' : 'Base', max: 20, min: -3, onChange: handleAnchorChange, precision: 3, step: 0.01, unit: "m", value: Math.round(getSlabAnchorElevation(node) * 1000) / 1000 }), node.recessed ? (_jsx(SliderControl, { label: "Depth", max: 2, min: MIN_SLAB_THICKNESS, onChange: handleRecessDepthChange, precision: 2, step: 0.01, unit: "m", value: Math.round(getSlabRecessDepth(node) * 100) / 100 })) : (_jsx(SliderControl, { label: "Thickness", max: 0.5, min: MIN_SLAB_THICKNESS, onChange: handleThicknessChange, precision: 2, step: 0.01, unit: "m", value: Math.round((node.thickness ?? 0.05) * 100) / 100 })), !node.recessed && (_jsxs(_Fragment, { children: [_jsx("div", { className: "px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Foundation" }), _jsx(SegmentedControl, { onChange: handleTerrainModeChange, options: [
207
207
  { label: 'Fixed', value: 'fixed' },
208
208
  { label: 'Follows terrain', value: 'terrain' },
209
209
  ], value: node.fillToTerrain ? 'terrain' : 'fixed' }), node.fillToTerrain && (_jsx("div", { className: "px-1 text-[11px] text-muted-foreground", children: "Extends the perimeter down to terrain. The flat surface, base, and thickness stay unchanged." }))] })), _jsx("div", { className: "mt-2 grid grid-cols-2 gap-1.5 px-1 pb-1", children: elevationPresets.map((preset) => (_jsx(ActionButton, { label: preset.label, onClick: () => handleElevationPreset(preset.elevation) }, 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) => {
@@ -2,10 +2,9 @@ import { type WallNode } from '@pascal-app/core';
2
2
  /**
3
3
  * Phase 5 Stage D — wall curve tool (kind-owned).
4
4
  *
5
- * 1:1 port of the legacy `CurveWallTool`. Same snap pipeline,
6
- * history dance, activation grace. The wall variant uses
7
- * `useScene.temporal.getState().pause()` / `.resume()` directly rather
8
- * than the depth-counted `pauseSceneHistory` helpers — matches legacy.
5
+ * 1:1 port of the legacy `CurveWallTool`. Same snap pipeline and
6
+ * activation grace. History uses an idempotent lease because cancel and
7
+ * effect cleanup can both release the active interaction.
9
8
  */
10
9
  export declare const CurveWallTool: React.FC<{
11
10
  node: WallNode;
@@ -1 +1 @@
1
- {"version":3,"file":"curve-tool.d.ts","sourceRoot":"","sources":["../../src/wall/curve-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAUL,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAazB;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CA6ItD,CAAA;AAED,eAAe,aAAa,CAAA"}
1
+ {"version":3,"file":"curve-tool.d.ts","sourceRoot":"","sources":["../../src/wall/curve-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAWL,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAazB;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAgJtD,CAAA;AAED,eAAe,aAAa,CAAA"}
@@ -1,16 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { emitter, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallChordFrame, getWallMidpointHandlePoint, normalizeWallCurveOffset, 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, snapBuildingLocalToWorldGrid, snapScalarToGrid, triggerSFX, useInteractionScope, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { useCallback, useEffect, useRef, useState } from 'react';
7
7
  /**
8
8
  * Phase 5 Stage D — wall curve tool (kind-owned).
9
9
  *
10
- * 1:1 port of the legacy `CurveWallTool`. Same snap pipeline,
11
- * history dance, activation grace. The wall variant uses
12
- * `useScene.temporal.getState().pause()` / `.resume()` directly rather
13
- * than the depth-counted `pauseSceneHistory` helpers — matches legacy.
10
+ * 1:1 port of the legacy `CurveWallTool`. Same snap pipeline and
11
+ * activation grace. History uses an idempotent lease because cancel and
12
+ * effect cleanup can both release the active interaction.
14
13
  */
15
14
  export const CurveWallTool = ({ node }) => {
16
15
  const activatedAtRef = useRef(Date.now());
@@ -33,8 +32,8 @@ export const CurveWallTool = ({ node }) => {
33
32
  const originalCurveOffset = originalCurveOffsetRef.current;
34
33
  const chord = getWallChordFrame(node);
35
34
  const maxCurveOffset = getMaxWallCurveOffset(node);
36
- useScene.temporal.getState().pause();
37
- let wasCommitted = false;
35
+ let releaseHistory = acquireSceneHistoryPause(useScene);
36
+ let wasFinalized = false;
38
37
  const applyPreview = (curveOffset) => {
39
38
  if (previewOffsetRef.current === curveOffset) {
40
39
  return;
@@ -77,21 +76,23 @@ export const CurveWallTool = ({ node }) => {
77
76
  applyPreview(nextCurveOffset);
78
77
  };
79
78
  const onGridClick = (event) => {
79
+ if (wasFinalized)
80
+ return;
80
81
  if (Date.now() - activatedAtRef.current < 150) {
81
82
  event.nativeEvent?.stopPropagation?.();
82
83
  return;
83
84
  }
84
85
  const curveOffset = previewOffsetRef.current;
85
- wasCommitted = true;
86
+ wasFinalized = true;
86
87
  if (curveOffset !== originalCurveOffset) {
87
88
  // Restore original baseline while paused so the next resume+update
88
89
  // registers as a single tracked change (undo reverts to original).
89
90
  useScene.getState().updateNode(nodeId, { curveOffset: originalCurveOffset });
90
91
  useScene.getState().markDirty(nodeId);
91
- useScene.temporal.getState().resume();
92
+ releaseHistory();
92
93
  useScene.getState().updateNode(nodeId, { curveOffset });
93
94
  useScene.getState().markDirty(nodeId);
94
- useScene.temporal.getState().pause();
95
+ releaseHistory = acquireSceneHistoryPause(useScene);
95
96
  }
96
97
  triggerSFX('sfx:item-place');
97
98
  useViewer.getState().setSelection({ selectedIds: [nodeId] });
@@ -99,9 +100,12 @@ export const CurveWallTool = ({ node }) => {
99
100
  event.nativeEvent?.stopPropagation?.();
100
101
  };
101
102
  const onCancel = () => {
103
+ if (wasFinalized)
104
+ return;
102
105
  restoreOriginal();
106
+ wasFinalized = true;
103
107
  useViewer.getState().setSelection({ selectedIds: [nodeId] });
104
- useScene.temporal.getState().resume();
108
+ releaseHistory();
105
109
  markToolCancelConsumed();
106
110
  exitCurveMode();
107
111
  };
@@ -109,10 +113,10 @@ export const CurveWallTool = ({ node }) => {
109
113
  emitter.on('grid:click', onGridClick);
110
114
  emitter.on('tool:cancel', onCancel);
111
115
  return () => {
112
- if (!wasCommitted) {
116
+ if (!wasFinalized) {
113
117
  restoreOriginal();
114
118
  }
115
- useScene.temporal.getState().resume();
119
+ releaseHistory();
116
120
  emitter.off('grid:move', onGridMove);
117
121
  emitter.off('grid:click', onGridClick);
118
122
  emitter.off('tool:cancel', onCancel);
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/wall/panel.tsx"],"names":[],"mappings":"AAoEA,MAAM,CAAC,OAAO,UAAU,SAAS,uCAwRhC"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/wall/panel.tsx"],"names":[],"mappings":"AAyFA,MAAM,CAAC,OAAO,UAAU,SAAS,uCAkUhC"}
@@ -1,11 +1,27 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { buildWallFaceBandCountPatch, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallCurveLength, getWallFaceBandConfig, normalizeWallCurveOffset, useLiveNodeOverrides, useScene, WALL_CHAIR_RAIL_DEFAULT, WALL_CROWN_DEFAULT, WALL_FACE_BAND_DEFAULT, WALL_SKIRTING_DEFAULT, } from '@pascal-app/core';
4
- import { ActionButton, ActionGroup, curveReshapeScope, getLinearUnitLabel, linearControlValueToMeters, metersToLinearUnit, PanelSection, PanelWrapper, SegmentedControl, SliderControl, triggerSFX, useInteractionScope, } from '@pascal-app/editor';
3
+ import { buildWallFaceBandCountPatch, GROUND_SUPPORT_ID, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallCurveLength, getWallFaceBandConfig, normalizeWallCurveOffset, terrainSupportLift, useLiveNodeOverrides, useScene, WALL_CHAIR_RAIL_DEFAULT, WALL_CROWN_DEFAULT, WALL_FACE_BAND_DEFAULT, WALL_SKIRTING_DEFAULT, } from '@pascal-app/core';
4
+ import { ActionButton, ActionGroup, curveReshapeScope, formatLinearMeasurement, getLinearUnitLabel, linearControlValueToMeters, metersToLinearUnit, PanelSection, PanelWrapper, SegmentedControl, SliderControl, triggerSFX, useInteractionScope, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { Spline } from 'lucide-react';
7
7
  import { useCallback, useMemo, useRef } from 'react';
8
8
  import { resolveWallOpeningCeiling } from '../shared/wall-opening-ceiling';
9
+ /**
10
+ * Base half of the plane-bound repair: a stamped draft offset goes, and a
11
+ * ground host is dropped unless sculpted terrain actually supports it — a
12
+ * terrain-less ground host (regression-era data) pins the base at the level
13
+ * floor and buries the wall in any later slab.
14
+ */
15
+ function wallBaseRepairPatch(n) {
16
+ const nodes = useScene.getState().nodes;
17
+ const terrainSupported = n.parentId != null && terrainSupportLift(nodes, n.parentId, n.start[0], n.start[1]) != null;
18
+ return {
19
+ supportOffset: undefined,
20
+ ...(n.supportSlabId === GROUND_SUPPORT_ID && !terrainSupported
21
+ ? { supportSlabId: undefined }
22
+ : {}),
23
+ };
24
+ }
9
25
  const WALL_TRIM_PROFILE_OPTIONS = {
10
26
  skirting: [
11
27
  { label: 'Flat', value: 'flat' },
@@ -103,15 +119,37 @@ export default function WallPanel() {
103
119
  ];
104
120
  handleUpdate({ end: newEnd });
105
121
  }, [handleUpdate]);
106
- const handleBaseModeChange = useCallback((mode) => {
122
+ const handleTopModeChange = useCallback((mode) => {
107
123
  const n = nodeRef.current;
108
124
  if (!n)
109
125
  return;
110
- const height = n.height ?? resolveWallOpeningCeiling(n, useScene.getState().nodes);
111
- handleUpdate({
112
- height: Math.max(0.1, height),
113
- fillToTerrain: mode === 'terrain' ? true : undefined,
114
- });
126
+ const isCustom = n.height != null;
127
+ if (mode === 'custom' && !isCustom) {
128
+ // Seed from the current effective height so the geometry doesn't
129
+ // jump at the moment of detaching from the storey plane.
130
+ const seeded = resolveWallOpeningCeiling(n, useScene.getState().nodes);
131
+ handleUpdate({ height: Math.max(0.1, seeded) });
132
+ }
133
+ else if (mode === 'storey' && isCustom) {
134
+ // Absent `height` = plane-bound; the store strips undefined keys.
135
+ handleUpdate({ height: undefined, ...wallBaseRepairPatch(n) });
136
+ }
137
+ }, [handleUpdate]);
138
+ // Terrain infill only extends the bottom; it must never materialize an
139
+ // explicit height, or toggling it would silently detach the wall top from
140
+ // the storey plane. "Auto" is a re-election, so it carries the same base
141
+ // repair as the follows-level toggle — and the control fires on a click of
142
+ // the already-selected segment, so regression-era walls that DISPLAY Auto
143
+ // while secretly ground-pinned heal from a click on Auto itself.
144
+ const handleInfillChange = useCallback((mode) => {
145
+ const n = nodeRef.current;
146
+ if (!n)
147
+ return;
148
+ if (mode === 'terrain') {
149
+ handleUpdate({ fillToTerrain: true });
150
+ return;
151
+ }
152
+ handleUpdate({ fillToTerrain: undefined, ...wallBaseRepairPatch(n) });
115
153
  }, [handleUpdate]);
116
154
  const handleClose = useCallback(() => {
117
155
  setSelection({ selectedIds: [] });
@@ -127,6 +165,7 @@ export default function WallPanel() {
127
165
  return null;
128
166
  const length = getWallCurveLength(node);
129
167
  const followsTerrain = node.fillToTerrain === true;
168
+ const isPlaneBound = node.height == null;
130
169
  const height = node.height ?? resolvedHeightMeters ?? 2.5;
131
170
  const thickness = node.thickness ?? 0.1;
132
171
  const curveOffset = getClampedWallCurveOffset(node);
@@ -142,12 +181,15 @@ export default function WallPanel() {
142
181
  const skirting = { ...WALL_SKIRTING_DEFAULT, ...(node.skirting ?? {}) };
143
182
  const crown = { ...WALL_CROWN_DEFAULT, ...(node.crown ?? {}) };
144
183
  const chairRail = { ...WALL_CHAIR_RAIL_DEFAULT, ...(node.chairRail ?? {}) };
145
- return (_jsxs(PanelWrapper, { icon: "/icons/wall.webp", onClose: handleClose, title: node.name || 'Wall', width: 280, children: [_jsxs(PanelSection, { title: "Dimensions", children: [_jsx(SliderControl, { label: "Length", max: metersToLinearUnit(20, unit), min: metersToLinearUnit(0.1, unit), onChange: (value) => handleUpdateLength(linearControlValueToMeters(value, unit, { maxMeters: 20, minMeters: 0.1 })), precision: 2, step: unit === 'imperial' ? 0.1 : 0.01, unit: unitLabel, value: displayLength }), _jsx(SliderControl, { label: "Height", max: metersToLinearUnit(6, unit), min: metersToLinearUnit(0.1, unit), onChange: (v) => handleUpdate({
146
- height: linearControlValueToMeters(v, unit, { maxMeters: 6, minMeters: 0.1 }),
147
- }), precision: 2, step: 0.1, unit: unitLabel, value: Math.round(displayHeight * 100) / 100 }), _jsx("div", { className: "px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Base" }), _jsx(SegmentedControl, { onChange: handleBaseModeChange, options: [
148
- { label: 'Fixed', value: 'fixed' },
149
- { label: 'Follows level', value: 'terrain' },
150
- ], value: followsTerrain ? 'terrain' : 'fixed' }), followsTerrain && (_jsx("div", { className: "px-1 text-[11px] text-muted-foreground", children: "Extends downward to meet the terrain. Height and top stay unchanged." })), _jsx(SliderControl, { label: "Thickness", max: metersToLinearUnit(1, unit), min: metersToLinearUnit(0.05, unit), onChange: (v) => handleUpdate({
184
+ return (_jsxs(PanelWrapper, { icon: "/icons/wall.webp", onClose: handleClose, title: node.name || 'Wall', width: 280, children: [_jsxs(PanelSection, { title: "Dimensions", children: [_jsx(SliderControl, { label: "Length", max: metersToLinearUnit(20, unit), min: metersToLinearUnit(0.1, unit), onChange: (value) => handleUpdateLength(linearControlValueToMeters(value, unit, { maxMeters: 20, minMeters: 0.1 })), precision: 2, step: unit === 'imperial' ? 0.1 : 0.01, unit: unitLabel, value: displayLength }), _jsx("div", { className: "px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Top" }), _jsx(SegmentedControl, { onChange: handleTopModeChange, options: [
185
+ { label: 'Follows level', value: 'storey' },
186
+ { label: 'Custom height', value: 'custom' },
187
+ ], value: isPlaneBound ? 'storey' : 'custom' }), isPlaneBound ? (_jsxs("div", { className: "px-1 text-[11px] text-muted-foreground", children: ["Currently ", formatLinearMeasurement(height, unit)] })) : (_jsx(SliderControl, { label: "Height", max: metersToLinearUnit(20, unit), min: metersToLinearUnit(0.1, unit), onChange: (v) => handleUpdate({
188
+ height: linearControlValueToMeters(v, unit, { maxMeters: 20, minMeters: 0.1 }),
189
+ }), precision: 2, step: 0.1, unit: unitLabel, value: Math.round(displayHeight * 100) / 100 })), _jsx("div", { className: "px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Bottom" }), _jsx(SegmentedControl, { onChange: handleInfillChange, options: [
190
+ { label: 'Auto', value: 'auto' },
191
+ { label: 'Fill to terrain', value: 'terrain' },
192
+ ], value: followsTerrain ? 'terrain' : 'auto' }), followsTerrain && (_jsx("div", { className: "px-1 text-[11px] text-muted-foreground", children: "Extends downward to meet the terrain. Height and top stay unchanged." })), _jsx(SliderControl, { label: "Thickness", max: metersToLinearUnit(1, unit), min: metersToLinearUnit(0.05, unit), onChange: (v) => handleUpdate({
151
193
  thickness: linearControlValueToMeters(v, unit, {
152
194
  maxMeters: 1,
153
195
  minMeters: 0.05,
@@ -18,7 +18,7 @@ export const wallParametrics = {
18
18
  { key: 'thickness', kind: 'number', unit: 'm', min: 0.05, max: 0.6, step: 0.01 },
19
19
  // `height` may be absent (plane-bound top); the custom panel owns the
20
20
  // Follows storey / Custom height mode switch, so this is metadata only.
21
- { key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 6, step: 0.05 },
21
+ { key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 20, step: 0.05 },
22
22
  { key: 'curveOffset', kind: 'number', unit: 'm', min: -3, max: 3, step: 0.05 },
23
23
  ],
24
24
  },
@@ -8,6 +8,9 @@
8
8
  * bulk of the wall runtime (~820 lines in viewer).
9
9
  * - **`WallCutout`** — cutaway-mode hide/show logic based on camera
10
10
  * direction and `frontSide` / `backSide` interior/exterior tags.
11
+ * - **`WallBatchSystem`** — once a level stops changing, sews its opaque
12
+ * walls into one mesh per material set so a floor costs a handful of
13
+ * draw calls instead of one per wall face run.
11
14
  */
12
15
  declare const WallSystems: () => import("react").JSX.Element;
13
16
  export default WallSystems;
@@ -1 +1 @@
1
- {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/wall/system.tsx"],"names":[],"mappings":"AAyCA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,WAAW,mCAQhB,CAAA;AAED,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/wall/system.tsx"],"names":[],"mappings":"AA0CA;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,WAAW,mCAShB,CAAA;AAED,eAAe,WAAW,CAAA"}
@@ -5,6 +5,7 @@ import { WallCutout, WallSystem } from '@pascal-app/viewer';
5
5
  import { useFrame } from '@react-three/fiber';
6
6
  import { buildWallTreatmentLevelData, useWallTreatmentLevelData } from './treatment-level-data';
7
7
  import { wallTreatmentProudOffsets } from './treatments';
8
+ import { WallBatchSystem } from './wall-batch-system';
8
9
  function effectiveWall(wall) {
9
10
  const override = useLiveNodeOverrides.getState().get(wall.id);
10
11
  return override ? { ...wall, ...override } : wall;
@@ -46,8 +47,11 @@ const WallTreatmentMiterSystem = () => {
46
47
  * bulk of the wall runtime (~820 lines in viewer).
47
48
  * - **`WallCutout`** — cutaway-mode hide/show logic based on camera
48
49
  * direction and `frontSide` / `backSide` interior/exterior tags.
50
+ * - **`WallBatchSystem`** — once a level stops changing, sews its opaque
51
+ * walls into one mesh per material set so a floor costs a handful of
52
+ * draw calls instead of one per wall face run.
49
53
  */
50
54
  const WallSystems = () => {
51
- return (_jsxs(_Fragment, { children: [_jsx(WallTreatmentMiterSystem, {}), _jsx(WallSystem, {}), _jsx(WallCutout, {})] }));
55
+ return (_jsxs(_Fragment, { children: [_jsx(WallTreatmentMiterSystem, {}), _jsx(WallSystem, {}), _jsx(WallCutout, {}), _jsx(WallBatchSystem, {})] }));
52
56
  };
53
57
  export default WallSystems;
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/wall/tool.tsx"],"names":[],"mappings":"AAgcA,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EA+e5B,CAAA;AAED,eAAe,QAAQ,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/wall/tool.tsx"],"names":[],"mappings":"AA6bA,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EAkf5B,CAAA;AAED,eAAe,QAAQ,CAAA"}
package/dist/wall/tool.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { calculateLevelMiters, collectAlignmentAnchors, DEFAULT_LEVEL_HEIGHT, emitter, getWallMiterBoundaryPoints, resolveAlignment, resolveBuildingForLevel, sceneRegistry, useScene, wallClosesRoom, } from '@pascal-app/core';
2
+ import { calculateLevelMiters, collectAlignmentAnchors, DEFAULT_LEVEL_HEIGHT, emitter, GROUND_SUPPORT_ID, getWallMiterBoundaryPoints, resolveAlignment, resolveBuildingForLevel, sceneRegistry, useScene, wallClosesRoom, } from '@pascal-app/core';
3
3
  import { CursorSphere, chainEndJoinsExistingWall, clearPlacementSurface, createWallOnCurrentLevel, EDITOR_LAYER, formatAngleRadians, formatLinearMeasurement, getAngleArcToSegmentReference, getAngleToSegmentReference, getSegmentAngleReferenceAtPoint, isAlignmentGuideActive, isAngleSnapActive, isMagneticSnapActive, markToolCancelConsumed, publishHorizontalConstructionPlane, publishPlacementSurface, resampleTerrainConstructionPlane, resolveEventConstructionPlane, resolvePointerSupportSurface, snapWallDraftPointDetailed, triggerSFX, useAlignmentGuides, useEditor, useFloorplanDraftPreview, useSegmentDraftChain, useWallSnapIndicator, WALL_CONNECT_SNAP_RADIUS, WALL_JOIN_SNAP_RADIUS, } from '@pascal-app/editor';
4
4
  import { getSceneTheme, useViewer } from '@pascal-app/viewer';
5
5
  import { useThree } from '@react-three/fiber';
6
6
  import { useEffect, useRef, useState } from 'react';
7
- import { BoxGeometry, DoubleSide, Vector3 } from 'three';
7
+ import { DoubleSide, Vector3 } from 'three';
8
8
  import { DraftAngleArc, DraftAxisGuides, DraftMeasurementLabel, getNearestAxisAngleLabel, } from '../shared/draft-axis-guides';
9
9
  /**
10
10
  * Phase 5 Stage D — wall placement tool (kind-owned).
@@ -217,14 +217,14 @@ function getDraftAngleLabels(start, end, walls, baseY, previewHeight) {
217
217
  }
218
218
  return labels;
219
219
  }
220
- function getDraftMeasurementState(start, end, walls, unit, baseY, previewHeight) {
220
+ function getDraftMeasurementState(start, end, walls, unit, metricNotation, baseY, previewHeight) {
221
221
  const dx = end[0] - start[0];
222
222
  const dz = end[1] - start[1];
223
223
  const length = Math.hypot(dx, dz);
224
224
  if (length < 0.01)
225
225
  return null;
226
226
  return {
227
- lengthLabel: formatLinearMeasurement(length, unit),
227
+ lengthLabel: formatLinearMeasurement(length, unit, metricNotation),
228
228
  lengthPosition: [
229
229
  (start[0] + end[0]) / 2,
230
230
  baseY + previewHeight + DRAFT_LABEL_Y_OFFSET,
@@ -242,14 +242,10 @@ function updateWallPreview(mesh, start, end, previewHeight, previewThickness) {
242
242
  }
243
243
  mesh.visible = true;
244
244
  direction.normalize();
245
- const geometry = new BoxGeometry(length, previewHeight, previewThickness);
246
245
  const angle = Math.atan2(direction.z, direction.x);
247
246
  mesh.position.set((start.x + end.x) / 2, start.y + previewHeight / 2, (start.z + end.z) / 2);
248
247
  mesh.rotation.y = -angle;
249
- if (mesh.geometry) {
250
- mesh.geometry.dispose();
251
- }
252
- mesh.geometry = geometry;
248
+ mesh.scale.set(length, previewHeight, previewThickness);
253
249
  }
254
250
  function getLevelWalls(levelId, nodes) {
255
251
  if (!levelId)
@@ -295,6 +291,7 @@ function getBelowLevelWalls() {
295
291
  }
296
292
  export const WallTool = () => {
297
293
  const unit = useViewer((state) => state.unit);
294
+ const metricNotation = useViewer((state) => state.metricNotation);
298
295
  const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
299
296
  const activeLevelId = useViewer((state) => state.selection.levelId);
300
297
  const activeLevelHeight = useScene((state) => {
@@ -512,7 +509,7 @@ export const WallTool = () => {
512
509
  }
513
510
  previousWallEnd = currentWallEnd;
514
511
  updateWallPreview(wallPreviewRef.current, startingPoint.current, endingPoint.current, previewHeightRef.current, previewThicknessRef.current);
515
- setDraftMeasurement(getDraftMeasurementState([startingPoint.current.x, startingPoint.current.z], snappedLocal, walls, unit, startingPoint.current.y, previewHeightRef.current));
512
+ setDraftMeasurement(getDraftMeasurementState([startingPoint.current.x, startingPoint.current.z], snappedLocal, walls, unit, metricNotation, startingPoint.current.y, previewHeightRef.current));
516
513
  }
517
514
  else {
518
515
  const hoverPlane = resampleTerrainConstructionPlane(resolveEventConstructionPlane(event, pointed), gridPosition);
@@ -564,13 +561,9 @@ export const WallTool = () => {
564
561
  angleLabel: null,
565
562
  });
566
563
  triggerSFX('sfx:structure-build-start');
567
- // Visibility is owned by `updateWallPreview` — it flips
568
- // `mesh.visible` based on segment length. Setting it here
569
- // (before any geometry data has been written) draws the
570
- // mesh's empty `<shapeGeometry/>` placeholder, which WebGPU
571
- // flags as "Vertex buffer slot 0 ... was not set" on the
572
- // first frame after click. Leaving it false until the next
573
- // `onGridMove` writes a real BoxGeometry skips that frame.
564
+ // Visibility is owned by `updateWallPreview`. Leave the
565
+ // unit box hidden until the first pointer move scales and
566
+ // positions it for the active segment.
574
567
  setDraftMeasurement(null);
575
568
  }
576
569
  else if (buildingState.current === 1) {
@@ -586,11 +579,17 @@ export const WallTool = () => {
586
579
  const dz = snappedEnd[1] - startingPoint.current.z;
587
580
  if (dx * dx + dz * dz < 0.01 * 0.01)
588
581
  return;
582
+ // A ground(terrain)-hosted chain keeps its frozen construction plane;
583
+ // any other chain re-resolves the aimed surface per commit so a later
584
+ // segment can still elect the slab it visibly crosses instead of
585
+ // being capped at the first click's elevation.
586
+ const draftPlane = constructionPlane.current;
587
+ const commitPointed = draftPlane?.supportSlabId === GROUND_SUPPORT_ID ? null : pointedSurfaceFor(event);
589
588
  // Both start and end are building-local ✓
590
589
  const createdWall = createWallOnCurrentLevel([startingPoint.current.x, startingPoint.current.z], snappedEnd, {
591
- supportCap: constructionPlane.current?.elevation ?? null,
592
- preferredSupportSlabId: constructionPlane.current?.supportSlabId ?? null,
593
- constructionElevation: constructionPlane.current?.elevation ?? null,
590
+ supportCap: commitPointed ? commitPointed.elevation : (draftPlane?.elevation ?? null),
591
+ preferredSupportSlabId: draftPlane?.supportSlabId ?? null,
592
+ constructionElevation: draftPlane?.elevation ?? null,
594
593
  constructionHeight: previewHeightRef.current,
595
594
  });
596
595
  if (!createdWall)
@@ -642,11 +641,9 @@ export const WallTool = () => {
642
641
  y: draftY,
643
642
  angleLabel: null,
644
643
  });
645
- // Hide the preview until the next `onGridMove` writes the
646
- // new segment's geometry. Without this the prior segment's
647
- // BoxGeometry stays visible for a frame on top of the
648
- // freshly-committed real wall, producing a brief
649
- // double-paint at the new wall's position.
644
+ // Hide the preview until the next `onGridMove` scales and
645
+ // repositions it. Otherwise the prior segment stays visible
646
+ // for a frame on top of the freshly committed wall.
650
647
  if (wallPreviewRef.current) {
651
648
  wallPreviewRef.current.visible = false;
652
649
  }
@@ -674,7 +671,7 @@ export const WallTool = () => {
674
671
  draftPreview.setWallDraftStart(null);
675
672
  draftPreview.setWallDraftEnd(null);
676
673
  };
677
- }, [unit]);
678
- return (_jsxs("group", { children: [_jsx(DraftAxisGuides, { guide: axisGuide, labelColor: measurementColor, labelShadowColor: measurementShadowColor }), _jsx(CursorSphere, { height: previewHeight, ref: cursorRef }), _jsxs("mesh", { layers: EDITOR_LAYER, ref: wallPreviewRef, renderOrder: 1, visible: false, children: [_jsx("shapeGeometry", {}), _jsx("meshBasicMaterial", { color: "#818cf8", depthTest: false, depthWrite: false, opacity: 0.5, 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)))] }))] }));
674
+ }, [unit, metricNotation]);
675
+ return (_jsxs("group", { children: [_jsx(DraftAxisGuides, { guide: axisGuide, labelColor: measurementColor, labelShadowColor: measurementShadowColor }), _jsx(CursorSphere, { height: previewHeight, ref: cursorRef }), _jsxs("mesh", { layers: EDITOR_LAYER, ref: wallPreviewRef, renderOrder: 1, visible: false, children: [_jsx("boxGeometry", {}), _jsx("meshBasicMaterial", { color: "#818cf8", depthTest: false, depthWrite: false, opacity: 0.5, 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)))] }))] }));
679
676
  };
680
677
  export default WallTool;