@pascal-app/editor 0.9.1 → 0.9.2

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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -2,6 +2,12 @@
2
2
 
3
3
  import { useScene } from '@pascal-app/core'
4
4
  import { useCallback, useEffect, useRef, useState } from 'react'
5
+ import {
6
+ lingoUnitSpec,
7
+ measurementHint,
8
+ parseMeasurement,
9
+ } from '../../../lib/measurement-parser'
10
+ import { useLinearDisplay } from '../../../lib/use-linear-display'
5
11
  import { cn } from '../../../lib/utils'
6
12
 
7
13
  interface SliderControlProps {
@@ -59,10 +65,17 @@ export function SliderControl({
59
65
  unit = '',
60
66
  restoreOnCommit = true,
61
67
  }: SliderControlProps) {
68
+ // Display/storage conversion so the value honors the metric/imperial toggle.
69
+ // `value`, `onChange`, `onCommit`, `min`/`max`/`clamp` are always in the
70
+ // stored unit (meters for `unit === 'm'`); the step, drag deltas, text field
71
+ // and rendered number are in the DISPLAY unit (feet when imperial). For
72
+ // metric and non-length units these conversions are the identity.
73
+ const { isImperial, displayUnit, toDisplay, toStored } = useLinearDisplay(unit, precision)
74
+
62
75
  const [isEditing, setIsEditing] = useState(false)
63
76
  const [isDragging, setIsDragging] = useState(false)
64
77
  const [isHovered, setIsHovered] = useState(false)
65
- const [inputValue, setInputValue] = useState(value.toFixed(precision))
78
+ const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
66
79
 
67
80
  const dragRef = useRef<{
68
81
  // Original value at drag start — preserved across modifier re-anchors so
@@ -80,12 +93,23 @@ export function SliderControl({
80
93
  valueRef.current = value
81
94
 
82
95
  const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
96
+ // Apply a signed display-unit delta to a stored value, rounding in the
97
+ // display unit and clamping in the stored unit.
98
+ const applyDisplayDelta = useCallback(
99
+ (storedValue: number, displayDelta: number, displayStep: number) =>
100
+ clamp(
101
+ toStored(
102
+ Number.parseFloat((toDisplay(storedValue) + displayDelta).toFixed(stepPrecision(displayStep))),
103
+ ),
104
+ ),
105
+ [clamp, toDisplay, toStored],
106
+ )
83
107
 
84
108
  useEffect(() => {
85
109
  if (!isEditing) {
86
- setInputValue(value.toFixed(precision))
110
+ setInputValue(toDisplay(value).toFixed(precision))
87
111
  }
88
- }, [value, precision, isEditing])
112
+ }, [value, precision, isEditing, toDisplay])
89
113
 
90
114
  // Wheel support on the label
91
115
  useEffect(() => {
@@ -96,14 +120,13 @@ export function SliderControl({
96
120
  e.preventDefault()
97
121
  const direction = e.deltaY < 0 ? 1 : -1
98
122
  const s = getAdjustedStep(step, e)
99
- const newValue = clamp(valueRef.current + direction * s)
100
- const final = Number.parseFloat(newValue.toFixed(stepPrecision(s)))
123
+ const final = applyDisplayDelta(valueRef.current, direction * s, s)
101
124
  if (final !== valueRef.current) onChange(final)
102
125
  onCommit?.(final)
103
126
  }
104
127
  el.addEventListener('wheel', handleWheel, { passive: false })
105
128
  return () => el.removeEventListener('wheel', handleWheel)
106
- }, [isEditing, step, clamp, onChange, onCommit])
129
+ }, [isEditing, step, applyDisplayDelta, onChange, onCommit])
107
130
 
108
131
  // Arrow key support while hovered
109
132
  useEffect(() => {
@@ -115,15 +138,14 @@ export function SliderControl({
115
138
  if (direction !== 0) {
116
139
  e.preventDefault()
117
140
  const s = getAdjustedStep(step, e)
118
- const newValue = clamp(valueRef.current + direction * s)
119
- const final = Number.parseFloat(newValue.toFixed(stepPrecision(s)))
141
+ const final = applyDisplayDelta(valueRef.current, direction * s, s)
120
142
  if (final !== valueRef.current) onChange(final)
121
143
  onCommit?.(final)
122
144
  }
123
145
  }
124
146
  window.addEventListener('keydown', handleKeyDown)
125
147
  return () => window.removeEventListener('keydown', handleKeyDown)
126
- }, [isHovered, isEditing, step, clamp, onChange, onCommit])
148
+ }, [isHovered, isEditing, step, applyDisplayDelta, onChange, onCommit])
127
149
 
128
150
  const handleLabelPointerDown = useCallback(
129
151
  (e: React.PointerEvent<HTMLDivElement>) => {
@@ -160,15 +182,13 @@ export function SliderControl({
160
182
  const dx = e.clientX - anchorX
161
183
  const s = step * multiplier
162
184
  // 4 px per step at default sensitivity
163
- const newValue = clamp(
164
- Number.parseFloat((anchorValue + (dx / 4) * s).toFixed(stepPrecision(s))),
165
- )
185
+ const newValue = applyDisplayDelta(anchorValue, (dx / 4) * s, s)
166
186
  if (newValue !== valueRef.current) {
167
187
  valueRef.current = newValue
168
188
  onChange(newValue)
169
189
  }
170
190
  },
171
- [step, clamp, onChange],
191
+ [step, applyDisplayDelta, onChange],
172
192
  )
173
193
 
174
194
  const handleLabelPointerUp = useCallback(
@@ -195,49 +215,65 @@ export function SliderControl({
195
215
 
196
216
  const handleValueClick = useCallback(() => {
197
217
  setIsEditing(true)
198
- setInputValue(value.toFixed(precision))
199
- }, [value, precision])
218
+ setInputValue(toDisplay(value).toFixed(precision))
219
+ }, [value, precision, toDisplay])
200
220
 
201
221
  const submitValue = useCallback(() => {
202
- const numValue = Number.parseFloat(inputValue)
203
- if (Number.isNaN(numValue)) {
204
- setInputValue(value.toFixed(precision))
222
+ const spec = lingoUnitSpec(unit)
223
+ let stored = spec
224
+ ? parseMeasurement(inputValue, spec, {
225
+ bareUnit: isImperial ? 'ft' : spec.unitId,
226
+ system: isImperial ? 'us' : 'metric',
227
+ })
228
+ : null
229
+ if (stored === null) {
230
+ // Fallback: a bare number typed in the DISPLAY unit → convert to stored.
231
+ const numValue = Number.parseFloat(inputValue)
232
+ stored = Number.isFinite(numValue) ? toStored(numValue) : null
233
+ }
234
+ if (stored === null) {
235
+ setInputValue(toDisplay(value).toFixed(precision))
205
236
  } else {
206
- const nextValue = clamp(Number.parseFloat(numValue.toFixed(precision)))
237
+ const nextValue = clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))))
207
238
  onChange(nextValue)
208
239
  onCommit?.(nextValue)
209
240
  }
210
241
  setIsEditing(false)
211
- }, [inputValue, onChange, onCommit, clamp, precision, value])
242
+ }, [inputValue, unit, isImperial, onChange, onCommit, clamp, precision, value, toDisplay, toStored])
243
+
244
+ const spec = lingoUnitSpec(unit)
245
+ const hint =
246
+ isEditing && spec
247
+ ? measurementHint(inputValue, spec, {
248
+ bareUnit: isImperial ? 'ft' : spec.unitId,
249
+ system: isImperial ? 'us' : 'metric',
250
+ displayUnit: isImperial ? 'ft' : spec.unitId,
251
+ precision,
252
+ clamp,
253
+ })
254
+ : null
212
255
 
