@pascal-app/editor 1.0.6 → 1.1.0

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 (382) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import { type AnyNodeId, useLiveNodeOverrides, useScene } from '@pascal-app/core'
2
+ import { type AnyNodeId, useLiveNodeOverrides, usePascalI18n, useScene } from '@pascal-app/core'
3
3
  import {
4
4
  ProceduralItemNode,
5
5
  snapParameters,
@@ -22,6 +22,7 @@ export function ProceduralInspector({
22
22
  partId: string | null
23
23
  onPartChange: (id: string | null) => void
24
24
  }) {
25
+ const i18n = usePascalI18n()
25
26
  const committed = useScene((s) => s.nodes[nodeId as AnyNodeId]) as unknown as
26
27
  | ProceduralItemNode
27
28
  | undefined
@@ -42,14 +43,24 @@ export function ProceduralInspector({
42
43
  const parameters = commit ? snapParameters(node.recipe, values) : values
43
44
  const parsed = ProceduralItemNode.safeParse({ ...node, parameters })
44
45
  if (!parsed.success) {
45
- setError(parsed.error.issues[0]?.message ?? 'Invalid edit')
46
+ setError(
47
+ parsed.error.issues[0]?.message ??
48
+ i18n._({ id: 'editor.proceduralItems.inspector.invalidEdit', message: 'Invalid edit' }),
49
+ )
46
50
  if (commit) useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
47
51
  return
48
52
  }
49
53
  try {
50
54
  validateProceduralRelations({ ...node, parameters }, useScene.getState().nodes)
51
55
  } catch (e) {
52
- setError(e instanceof Error ? e.message : 'Invalid attachment')
56
+ setError(
57
+ e instanceof Error
58
+ ? e.message
59
+ : i18n._({
60
+ id: 'editor.proceduralItems.inspector.invalidAttachment',
61
+ message: 'Invalid attachment',
62
+ }),
63
+ )
53
64
  if (commit) useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
54
65
  return
55
66
  }
@@ -64,7 +75,12 @@ export function ProceduralInspector({
64
75
  <div className="space-y-5">
65
76
  <div className="flex items-center justify-between">
66
77
  <h2 className="font-semibold text-lg">
67
- {partId ? node.recipe.parts.find((p) => p.id === partId)?.label : 'Adjust design'}
78
+ {partId
79
+ ? node.recipe.parts.find((p) => p.id === partId)?.label
80
+ : i18n._({
81
+ id: 'editor.proceduralItems.inspector.adjustDesign',
82
+ message: 'Adjust design',
83
+ })}
68
84
  </h2>
69
85
  {partId && (
70
86
  <button
@@ -72,7 +88,7 @@ export function ProceduralInspector({
72
88
  onClick={() => onPartChange(null)}
73
89
  type="button"
74
90
  >
75
- Back
91
+ {i18n._({ id: 'editor.proceduralItems.inspector.back', message: 'Back' })}
76
92
  </button>
77
93
  )}
78
94
  </div>
@@ -128,11 +144,21 @@ export function ProceduralInspector({
128
144
  useScene.getState().updateNode(nodeId as AnyNodeId, { parameters } as never)
129
145
  setError('')
130
146
  } catch (e) {
131
- setError(e instanceof Error ? e.message : 'Invalid reset')
147
+ setError(
148
+ e instanceof Error
149
+ ? e.message
150
+ : i18n._({
151
+ id: 'editor.proceduralItems.inspector.invalidReset',
152
+ message: 'Invalid reset',
153
+ }),
154
+ )
132
155
  }
133
156
  }}
134
157
  >
135
- Reset this part
158
+ {i18n._({
159
+ id: 'editor.proceduralItems.inspector.resetThisPart',
160
+ message: 'Reset this part',
161
+ })}
136
162
  </button>
137
163
  )}
138
164
  {error && (
@@ -142,7 +168,9 @@ export function ProceduralInspector({
142
168
  )}
143
169
  {!partId && node.recipe.parameters.some((p) => p.part) && (
144
170
  <div>
145
- <h3 className="mb-2 font-medium text-sm">Edit parts</h3>
171
+ <h3 className="mb-2 font-medium text-sm">
172
+ {i18n._({ id: 'editor.proceduralItems.inspector.editParts', message: 'Edit parts' })}
173
+ </h3>
146
174
  <div className="flex flex-wrap gap-2">
147
175
  {node.recipe.parts
148
176
  .filter((part) => node.recipe.parameters.some((p) => p.part === part.id))
@@ -188,13 +216,21 @@ function ProceduralPanel({ node }: { node: ProceduralItemNode }) {
188
216
  }
189
217
 
190
218
  function ProceduralPlacementControls({ node }: { node: ProceduralItemNode }) {
219
+ const i18n = usePascalI18n()
191
220
  const [error, setError] = useState('')
192
221
  const update = (patch: Partial<ProceduralItemNode>) => {
193
222
  try {
194
223
  useScene.getState().updateNode(node.id as AnyNodeId, patch as never)
195
224
  setError('')
196
225
  } catch (e) {
197
- setError(e instanceof Error ? e.message : 'Invalid placement')
226
+ setError(
227
+ e instanceof Error
228
+ ? e.message
229
+ : i18n._({
230
+ id: 'editor.proceduralItems.inspector.invalidPlacement',
231
+ message: 'Invalid placement',
232
+ }),
233
+ )
198
234
  }
199
235
  }
200
236
  const changePosition = (index: number, value: number, commit: boolean) => {
@@ -213,18 +249,29 @@ function ProceduralPlacementControls({ node }: { node: ProceduralItemNode }) {
213
249
  setError('')
214
250
  } catch (e) {
215
251
  if (commit) useLiveNodeOverrides.getState().clear(node.id as AnyNodeId)
216
- setError(e instanceof Error ? e.message : 'Invalid position')
252
+ setError(
253
+ e instanceof Error
254
+ ? e.message
255
+ : i18n._({
256
+ id: 'editor.proceduralItems.inspector.invalidPosition',
257
+ message: 'Invalid position',
258
+ }),
259
+ )
217
260
  }
218
261
  }
219
262
  return (
220
- <PanelSection title="Position">
263
+ <PanelSection
264
+ title={i18n._({ id: 'editor.proceduralItems.inspector.position', message: 'Position' })}
265
+ >
221
266
  {(['X', 'Y', 'Z'] as const).map((axis, index) => (
222
267
  <SliderControl
223
268
  key={axis}
224
269
  label={
225
270
  <>
226
271
  {axis}
227
- <sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
272
+ <sub className="ml-[1px] text-[11px] opacity-70">
273
+ {i18n._({ id: 'editor.proceduralItems.inspector.pos', message: 'pos' })}
274
+ </sub>
228
275
  </>
229
276
  }
230
277
  value={node.position[index]!}
@@ -29,6 +29,7 @@ import {
29
29
  type AttributesBackend = {
30
30
  createAttribute(): void
31
31
  updateAttribute(attribute: BufferAttribute): void
32
+ destroyAttribute(attribute: BufferAttribute): void
32
33
  }
33
34
  const Attributes = UntypedAttributes as new (
34
35
  backend: AttributesBackend,
@@ -371,6 +372,7 @@ test('WebGPU attribute updates skip resting frames and upload only written slots
371
372
  const attributes = new Attributes(
372
373
  {
373
374
  createAttribute() {},
375
+ destroyAttribute() {},
374
376
  updateAttribute(attribute: BufferAttribute) {
375
377
  uploads.push({ attribute, ranges: attribute.updateRanges.map((range) => ({ ...range })) })
376
378
  attribute.clearUpdateRanges()
@@ -0,0 +1,141 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { BuildingNode, CeilingNode, LevelNode, useScene } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { act, createRoot } from '@react-three/fiber'
5
+ import { createElement, Fragment } from 'react'
6
+ import { OrthographicCamera, type WebGLRenderer } from 'three'
7
+ import useEditor, { normalizePersistedEditorLayoutState } from '../../../store/use-editor'
8
+ import useInteractionScope from '../../../store/use-interaction-scope'
9
+ import { CeilingSystem, CeilingTopsSync } from './ceiling-system'
10
+
11
+ test('ceilings start on for the roof level and off for the floor plans, each kept as set', () => {
12
+ const fresh = normalizePersistedEditorLayoutState(undefined)
13
+ expect(fresh.showRoofCeilings).toBe(true)
14
+ expect(fresh.showCeilings).toBe(false)
15
+ const turned = normalizePersistedEditorLayoutState({
16
+ showRoofCeilings: false,
17
+ showCeilings: true,
18
+ })
19
+ expect(turned.showRoofCeilings).toBe(false)
20
+ expect(turned.showCeilings).toBe(true)
21
+ })
22
+
23
+ test('the level’s ceilings setting draws the ceiling tops, stepping aside to place on a ceiling', async () => {
24
+ const previous = {
25
+ scene: useScene.getState(),
26
+ viewer: useViewer.getState(),
27
+ editor: useEditor.getState(),
28
+ scope: useInteractionScope.getState(),
29
+ }
30
+ const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
31
+ const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
32
+ actGlobal.IS_REACT_ACT_ENVIRONMENT = true
33
+ const makeCanvas = () =>
34
+ Object.assign(new EventTarget(), {
35
+ getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
36
+ }) as unknown as HTMLCanvasElement
37
+ const canvas = makeCanvas()
38
+ const previewCanvas = makeCanvas()
39
+ const root = createRoot(canvas)
40
+ const previewRoot = createRoot(previewCanvas)
41
+ const building = BuildingNode.parse({})
42
+ const storey = LevelNode.parse({ parentId: building.id, level: 0, height: 3 })
43
+ const roof = LevelNode.parse({
44
+ parentId: building.id,
45
+ level: 1,
46
+ height: 3,
47
+ metadata: { role: 'roof' },
48
+ })
49
+ building.children = [storey.id, roof.id]
50
+ const ceiling = CeilingNode.parse({
51
+ parentId: storey.id,
52
+ height: 2.7,
53
+ polygon: [
54
+ [0, 0],
55
+ [4, 0],
56
+ [4, 4],
57
+ [0, 4],
58
+ ],
59
+ })
60
+ const select = (levelId: typeof storey.id) =>
61
+ act(async () =>
62
+ useViewer.setState({
63
+ selection: { buildingId: building.id, levelId, zoneId: null, selectedIds: [] },
64
+ }),
65
+ )
66
+ const tops = () => useViewer.getState().showCeilingTops
67
+ try {
68
+ useScene.setState({
69
+ nodes: {
70
+ [building.id]: building,
71
+ [storey.id]: storey,
72
+ [roof.id]: roof,
73
+ [ceiling.id]: ceiling,
74
+ },
75
+ })
76
+ useEditor.setState({
77
+ ...normalizePersistedEditorLayoutState(undefined),
78
+ tool: null,
79
+ selectedItem: null,
80
+ isPreviewMode: false,
81
+ isCaptureMode: false,
82
+ })
83
+ useInteractionScope.setState({ scope: { kind: 'idle' } })
84
+ useViewer.setState({ showCeilingTops: false, hoveredId: null })
85
+ await select(roof.id)
86
+ const configure = (target: typeof root, domElement: HTMLCanvasElement) =>
87
+ target.configure({
88
+ gl: {
89
+ domElement,
90
+ render() {},
91
+ setSize() {},
92
+ setPixelRatio() {},
93
+ } as unknown as WebGLRenderer,
94
+ camera: Object.assign(new OrthographicCamera(), { manual: true }),
95
+ frameloop: 'never',
96
+ dpr: 1,
97
+ size: { width: 100, height: 100, top: 0, left: 0 },
98
+ })
99
+ await configure(root, canvas)
100
+ await act(async () =>
101
+ root.render(
102
+ createElement(Fragment, null, createElement(CeilingTopsSync), createElement(CeilingSystem)),
103
+ ),
104
+ )
105
+ expect(tops()).toBe(true)
106
+
107
+ // A second viewer's ceiling system (the preview's) going away leaves the tops as set.
108
+ await configure(previewRoot, previewCanvas)
109
+ await act(async () => previewRoot.render(createElement(CeilingSystem)))
110
+ await act(async () => previewRoot.unmount())
111
+ expect(tops()).toBe(true)
112
+
113
+ await select(storey.id)
114
+ expect(tops()).toBe(false)
115
+ await act(async () => useEditor.getState().setShowCeilings(true))
116
+ expect(tops()).toBe(true)
117
+ await act(async () => useEditor.setState({ tool: 'ceiling' }))
118
+ expect(tops()).toBe(false)
119
+ await act(async () => useEditor.setState({ tool: null, isPreviewMode: true }))
120
+ expect(tops()).toBe(false)
121
+ await act(async () => useEditor.setState({ isPreviewMode: false }))
122
+ expect(tops()).toBe(true)
123
+
124
+ // The roof level keeps its own setting.
125
+ await select(roof.id)
126
+ await act(async () => useEditor.getState().setShowRoofCeilings(false))
127
+ expect(tops()).toBe(false)
128
+ expect(useEditor.getState().showCeilings).toBe(true)
129
+
130
+ await act(async () => useEditor.getState().setShowRoofCeilings(true))
131
+ expect(tops()).toBe(true)
132
+ await act(async () => root.unmount())
133
+ expect(tops()).toBe(false)
134
+ } finally {
135
+ useScene.setState(previous.scene, true)
136
+ useViewer.setState(previous.viewer, true)
137
+ useEditor.setState(previous.editor, true)
138
+ useInteractionScope.setState(previous.scope, true)
139
+ actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
140
+ }
141
+ })
@@ -1,4 +1,4 @@
1
- import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, sceneRegistry, servesAsRoofLevel, useScene } from '@pascal-app/core'
2
2
  import { isProceduralItem } from '@pascal-app/core/procedural-items'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
  import { useEffect } from 'react'
@@ -74,10 +74,59 @@ function setCeilingGridHighlighted(ceilingGrid: Mesh, highlighted: boolean) {
74
74
  delete userData[CEILING_GRID_HIGHLIGHT_MATERIAL_KEY]
75
75
  }
76
76
 
77
- export const CeilingSystem = () => {
77
+ /**
78
+ * The "Ceilings" setting that applies on `levelId`: the roof level's, on until turned off; the
79
+ * floor plans', off until turned on. On, ceilings read as solid from above too.
80
+ */
81
+ export function useCeilingsSetting(levelId: string | null | undefined): {
82
+ onRoofLevel: boolean
83
+ shown: boolean
84
+ setShown: (show: boolean) => void
85
+ } {
86
+ const onRoofLevel = useScene((state) => {
87
+ const level = levelId ? state.nodes[levelId as AnyNodeId] : undefined
88
+ return level?.type === 'level' && servesAsRoofLevel(state.nodes, level)
89
+ })
90
+ const shown = useEditor((state) => (onRoofLevel ? state.showRoofCeilings : state.showCeilings))
91
+ const setRoof = useEditor((state) => state.setShowRoofCeilings)
92
+ const setFloor = useEditor((state) => state.setShowCeilings)
93
+ return { onRoofLevel, shown, setShown: onRoofLevel ? setRoof : setFloor }
94
+ }
95
+
96
+ function useIsCeilingToolActive(): boolean {
78
97
  const tool = useEditor((state) => state.tool)
79
98
  const selectedItem = useEditor((state) => state.selectedItem)
80
99
  const movingNode = useMovingNode()
100
+ return (
101
+ tool === 'ceiling' ||
102
+ selectedItem?.attachTo === 'ceiling' ||
103
+ (movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling') ||
104
+ (isProceduralItem(movingNode) && movingNode.recipe.mounting?.attachTo === 'ceiling')
105
+ )
106
+ }
107
+
108
+ /**
109
+ * Whether the viewer draws ceiling tops (`showCeilingTops`), from the level's Ceilings setting.
110
+ * Mounted once by the editor, outside the canvases: the editor and preview viewers each mount a
111
+ * `CeilingSystem`, and two writers each turning the tops off on unmount raced.
112
+ */
113
+ export const CeilingTopsSync = () => {
114
+ const activeLevelId = useViewer((state) => state.selection.levelId)
115
+ const ceilingsShown = useCeilingsSetting(activeLevelId).shown
116
+ const isPreviewMode = useEditor((state) => state.isPreviewMode)
117
+ const isCaptureMode = useEditor((state) => state.isCaptureMode)
118
+ const isCeilingToolActive = useIsCeilingToolActive()
119
+ // Placing on a ceiling looks through it from above, so the tops step aside; preview and
120
+ // captures keep the open look.
121
+ const showTops = ceilingsShown && !isCeilingToolActive && !isPreviewMode && !isCaptureMode
122
+ useEffect(() => {
123
+ useViewer.getState().setShowCeilingTops(showTops)
124
+ return () => useViewer.getState().setShowCeilingTops(false)
125
+ }, [showTops])
126
+ return null
127
+ }
128
+
129
+ export const CeilingSystem = () => {
81
130
  const selectedIds = useViewer((state) => state.selection.selectedIds)
82
131
  const activeLevelId = useViewer((state) => state.selection.levelId)
83
132
  const hoveredId = useViewer((state) => state.hoveredId)
@@ -87,6 +136,7 @@ export const CeilingSystem = () => {
87
136
  // reveal is gated here, on the consumer.
88
137
  const inputDragging = useViewer((state) => state.inputDragging)
89
138
  const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
139
+ const isCeilingToolActive = useIsCeilingToolActive()
90
140
 
91
141
  useEffect(() => {
92
142
  const nodes = useScene.getState().nodes
@@ -96,12 +146,6 @@ export const CeilingSystem = () => {
96
146
 
97
147
  const levelsToShowCeilings = new Set<string>()
98
148
 
99
- const isCeilingToolActive =
100
- tool === 'ceiling' ||
101
- selectedItem?.attachTo === 'ceiling' ||
102
- (movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling') ||
103
- (isProceduralItem(movingNode) && movingNode.recipe.mounting?.attachTo === 'ceiling')
104
-
105
149
  if (isCeilingToolActive && activeLevelId) {
106
150
  levelsToShowCeilings.add(activeLevelId)
107
151
  }
@@ -162,15 +206,6 @@ export const CeilingSystem = () => {
162
206
  }
163
207
  }
164
208
  })
165
- }, [
166
- tool,
167
- selectedItem,
168
- movingNode,
169
- selectedIds,
170
- activeLevelId,
171
- hoveredId,
172
- scopeIdle,
173
- inputDragging,
174
- ])
209
+ }, [isCeilingToolActive, selectedIds, activeLevelId, hoveredId, scopeIdle, inputDragging])
175
210
  return null
176
211
  }
@@ -38,7 +38,6 @@ test('roof trim drag keeps its plane hit over a surface joining and leaving a ba
38
38
  id: 'rseg_batch_drag',
39
39
  width: 10,
40
40
  depth: 10,
41
- metadata: { showTrimPlanes: true },
42
41
  })
43
42
  const source = new THREE.Group()
44
43
  const surface = new THREE.Mesh(new THREE.BoxGeometry(30, 0.25, 30), new THREE.MeshBasicMaterial())
@@ -52,6 +51,9 @@ test('roof trim drag keeps its plane hit over a surface joining and leaving a ba
52
51
  hoveredId: null,
53
52
  selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [segment.id] },
54
53
  })
54
+ useInteractionScope
55
+ .getState()
56
+ .begin({ kind: 'reshaping', reshape: 'trim', nodeId: segment.id, driver: 'tool' })
55
57
  await root.configure({
56
58
  gl: {
57
59
  domElement: canvas,
@@ -99,6 +101,18 @@ test('roof trim drag keeps its plane hit over a surface joining and leaving a ba
99
101
  events.dispatchEvent(new Event('pointercancel'))
100
102
  })
101
103
  expect(useLiveNodeOverrides.getState().overrides.has(segment.id)).toBe(false)
104
+ const fieldEscape = Object.assign(new Event('keydown'), { key: 'Escape' })
105
+ Object.defineProperty(fieldEscape, 'target', {
106
+ value: { closest: () => ({ tagName: 'INPUT' }) },
107
+ })
108
+ await act(async () => {
109
+ events.dispatchEvent(fieldEscape)
110
+ })
111
+ expect(useInteractionScope.getState().scope.kind).toBe('reshaping')
112
+ await act(async () => {
113
+ events.dispatchEvent(Object.assign(new Event('keydown'), { key: 'Escape' }))
114
+ })
115
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
102
116
  } finally {
103
117
  await act(async () => {
104
118
  root.render(null)
@@ -34,6 +34,7 @@ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
34
34
  import { EDITOR_LAYER } from '../../../lib/constants'
35
35
  import { isHistoryShortcut } from '../../../lib/history'
36
36
  import { getHoveredRoofSegmentOutlineProxyName } from '../../../lib/roof-hover-outline-proxy'
37
+ import { isTypingTarget } from '../../../lib/typing-target'
37
38
  import useInteractionScope, { useMovingNode } from '../../../store/use-interaction-scope'
38
39
  import { swallowNextClick } from '../../editor/handles/use-handle-drag'
39
40
 
@@ -1006,15 +1007,6 @@ function getTrimCursor(side: RoofTrimSide): string {
1006
1007
  }
1007
1008
  }
1008
1009
 
1009
- function shouldShowTrimPlanes(metadata: unknown): boolean {
1010
- return (
1011
- typeof metadata === 'object' &&
1012
- metadata !== null &&
1013
- !Array.isArray(metadata) &&
1014
- (metadata as Record<string, unknown>).showTrimPlanes === true
1015
- )
1016
- }
1017
-
1018
1010
  function commitSegmentTrim(segment: RoofSegmentNode, trim: RoofSegmentTrim) {
1019
1011
  const scene = useScene.getState()
1020
1012
  scene.applyNodeChanges({
@@ -1031,6 +1023,28 @@ function RoofTrimHandles() {
1031
1023
  return node?.type === 'roof-segment' ? (node as RoofSegmentNode) : null
1032
1024
  })
1033
1025
  const readOnly = useScene((s) => s.readOnly)
1026
+ const showTrimPlanes = useInteractionScope(
1027
+ (s) =>
1028
+ s.scope.kind === 'reshaping' && s.scope.reshape === 'trim' && s.scope.nodeId === selectedId,
1029
+ )
1030
+ useEffect(() => {
1031
+ const end = () =>
1032
+ useInteractionScope
1033
+ .getState()
1034
+ .endIf(
1035
+ (scope) =>
1036
+ scope.kind === 'reshaping' && scope.reshape === 'trim' && scope.nodeId === selectedId,
1037
+ )
1038
+ const onKey = (event: KeyboardEvent) => {
1039
+ if (isTypingTarget(event.target)) return
1040
+ if (event.key === 'Escape') end()
1041
+ }
1042
+ window.addEventListener('keydown', onKey)
1043
+ return () => {
1044
+ window.removeEventListener('keydown', onKey)
1045
+ end()
1046
+ }
1047
+ }, [selectedId])
1034
1048
  const liveOverrideKey = useLiveNodeOverrides((s) =>
1035
1049
  segment ? JSON.stringify(s.overrides.get(segment.id) ?? null) : null,
1036
1050
  )
@@ -1042,7 +1056,10 @@ function RoofTrimHandles() {
1042
1056
  const zoom = camera instanceof THREE.OrthographicCamera ? 1 / camera.zoom : 1
1043
1057
  const handleBaseScale = zoom * TRIM_HANDLE_BASE_SCALE
1044
1058
 
1045
- useEffect(() => () => dragCleanupRef.current?.(), [])
1059
+ useEffect(() => {
1060
+ if (!selectedId || !showTrimPlanes || readOnly) dragCleanupRef.current?.()
1061
+ return () => dragCleanupRef.current?.()
1062
+ }, [selectedId, showTrimPlanes, readOnly])
1046
1063
 
1047
1064
  const liveSegment = useMemo(() => {
1048
1065
  if (!segment) return null
@@ -1059,8 +1076,6 @@ function RoofTrimHandles() {
1059
1076
  groupRef.current.matrixAutoUpdate = false
1060
1077
  })
1061
1078
 
1062
- const showTrimPlanes = shouldShowTrimPlanes(liveSegment?.metadata)
1063
-
1064
1079
  if (readOnly || !segment || !liveSegment || !showTrimPlanes) return null
1065
1080
 
1066
1081
  const trim = normalizeRoofSegmentTrim(liveSegment)
@@ -1314,9 +1329,7 @@ function RoofTrimHandles() {
1314
1329
 
1315
1330
  document.body.style.cursor = getTrimCursor(side)
1316
1331
  setDraggingSide(side)
1317
- useInteractionScope
1318
- .getState()
1319
- .begin({ kind: 'handle-drag', nodeId: segmentId, handle: getTrimLabel(side) })
1332
+
1320
1333
  useViewer.getState().setInputDragging(true)
1321
1334
  useScene.temporal.getState().pause()
1322
1335
 
@@ -1355,9 +1368,7 @@ function RoofTrimHandles() {
1355
1368
  document.body.style.cursor = ''
1356
1369
  }
1357
1370
  useScene.temporal.getState().resume()
1358
- useInteractionScope
1359
- .getState()
1360
- .endIf((scope) => scope.kind === 'handle-drag' && scope.nodeId === segmentId)
1371
+
1361
1372
  useViewer.getState().setInputDragging(false)
1362
1373
  setDraggingSide(null)
1363
1374
  dragCleanupRef.current = null
@@ -1393,7 +1404,7 @@ function RoofTrimHandles() {
1393
1404
  onCancel()
1394
1405
  }
1395
1406
 
1396
- dragCleanupRef.current = cleanup
1407
+ dragCleanupRef.current = onCancel
1397
1408
  window.addEventListener('pointermove', onMove)
1398
1409
  window.addEventListener('pointerup', onUp)
1399
1410
  window.addEventListener('pointercancel', onCancel)