@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,156 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, AnyNodeId } from '@pascal-app/core'
3
+ import { useScene } from '@pascal-app/core'
4
+ import { commitDimensionValue, isDimensionEditAllowed } from './floorplan-dimension-edit-overlay'
5
+
6
+ type Change = { id: AnyNodeId; data: Partial<AnyNode> }
7
+
8
+ function wall(id: string, start: [number, number], end: [number, number]): AnyNode {
9
+ return {
10
+ object: 'node',
11
+ id,
12
+ type: 'wall',
13
+ parentId: 'level_1',
14
+ visible: true,
15
+ metadata: {},
16
+ start,
17
+ end,
18
+ thickness: 0.15,
19
+ children: [],
20
+ } as unknown as AnyNode
21
+ }
22
+
23
+ function nodes(): Record<string, AnyNode> {
24
+ return {
25
+ level_1: {
26
+ object: 'node',
27
+ id: 'level_1',
28
+ type: 'level',
29
+ parentId: null,
30
+ visible: true,
31
+ metadata: {},
32
+ level: 0,
33
+ children: ['wall_a', 'wall_b', 'dim_1'],
34
+ } as unknown as AnyNode,
35
+ wall_a: wall('wall_a', [0, 0], [3, 0]),
36
+ wall_b: wall('wall_b', [3, 0], [3, 4]),
37
+ dim_1: {
38
+ object: 'node',
39
+ id: 'dim_1',
40
+ type: 'construction-dimension',
41
+ parentId: 'level_1',
42
+ visible: true,
43
+ metadata: {},
44
+ children: [],
45
+ } as unknown as AnyNode,
46
+ }
47
+ }
48
+
49
+ let initial: ReturnType<typeof useScene.getState>
50
+ let applied: Change[]
51
+ let updated: Change[]
52
+ let dirty: AnyNodeId[]
53
+
54
+ beforeEach(() => {
55
+ initial = useScene.getState()
56
+ applied = []
57
+ updated = []
58
+ dirty = []
59
+ useScene.setState({
60
+ nodes: nodes() as ReturnType<typeof useScene.getState>['nodes'],
61
+ applyNodeChanges: ((changes: { update?: Change[] }) => {
62
+ applied.push(...(changes.update ?? []))
63
+ }) as ReturnType<typeof useScene.getState>['applyNodeChanges'],
64
+ updateNode: ((id: AnyNodeId, data: Partial<AnyNode>) => {
65
+ updated.push({ id, data })
66
+ }) as ReturnType<typeof useScene.getState>['updateNode'],
67
+ markDirty: ((id: AnyNodeId) => {
68
+ dirty.push(id)
69
+ }) as ReturnType<typeof useScene.getState>['markDirty'],
70
+ })
71
+ })
72
+
73
+ afterEach(() => {
74
+ useScene.setState(initial, true)
75
+ })
76
+
77
+ describe('commitDimensionValue', () => {
78
+ test('a wall dimension drives the wall end and the wall sharing that corner', () => {
79
+ const outcome = commitDimensionValue({
80
+ target: {
81
+ ownerNodeId: 'wall_a' as AnyNodeId,
82
+ witnessStart: [0, 0],
83
+ witnessEnd: [3, 0],
84
+ value: 3,
85
+ },
86
+ nextLength: 3.5,
87
+ })
88
+
89
+ expect(outcome).toEqual({ ok: true })
90
+ const byId = new Map(applied.map((change) => [change.id, change.data]))
91
+ expect(byId.get('wall_a' as AnyNodeId)).toEqual({ start: [0, 0], end: [3.5, 0] })
92
+ expect(byId.get('wall_b' as AnyNodeId)).toEqual({ start: [3.5, 0], end: [3, 4] })
93
+ expect(dirty.sort()).toEqual(['wall_a', 'wall_b'] as AnyNodeId[])
94
+ expect(updated).toEqual([])
95
+ })
96
+
97
+ test('an undrivable wall dimension is refused and nothing is written', () => {
98
+ const outcome = commitDimensionValue({
99
+ target: {
100
+ ownerNodeId: 'wall_a' as AnyNodeId,
101
+ witnessStart: [1.5, -1],
102
+ witnessEnd: [1.5, 1],
103
+ value: 2,
104
+ },
105
+ nextLength: 2.5,
106
+ })
107
+
108
+ expect(outcome.ok).toBe(false)
109
+ expect(applied).toEqual([])
110
+ expect(updated).toEqual([])
111
+ })
112
+
113
+ test('an undrivable construction dimension keeps the typed text as an override', () => {
114
+ const outcome = commitDimensionValue({
115
+ target: {
116
+ ownerNodeId: 'dim_1' as AnyNodeId,
117
+ witnessStart: [10, 10],
118
+ witnessEnd: [12, 10],
119
+ value: 2,
120
+ },
121
+ nextLength: 3,
122
+ unit: 'metric',
123
+ })
124
+
125
+ expect(outcome).toEqual({ ok: true })
126
+ expect(applied).toEqual([])
127
+ expect(updated).toHaveLength(1)
128
+ expect(updated[0]?.id).toBe('dim_1' as AnyNodeId)
129
+ expect(typeof (updated[0]?.data as { textOverride?: unknown }).textOverride).toBe('string')
130
+ })
131
+ })
132
+
133
+ describe('isDimensionEditAllowed', () => {
134
+ const idle = {
135
+ workspaceMode: 'edit',
136
+ mode: 'select',
137
+ isPreviewMode: false,
138
+ isCaptureMode: false,
139
+ isFirstPersonMode: false,
140
+ } as const
141
+
142
+ test('only an editable scene in select mode of the edit workspace', () => {
143
+ expect(isDimensionEditAllowed(idle, { readOnly: false })).toBe(true)
144
+ expect(isDimensionEditAllowed(idle, { readOnly: true })).toBe(false)
145
+ expect(isDimensionEditAllowed({ ...idle, workspaceMode: 'studio' }, { readOnly: false })).toBe(
146
+ false,
147
+ )
148
+ expect(isDimensionEditAllowed({ ...idle, workspaceMode: 'sheets' }, { readOnly: false })).toBe(
149
+ false,
150
+ )
151
+ expect(isDimensionEditAllowed({ ...idle, mode: 'delete' }, { readOnly: false })).toBe(false)
152
+ expect(isDimensionEditAllowed({ ...idle, isPreviewMode: true }, { readOnly: false })).toBe(
153
+ false,
154
+ )
155
+ })
156
+ })
@@ -0,0 +1,278 @@
1
+ 'use client'
2
+
3
+ import type { AnyNode, AnyNodeId } from '@pascal-app/core'
4
+ import { useScene } from '@pascal-app/core'
5
+ import { useViewer } from '@pascal-app/viewer'
6
+ import { useCallback, useEffect, useRef, useState } from 'react'
7
+ import { createPortal } from 'react-dom'
8
+ import {
9
+ parseDimensionInput,
10
+ planDimensionDrive,
11
+ resolveDimensionDrive,
12
+ } from '../../../lib/floorplan/dimension-drive'
13
+ import { formatScheduleLength } from '../../../lib/floorplan/schedules'
14
+ import useEditor from '../../../store/use-editor'
15
+
16
+ /**
17
+ * Click-to-type dimensions — WS3.
18
+ *
19
+ * Single-clicking a dimension label opens an inline input over the label
20
+ * plate, pre-filled with the current text. Enter commits, Esc cancels.
21
+ * Committing DRIVES geometry: the anchor the dimension runs toward moves
22
+ * along the dimension direction by the delta, and connected wall junctions
23
+ * follow (`planDimensionDrive`). When nothing bindable is under the far
24
+ * anchor the value is written as `textOverride` on a construction-dimension
25
+ * node instead, and the label renders an "override" badge.
26
+ *
27
+ * The label is found by DOM delegation on the attributes
28
+ * `floorplan-dimension-renderer.tsx` emits, so this works identically for
29
+ * `dimension` and `dimension-string` geometry, for automatic wall dimensions,
30
+ * contextual dimensions and manual construction-dimension nodes — and on
31
+ * sheets, which mount the same renderer.
32
+ */
33
+
34
+ type EditGateEditorState = Pick<
35
+ ReturnType<typeof useEditor.getState>,
36
+ 'workspaceMode' | 'mode' | 'isPreviewMode' | 'isCaptureMode' | 'isFirstPersonMode'
37
+ >
38
+
39
+ /**
40
+ * Typing a dimension moves geometry, so it is only offered on an editable
41
+ * scene in idle select mode — never in studio, sheets, preview, capture or
42
+ * read-only (which is how version preview locks the graph).
43
+ */
44
+ export function isDimensionEditAllowed(
45
+ editor: EditGateEditorState,
46
+ scene: { readOnly: boolean },
47
+ ): boolean {
48
+ return (
49
+ editor.workspaceMode === 'edit' &&
50
+ editor.mode === 'select' &&
51
+ !editor.isPreviewMode &&
52
+ !editor.isCaptureMode &&
53
+ !editor.isFirstPersonMode &&
54
+ !scene.readOnly
55
+ )
56
+ }
57
+
58
+ export function useDimensionEditAllowed(): boolean {
59
+ const readOnly = useScene((state) => state.readOnly)
60
+ return useEditor((state) => isDimensionEditAllowed(state, { readOnly }))
61
+ }
62
+
63
+ type EditTarget = {
64
+ ownerNodeId: AnyNodeId
65
+ text: string
66
+ value: number
67
+ witnessStart: [number, number]
68
+ witnessEnd: [number, number]
69
+ rect: { left: number; top: number; width: number; height: number }
70
+ }
71
+
72
+ const MIN_INPUT_WIDTH_PX = 76
73
+ const CLICK_SLOP_PX = 4
74
+
75
+ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
76
+ const [target, setTarget] = useState<EditTarget | null>(null)
77
+ const [draft, setDraft] = useState('')
78
+ const [error, setError] = useState<string | null>(null)
79
+ const inputRef = useRef<HTMLInputElement | null>(null)
80
+ const unit = useViewer((state) => state.unit)
81
+
82
+ useEffect(() => {
83
+ let down: { x: number; y: number } | null = null
84
+ const onPointerDown = (event: PointerEvent) => {
85
+ down = { x: event.clientX, y: event.clientY }
86
+ }
87
+ const onClick = (event: MouseEvent) => {
88
+ if (event.button !== 0 || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) {
89
+ return
90
+ }
91
+ if (down && Math.hypot(event.clientX - down.x, event.clientY - down.y) > CLICK_SLOP_PX) {
92
+ return
93
+ }
94
+ if (!isDimensionEditAllowed(useEditor.getState(), useScene.getState())) return
95
+ const origin = event.target as Element | null
96
+ const hit = origin?.closest?.('[data-floorplan-dimension-hit]')
97
+ if (!hit) return
98
+ const label = hit.closest('[data-floorplan-annotation-label]')
99
+ const owner = hit.closest('[data-node-id]')
100
+ const ownerNodeId = owner?.getAttribute('data-node-id')
101
+ if (!label || !ownerNodeId) return
102
+
103
+ const value = number(label.getAttribute('data-floorplan-dimension-value'))
104
+ const witnessStart = point(
105
+ label.getAttribute('data-floorplan-dimension-witness-start-x'),
106
+ label.getAttribute('data-floorplan-dimension-witness-start-y'),
107
+ )
108
+ const witnessEnd = point(
109
+ label.getAttribute('data-floorplan-dimension-witness-end-x'),
110
+ label.getAttribute('data-floorplan-dimension-witness-end-y'),
111
+ )
112
+ if (value === null || !witnessStart || !witnessEnd) return
113
+
114
+ event.preventDefault()
115
+ event.stopPropagation()
116
+ const box = label.getBoundingClientRect()
117
+ const text = label.getAttribute('data-floorplan-dimension-text') ?? ''
118
+ setTarget({
119
+ ownerNodeId: ownerNodeId as AnyNodeId,
120
+ text,
121
+ value,
122
+ witnessStart,
123
+ witnessEnd,
124
+ rect: {
125
+ left: box.left + box.width / 2,
126
+ top: box.top + box.height / 2,
127
+ width: Math.max(MIN_INPUT_WIDTH_PX, box.width + 24),
128
+ height: Math.max(22, box.height + 6),
129
+ },
130
+ })
131
+ setDraft(text)
132
+ setError(null)
133
+ }
134
+
135
+ // Document, not window: drag and marquee handlers swallow the click that
136
+ // ends them with a window capture listener registered after this one, and
137
+ // only a listener further down the capture path is skipped by it.
138
+ document.addEventListener('pointerdown', onPointerDown, true)
139
+ document.addEventListener('click', onClick, true)
140
+ return () => {
141
+ document.removeEventListener('pointerdown', onPointerDown, true)
142
+ document.removeEventListener('click', onClick, true)
143
+ }
144
+ }, [])
145
+
146
+ useEffect(() => {
147
+ if (!target) return
148
+ const frame = requestAnimationFrame(() => {
149
+ inputRef.current?.focus()
150
+ inputRef.current?.select()
151
+ })
152
+ return () => cancelAnimationFrame(frame)
153
+ }, [target])
154
+
155
+ const commit = useCallback(() => {
156
+ if (!target) return
157
+ const nextLength = parseDimensionInput(draft, unit)
158
+ if (nextLength === null) {
159
+ setError(`Cannot read "${draft.trim()}"`)
160
+ return
161
+ }
162
+ const outcome = commitDimensionValue({ target, nextLength, unit })
163
+ if (!outcome.ok) {
164
+ setError(outcome.reason)
165
+ return
166
+ }
167
+ setTarget(null)
168
+ setError(null)
169
+ }, [draft, target, unit])
170
+
171
+ if (!target) return null
172
+ if (typeof document === 'undefined') return null
173
+
174
+ return createPortal(
175
+ <div
176
+ className="pointer-events-none fixed inset-0 z-[70]"
177
+ data-floorplan-dimension-edit-overlay=""
178
+ >
179
+ <div
180
+ className="pointer-events-auto absolute -translate-x-1/2 -translate-y-1/2"
181
+ style={{ left: target.rect.left, top: target.rect.top, width: target.rect.width }}
182
+ >
183
+ <input
184
+ className="w-full rounded-sm border border-primary bg-card px-1 py-0.5 text-center font-mono text-foreground text-xs shadow-sm outline-none"
185
+ onBlur={() => setTarget(null)}
186
+ onChange={(event) => {
187
+ setDraft(event.target.value)
188
+ setError(null)
189
+ }}
190
+ onKeyDown={(event) => {
191
+ if (event.key === 'Enter') {
192
+ event.preventDefault()
193
+ commit()
194
+ } else if (event.key === 'Escape') {
195
+ event.preventDefault()
196
+ setTarget(null)
197
+ setError(null)
198
+ }
199
+ event.stopPropagation()
200
+ }}
201
+ ref={inputRef}
202
+ spellCheck={false}
203
+ value={draft}
204
+ />
205
+ {error ? (
206
+ <div className="mt-0.5 whitespace-nowrap rounded-sm bg-card px-1 text-[10px] text-destructive shadow-sm">
207
+ {error}
208
+ </div>
209
+ ) : null}
210
+ </div>
211
+ </div>,
212
+ document.body,
213
+ )
214
+ }
215
+
216
+ type CommitOutcome = { ok: true } | { ok: false; reason: string }
217
+
218
+ /**
219
+ * Apply a typed dimension value. Exported for tests — it is the seam between
220
+ * the pure drive math and the scene store.
221
+ */
222
+ export function commitDimensionValue(args: {
223
+ target: Pick<EditTarget, 'ownerNodeId' | 'witnessStart' | 'witnessEnd' | 'value'>
224
+ nextLength: number
225
+ unit?: 'metric' | 'imperial'
226
+ }): CommitOutcome {
227
+ const store = useScene.getState()
228
+ const nodes = store.nodes as Readonly<Record<string, AnyNode>>
229
+ const resolution = resolveDimensionDrive({
230
+ nodes,
231
+ ownerNodeId: args.target.ownerNodeId,
232
+ start: args.target.witnessStart,
233
+ end: args.target.witnessEnd,
234
+ })
235
+
236
+ if (!resolution.drivable) {
237
+ // Fallback: annotate rather than move. Only a construction-dimension
238
+ // node can carry an override — anything else has nowhere to put it.
239
+ const owner = nodes[args.target.ownerNodeId]
240
+ if (owner?.type !== 'construction-dimension') {
241
+ return { ok: false, reason: `Not drivable — ${resolution.reason}` }
242
+ }
243
+ store.updateNode(args.target.ownerNodeId, {
244
+ textOverride: formatScheduleLength(args.nextLength, args.unit ?? 'imperial'),
245
+ } as Partial<AnyNode>)
246
+ return { ok: true }
247
+ }
248
+
249
+ const plan = planDimensionDrive({
250
+ nodes,
251
+ target: resolution.target,
252
+ currentLength: args.target.value,
253
+ nextLength: args.nextLength,
254
+ })
255
+ if (!plan) return { ok: false, reason: 'Value would collapse the geometry' }
256
+ if (plan.updates.length === 0) return { ok: true }
257
+
258
+ const changes = plan.updates.map((update) => ({
259
+ id: update.id,
260
+ data: update.data as Partial<AnyNode>,
261
+ }))
262
+ if (store.applyNodeChanges) store.applyNodeChanges({ update: changes })
263
+ else for (const change of changes) store.updateNode(change.id, change.data)
264
+ for (const change of changes) store.markDirty(change.id)
265
+ return { ok: true }
266
+ }
267
+
268
+ function number(value: string | null): number | null {
269
+ if (value === null) return null
270
+ const parsed = Number.parseFloat(value)
271
+ return Number.isFinite(parsed) ? parsed : null
272
+ }
273
+
274
+ function point(x: string | null, y: string | null): [number, number] | null {
275
+ const parsedX = number(x)
276
+ const parsedY = number(y)
277
+ return parsedX === null || parsedY === null ? null : [parsedX, parsedY]
278
+ }
@@ -1,4 +1,5 @@
1
1
  import type { FloorplanGeometry, FloorplanPoint } from '@pascal-app/core'