213
256
  const handleInputKeyDown = useCallback(
214
257
  (e: React.KeyboardEvent<HTMLInputElement>) => {
215
258
  if (e.key === 'Enter') {
216
259
  submitValue()
217
260
  } else if (e.key === 'Escape') {
218
- setInputValue(value.toFixed(precision))
261
+ setInputValue(toDisplay(value).toFixed(precision))
219
262
  setIsEditing(false)
220
- } else if (e.key === 'ArrowUp') {
263
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
221
264
  e.preventDefault()
265
+ const direction = e.key === 'ArrowUp' ? 1 : -1
222
266
  const adjustedStep = getAdjustedStep(step, e)
223
- const newV = clamp(
224
- Number.parseFloat((value + adjustedStep).toFixed(stepPrecision(adjustedStep))),
225
- )
267
+ const newV = applyDisplayDelta(value, direction * adjustedStep, adjustedStep)
226
268
  onChange(newV)
227
- setInputValue(newV.toFixed(precision))
228
- } else if (e.key === 'ArrowDown') {
229
- e.preventDefault()
230
- const adjustedStep = getAdjustedStep(step, e)
231
- const newV = clamp(
232
- Number.parseFloat((value - adjustedStep).toFixed(stepPrecision(adjustedStep))),
233
- )
234
- onChange(newV)
235
- setInputValue(newV.toFixed(precision))
269
+ setInputValue(toDisplay(newV).toFixed(precision))
236
270
  }
237
271
  },
238
- [submitValue, value, precision, step, clamp, onChange],
272
+ [submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
239
273
  )
240
274
 
275
+ const displayValue = toDisplay(value)
276
+
241
277
  return (
242
278
  <div
243
279
  className={cn(
@@ -279,6 +315,11 @@ export function SliderControl({
279
315
  <div className="flex items-center text-xs">
280
316
  {isEditing ? (
281
317
  <>
318
+ {hint && (
319
+ <span className="mr-1 shrink-0 whitespace-nowrap text-[10px] text-muted-foreground/50 tabular-nums">
320
+ {hint}
321
+ </span>
322
+ )}
282
323
  <input
283
324
  autoFocus
284
325
  className="w-14 bg-transparent p-0 text-right font-mono text-foreground outline-none selection:bg-primary/30"
@@ -288,7 +329,16 @@ export function SliderControl({
288
329
  type="text"
289
330
  value={inputValue}
290
331
  />
291
- {unit && <span className="ml-[1px] text-muted-foreground">{unit}</span>}
332
+ {displayUnit && (
333
+ <span
334
+ className={cn(
335
+ 'ml-[1px] transition-opacity duration-150',
336
+ hint ? 'opacity-0' : 'text-muted-foreground/40',
337
+ )}
338
+ >
339
+ {displayUnit}
340
+ </span>
341
+ )}
292
342
  </>
293
343
  ) : (
294
344
  <div
@@ -296,9 +346,9 @@ export function SliderControl({
296
346
  onClick={handleValueClick}
297
347
  >
298
348
  <span className="font-mono tabular-nums tracking-tight" suppressHydrationWarning>
299
- {Number(value.toFixed(precision)).toFixed(precision)}
349
+ {Number(displayValue.toFixed(precision)).toFixed(precision)}
300
350
  </span>
301
- {unit && <span className="ml-[1px] text-muted-foreground">{unit}</span>}
351
+ {displayUnit && <span className="ml-[1px] text-muted-foreground">{displayUnit}</span>}
302
352
  </div>
303
353
  )}
304
354
  </div>
@@ -41,7 +41,7 @@ import {
41
41
  buildLevelDuplicateCreateOps,
42
42
  type LevelDuplicatePreset,
43
43
  } from '../../lib/level-duplication'
44
- import { getDefaultLevelName, getLevelDisplayName } from '../../lib/level-name'
44
+ import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
45
45
  import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
46
46
  import {
47
47
  getEditorClipboardSnapshot,
@@ -1,32 +1,19 @@
1
- import { ShortcutToken } from '../primitives/shortcut-token'
1
+ import { ContextualHelperPanel } from './contextual-helper-panel'
2
2
 
3
3
  interface BuildingHelperProps {
4
4
  showRotate?: boolean
5
5
  }
6
6
 
7
+ // Rotate is one hint with both keys (R / T) — never two separate
8
+ // counterclockwise / clockwise rows — to match every other placement helper.
7
9
  export function BuildingHelper({ showRotate }: BuildingHelperProps) {
8
10
  return (
9
- <div className="pointer-events-none fixed top-1/2 right-4 z-40 flex -translate-y-1/2 flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
10
- <div className="flex items-center gap-2 text-sm">
11
- <ShortcutToken value="Left click" />
12
- <span className="text-muted-foreground">Place building</span>
13
- </div>
14
- {showRotate && (
15
- <>
16
- <div className="flex items-center gap-2 text-sm">
17
- <ShortcutToken value="R" />
18
- <span className="text-muted-foreground">Rotate counterclockwise</span>
19
- </div>
20
- <div className="flex items-center gap-2 text-sm">
21
- <ShortcutToken value="T" />
22
- <span className="text-muted-foreground">Rotate clockwise</span>
23
- </div>
24
- </>
25
- )}
26
- <div className="flex items-center gap-2 text-sm">
27
- <ShortcutToken value="Esc" />
28
- <span className="text-muted-foreground">Cancel</span>
29
- </div>
30
- </div>
11
+ <ContextualHelperPanel
12
+ hints={[
13
+ { keys: ['Left click'], label: 'Place building' },
14
+ ...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),
15
+ { keys: ['Esc'], label: 'Cancel' },
16
+ ]}
17
+ />
31
18
  )
32
19
  }