2
+ import { useDimensionEditAllowed } from './floorplan-dimension-edit-overlay'
2
3
  import { resolveFloorplanLabelAngle } from './floorplan-label-angle'
3
4
 
4
5
  const EXTENSION_START_GAP = 0.075
@@ -21,6 +22,22 @@ const SQRT_ONE_HALF = Math.SQRT1_2
21
22
 
22
23
  type FloorplanDimensionRenderMode = 'screen' | 'pdf'
23
24
 
25
+ /**
26
+ * Click-to-type dimensions (WS3).
27
+ *
28
+ * On screen the label plate carries an invisible hit rect plus the data the
29
+ * editing layer needs to drive geometry from a typed value. The layer picks
30
+ * it up by DOM delegation (`FloorplanDimensionEditOverlay` below), so every
31
+ * producer of `dimension` / `dimension-string` geometry — wall automatic
32
+ * dimensions, contextual dimensions, manual construction-dimension nodes —
33
+ * becomes editable without each one wiring up a callback.
34
+ *
35
+ * Witness points are the measured feature points in level-plan metres; the
36
+ * value is the length the label actually reads (dimension-line points when
37
+ * the producer supplied them, witness points otherwise).
38
+ */
39
+ export const FLOORPLAN_DIMENSION_HIT_ATTRIBUTE = 'data-floorplan-dimension-hit'
40
+
24
41
  type DimensionGeometry = Extract<FloorplanGeometry, { kind: 'dimension' }>
25
42
  type DimensionStringGeometry = Extract<FloorplanGeometry, { kind: 'dimension-string' }>
26
43
  type DimensionTerminator = NonNullable<DimensionGeometry['terminator']>
@@ -289,10 +306,20 @@ export function FloorplanDimensionRenderer({
289
306
  data-floorplan-dimension-start-y={layout.dimensionStart[1]}
290
307
  data-floorplan-dimension-end-x={layout.dimensionEnd[0]}
291
308
  data-floorplan-dimension-end-y={layout.dimensionEnd[1]}
309
+ data-floorplan-dimension-text={geometry.text}
310
+ data-floorplan-dimension-value={dimensionLabelValue(layout)}
311
+ data-floorplan-dimension-witness-start-x={geometry.start[0]}
312
+ data-floorplan-dimension-witness-start-y={geometry.start[1]}
313
+ data-floorplan-dimension-witness-end-x={geometry.end[0]}
314
+ data-floorplan-dimension-witness-end-y={geometry.end[1]}
292
315
  transform={labelTransform}
293
316
  >
294
317
  <DimensionLabel
295
318
  fontSize={labelFontSize}
319
+ // `overridden` is declared on the geometry union in
320
+ // packages/core/src/registry/types.ts; the cast keeps this file
321
+ // compiling against a core dist built before that field landed.
322
+ overridden={(geometry as { overridden?: boolean }).overridden === true}
296
323
  renderMode={renderMode}
297
324
  stroke={stroke}
298
325
  text={geometry.text}
@@ -464,6 +491,12 @@ export function FloorplanDimensionStringRenderer({
464
491
  data-floorplan-dimension-start-y={layout.dimensionStart[1]}
465
492
  data-floorplan-dimension-end-x={layout.dimensionEnd[0]}
466
493
  data-floorplan-dimension-end-y={layout.dimensionEnd[1]}
494
+ data-floorplan-dimension-text={segment.text}
495
+ data-floorplan-dimension-value={dimensionLabelValue(layout)}
496
+ data-floorplan-dimension-witness-start-x={segment.start[0]}
497
+ data-floorplan-dimension-witness-start-y={segment.start[1]}
498
+ data-floorplan-dimension-witness-end-x={segment.end[0]}
499
+ data-floorplan-dimension-witness-end-y={segment.end[1]}
467
500
  transform={labelTransform}
468
501
  >
469
502
  <DimensionLabel
@@ -481,6 +514,14 @@ export function FloorplanDimensionStringRenderer({
481
514
  )
482
515
  }
483
516
 
517
+ /** The length the label reads — the dimension-line span, offset cancels out. */
518
+ function dimensionLabelValue(layout: ArchitecturalDimensionLayout): number {
519
+ return Math.hypot(
520
+ layout.dimensionEnd[0] - layout.dimensionStart[0],
521
+ layout.dimensionEnd[1] - layout.dimensionStart[1],
522
+ )
523
+ }
524
+
484
525
  function rotateVector(vector: FloorplanPoint, radians: number): FloorplanPoint {
485
526
  const cosine = Math.cos(radians)
486
527
  const sine = Math.sin(radians)
@@ -493,17 +534,21 @@ function DimensionLabel({
493
534
  fontSize,
494
535
  stroke,
495
536
  renderMode,
537
+ overridden = false,
496
538
  }: {
497
539
  text: string
498
540
  y: number
499
541
  fontSize: number
500
542
  stroke: string
501
543
  renderMode: FloorplanDimensionRenderMode
544
+ /** Draws the "override" badge when the value could not drive geometry. */
545
+ overridden?: boolean
502
546
  }) {
503
547
  const width = Math.max(fontSize, text.length * fontSize * LABEL_CHARACTER_WIDTH_RATIO)
504
548
  const plateWidth = width + fontSize * 0.5
505
549
  const plateHeight = fontSize * 1.2
506
550
  const plateY = y - fontSize * 0.82
551
+ const editable = useDimensionEditAllowed()
507
552
 
508
553
  return (
509
554
  <>
@@ -535,6 +580,38 @@ function DimensionLabel({
535
580
  >
536
581
  {text}
537
582
  </text>
583
+ {overridden && renderMode !== 'pdf' ? (
584
+ <text
585
+ fill={stroke}
586
+ fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
587
+ fontSize={fontSize * 0.62}
588
+ opacity={0.75}
589
+ paintOrder="stroke"
590
+ stroke="#ffffff"
591
+ strokeLinejoin="round"
592
+ strokeWidth={2}
593
+ textAnchor="middle"
594
+ vectorEffect="non-scaling-stroke"
595
+ x={0}
596
+ y={y + fontSize * 0.86}
597
+ >
598
+ override
599
+ </text>
600
+ ) : null}
601
+ {renderMode === 'pdf' || !editable ? null : (
602
+ // Invisible click target on the plate. `pointer-events` is inherited
603
+ // from the dimension group (`none`), so it is re-enabled here only.
604
+ <rect
605
+ {...{ [FLOORPLAN_DIMENSION_HIT_ATTRIBUTE]: '' }}
606
+ cursor="text"
607
+ fill="transparent"
608
+ height={plateHeight}
609
+ pointerEvents="all"
610
+ width={plateWidth}
611
+ x={-plateWidth / 2}
612
+ y={plateY}
613
+ />
614
+ )}
538
615
  </>
539
616
  )
540
617
  }