@pascal-app/editor 1.0.7 → 1.1.1

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 (336) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +101 -45
  5. package/src/components/editor/duplicate-node.ts +2 -1
  6. package/src/components/editor/editor-layout-v2.tsx +1 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +20 -1983
  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 +5 -3
  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 +8 -4
  15. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  16. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  17. package/src/components/editor/floorplan-panel.tsx +199 -30
  18. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  19. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  20. package/src/components/editor/follow-pascal.test.tsx +1 -2
  21. package/src/components/editor/follow-pascal.tsx +1 -1
  22. package/src/components/editor/footprint-height-handle.tsx +2 -2
  23. package/src/components/editor/group-actions.ts +1 -2
  24. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  25. package/src/components/editor/group-move-3d.ts +1 -2
  26. package/src/components/editor/group-rotate-handle.tsx +1 -2
  27. package/src/components/editor/group-transform-shared.ts +3 -1
  28. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  29. package/src/components/editor/index.tsx +43 -13
  30. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  31. package/src/components/editor/measurement-pill.tsx +1 -1
  32. package/src/components/editor/node-arrow-handles.tsx +2 -2
  33. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  34. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  35. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  36. package/src/components/editor/quick-measurement-card.tsx +1 -1
  37. package/src/components/editor/room-controls.tsx +10 -6
  38. package/src/components/editor/room-handles-2d.tsx +3 -3
  39. package/src/components/editor/room-handles.tsx +4 -5
  40. package/src/components/editor/room-highlight.tsx +6 -105
  41. package/src/components/editor/room-labels.test.ts +8 -822
  42. package/src/components/editor/room-labels.tsx +41 -1670
  43. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  44. package/src/components/editor/selection-manager.tsx +5 -3
  45. package/src/components/editor/site-edge-labels.tsx +7 -3
  46. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  47. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  48. package/src/components/editor/views/plugins-view.tsx +1 -1
  49. package/src/components/editor/views/view-bar.tsx +9 -9
  50. package/src/components/editor/wall-measurement-label.tsx +31 -15
  51. package/src/components/editor/wall-move-side-handles.tsx +11 -7
  52. package/src/components/editor/wall-pull-grip.tsx +2 -2
  53. package/src/components/editor/wall-region-handles.tsx +2 -2
  54. package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
  55. package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
  56. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
  57. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
  58. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  59. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
  60. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  61. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  62. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  63. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  64. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
  65. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  66. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +30 -10
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
  69. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  70. package/src/components/systems/roof/roof-edit-system.tsx +33 -21
  71. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  72. package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
  73. package/src/components/tools/item/move-tool.tsx +1 -6
  74. package/src/components/tools/item/placement-history.test.tsx +1 -1
  75. package/src/components/tools/item/use-placement-coordinator.tsx +3 -4
  76. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
  79. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  80. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  81. package/src/components/tools/shared/placement-box.tsx +2 -6
  82. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  83. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  84. package/src/components/tools/shared/segment-angle.ts +0 -4
  85. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  86. package/src/components/tools/tool-manager.tsx +1 -24
  87. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  88. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  89. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  90. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  91. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  92. package/src/components/ui/action-menu/index.tsx +3 -3
  93. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  94. package/src/components/ui/action-menu/placement.ts +0 -18
  95. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  96. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  97. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  98. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  99. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  100. package/src/components/ui/controls/select-control.tsx +1 -1
  101. package/src/components/ui/controls/slider-control.tsx +18 -6
  102. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  103. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  104. package/src/components/ui/floating-level-selector.tsx +3 -3
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  106. package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
  107. package/src/components/ui/helpers/helper-manager.tsx +55 -1
  108. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  109. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  110. package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
  111. package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
  112. package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
  113. package/src/components/ui/panels/multi-height-mode.tsx +3 -3
  114. package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
  115. package/src/components/ui/panels/node-display.ts +1 -1
  116. package/src/components/ui/panels/panel-manager.tsx +5 -3
  117. package/src/components/ui/panels/panel-wrapper.tsx +42 -19
  118. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  119. package/src/components/ui/panels/room-construction-rows.tsx +7 -7
  120. package/src/components/ui/panels/room-name-options.ts +1 -5
  121. package/src/components/ui/panels/room-openings-field.tsx +1 -1
  122. package/src/components/ui/panels/room-panel.tsx +7 -9
  123. package/src/components/ui/primitives/color-dot.tsx +2 -2
  124. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  125. package/src/components/ui/primitives/sidebar.tsx +3 -3
  126. package/src/components/ui/right-stack.test.ts +13 -6
  127. package/src/components/ui/right-stack.tsx +22 -11
  128. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  129. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  130. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  131. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  132. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  133. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  134. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
  135. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  136. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  137. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
  139. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  140. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  141. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  142. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  144. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  145. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  146. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
  147. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  148. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  149. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
  150. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  151. package/src/components/viewer-overlay.tsx +1 -2
  152. package/src/hooks/use-auto-frame.ts +1 -2
  153. package/src/hooks/use-keyboard.ts +1 -2
  154. package/src/hooks/use-selected-room.ts +5 -7
  155. package/src/i18n/floor-footprint-name.ts +1 -1
  156. package/src/i18n/locales/en/messages.po +1036 -306
  157. package/src/i18n/locales/en/messages.ts +1 -1
  158. package/src/i18n/locales/en-XA/messages.ts +1 -1
  159. package/src/i18n/locales/es/messages.po +2267 -439
  160. package/src/i18n/locales/fr/messages.po +2267 -439
  161. package/src/i18n/locales/ja/messages.po +2267 -439
  162. package/src/i18n/locales/zh-CN/messages.po +2267 -439
  163. package/src/i18n/locales.test.ts +1 -1
  164. package/src/index.tsx +183 -178
  165. package/src/lib/below-grade.test.tsx +4 -4
  166. package/src/lib/camera-facing.test.ts +1 -1
  167. package/src/lib/camera-facing.ts +2 -3
  168. package/src/lib/contextual-help.ts +6 -0
  169. package/src/lib/editor-level-display.ts +1 -1
  170. package/src/lib/elevation-guides.ts +21 -6
  171. package/src/lib/elevator-support.ts +1 -72
  172. package/src/lib/floor-footprints.test.ts +3 -3
  173. package/src/lib/floor-footprints.ts +1 -9
  174. package/src/lib/floor-foundation-support.test.ts +8 -3
  175. package/src/lib/floor-opening-draft.ts +2 -2
  176. package/src/lib/floor-region-session.ts +1 -1
  177. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  178. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  179. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  180. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  181. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  182. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  183. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  184. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  185. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  186. package/src/lib/floorplan/geometry.ts +2 -112
  187. package/src/lib/floorplan/index.ts +0 -9
  188. package/src/lib/floorplan/items.ts +2 -1
  189. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  190. package/src/lib/floorplan/schedules.ts +6 -3
  191. package/src/lib/floorplan/sheet-export.ts +7 -3
  192. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  193. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  194. package/src/lib/follow-pascal-presentation.ts +1 -1
  195. package/src/lib/gesture-lifecycle.ts +3 -1
  196. package/src/lib/glb-export.ts +1 -1
  197. package/src/lib/interaction/scope.ts +1 -0
  198. package/src/lib/layer-thickness.ts +1 -1
  199. package/src/lib/level-selection.test.ts +18 -2
  200. package/src/lib/level-selection.ts +31 -8
  201. package/src/lib/linear-display.ts +1 -1
  202. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  203. package/src/lib/material-paint.ts +1 -1
  204. package/src/lib/measurement-label.test.ts +1 -1
  205. package/src/lib/measurements.test.ts +7 -5
  206. package/src/lib/mezzanine-selection.test.ts +1 -1
  207. package/src/lib/node-action-movement.test.tsx +115 -0
  208. package/src/lib/node-action-movement.ts +2 -0
  209. package/src/lib/paint-region-mode.ts +1 -1
  210. package/src/lib/placement-metadata.ts +4 -0
  211. package/src/lib/point-ask/describe-target.ts +6 -8
  212. package/src/lib/point-ask/region.ts +1 -1
  213. package/src/lib/point-ask/session.ts +1 -2
  214. package/src/lib/print-shell-compiler.ts +12 -1
  215. package/src/lib/print-wall-solids.test.ts +11 -0
  216. package/src/lib/print-wall-solids.ts +9 -0
  217. package/src/lib/roof-duplication.test.ts +100 -0
  218. package/src/lib/roof-duplication.ts +30 -5
  219. package/src/lib/room-built-on.test.ts +2 -5
  220. package/src/lib/room-built-on.ts +1 -15
  221. package/src/lib/room-construction-commands.ts +3 -2
  222. package/src/lib/room-controls-phase6.test.ts +1 -1
  223. package/src/lib/room-handle-drag.ts +8 -37
  224. package/src/lib/room-name-catalog.test.ts +2 -1
  225. package/src/lib/room-name-messages.test.ts +2 -1
  226. package/src/lib/room-name-messages.ts +1 -118
  227. package/src/lib/room-name-storage.test.ts +2 -1
  228. package/src/lib/room-push-dimensions.test.ts +1 -1
  229. package/src/lib/room-selection-commands.ts +2 -2
  230. package/src/lib/room-selection.test.ts +6 -6
  231. package/src/lib/room-transform-session.ts +1 -2
  232. package/src/lib/room-zone-routing.ts +2 -2
  233. package/src/lib/scene-clipboard.ts +2 -1
  234. package/src/lib/selection-routing.test.ts +0 -28
  235. package/src/lib/selection-routing.ts +1 -40
  236. package/src/lib/sfx-bus.ts +8 -32
  237. package/src/lib/sfx-player.test.ts +72 -1
  238. package/src/lib/sfx-player.ts +49 -337
  239. package/src/lib/snapping-mode.test.ts +23 -1
  240. package/src/lib/viewer-selection.test.ts +38 -10
  241. package/src/lib/wall-pull.test.ts +1 -1
  242. package/src/lib/wall-pull.ts +1 -27
  243. package/src/lib/wall-region-list.ts +3 -1
  244. package/src/lib/window-interaction.test.ts +4 -2
  245. package/src/store/use-editor.tsx +22 -26
  246. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  247. package/src/store/use-placement-typing.test.ts +165 -0
  248. package/src/store/use-placement-typing.ts +186 -0
  249. package/src/store/use-wall-region-handles-selection.ts +1 -2
  250. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  251. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  252. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  253. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  254. package/src/components/editor/first-person/ground-holes.ts +0 -130
  255. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  256. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  257. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  258. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  259. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  260. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  261. package/src/components/editor/localized-html.tsx +0 -22
  262. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  263. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  264. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  265. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  266. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  267. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  268. package/src/components/editor-2d/svg-paths.ts +0 -124
  269. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  270. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  271. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  272. package/src/components/ui/action-menu/action-button.tsx +0 -89
  273. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  274. package/src/components/ui/primitives/button.tsx +0 -69
  275. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  276. package/src/components/ui/primitives/popover.tsx +0 -42
  277. package/src/components/ui/primitives/switch.tsx +0 -29
  278. package/src/components/ui/primitives/tooltip.tsx +0 -57
  279. package/src/components/ui/scene-loader.tsx +0 -73
  280. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  281. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  282. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  283. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  284. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  285. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  286. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  287. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  288. package/src/components/viewer/render-settings-menu.tsx +0 -294
  289. package/src/components/viewer/sky-control.tsx +0 -710
  290. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  291. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  292. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  293. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  294. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  295. package/src/components/viewer/viewer-rooms.tsx +0 -248
  296. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  297. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  298. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  299. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  300. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  301. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  302. package/src/components/viewer/viewer-stage.tsx +0 -221
  303. package/src/components/walkthrough-hud.tsx +0 -193
  304. package/src/hooks/use-mobile.ts +0 -19
  305. package/src/i18n/level-name.ts +0 -39
  306. package/src/i18n/scene-theme-name.ts +0 -16
  307. package/src/lib/below-grade.ts +0 -133
  308. package/src/lib/camera-pose.test.ts +0 -237
  309. package/src/lib/camera-pose.ts +0 -172
  310. package/src/lib/door-interaction.ts +0 -102
  311. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  312. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  313. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  314. package/src/lib/floorplan/marks.test.ts +0 -213
  315. package/src/lib/floorplan/marks.ts +0 -238
  316. package/src/lib/measurement-label.ts +0 -68
  317. package/src/lib/measurements.ts +0 -225
  318. package/src/lib/node-display-label.ts +0 -37
  319. package/src/lib/room-assembly-overlay.test.ts +0 -275
  320. package/src/lib/room-assembly-overlay.ts +0 -265
  321. package/src/lib/room-selection.ts +0 -417
  322. package/src/lib/scene-bounds.test.ts +0 -183
  323. package/src/lib/scene-bounds.ts +0 -169
  324. package/src/lib/typing-target.ts +0 -9
  325. package/src/lib/viewer-selection.ts +0 -595
  326. package/src/lib/window-interaction.ts +0 -142
  327. package/src/lib/zone-content.ts +0 -133
  328. package/src/store/camera-pose-store.test.ts +0 -29
  329. package/src/store/camera-pose-store.ts +0 -27
  330. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  331. package/src/store/navigation-sync-pose-store.ts +0 -27
  332. package/src/store/use-audio.test.ts +0 -146
  333. package/src/store/use-audio.tsx +0 -169
  334. package/src/store/use-first-person-hud.ts +0 -40
  335. package/src/store/use-follow-pascal.test.ts +0 -62
  336. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,68 +1,5 @@
1
- import type { Point2D } from '@pascal-app/core'
2
- import { getWallFaceOffsets, type WallJustification } from '@pascal-app/core'
3
- import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
4
-
5
- // Baseline rotation (deg) that orients the plan-local scene "north up" on
6
- // screen. The on-screen floor-plan scene `<g>` is rotated by
7
- // `FLOORPLAN_VIEW_ROTATION_DEG + userRotation - buildingRotation`; PDF export
8
- // uses the same user rotation so its orientation matches the live plan.
9
- //
10
- // North is world −Z: with a 0 baseline a rotation-0 reference image (top =
11
- // −Z) reads upright in the north-aligned view, and "align north" maps to a
12
- // 3D camera azimuth of 0. The old 90° baseline made north world −X, so
13
- // north-up floorplan scans displayed sideways whenever the compass claimed
14
- // the view was aligned.
15
- export const FLOORPLAN_VIEW_ROTATION_DEG = 0
16
-
17
- export function clampPlanValue(value: number, min: number, max: number) {
18
- return Math.min(Math.max(value, min), max)
19
- }
20
-
21
- export function rotatePlanVector(x: number, y: number, rotation: number): [number, number] {
22
- const cos = Math.cos(rotation)
23
- const sin = Math.sin(rotation)
24
- return [x * cos + y * sin, -x * sin + y * cos]
25
- }
26
-
27
- // Converts a world X/Z point into the floor-plan-local (building-local)
28
- // frame used by the SVG scene `<g>` and every stored node position. The
29
- // inverse of `floorplanLocalToWorldPoint`. Shared so the floor-plan panel
30
- // and the 2D move overlay resolve the same frame — feeding a world-space
31
- // `original` into a local-space cursor solver lands the drop off by the
32
- // building's world X/Z (worse for an off-origin building).
33
- export function worldToFloorplanLocalPoint(
34
- worldX: number,
35
- worldZ: number,
36
- buildingPosition: readonly [number, number, number],
37
- buildingRotationY: number,
38
- ): Point2D {
39
- const dx = worldX - buildingPosition[0]
40
- const dz = worldZ - buildingPosition[2]
41
- const cos = Math.cos(buildingRotationY)
42
- const sin = Math.sin(buildingRotationY)
43
-
44
- return {
45
- x: dx * cos - dz * sin,
46
- y: dx * sin + dz * cos,
47
- }
48
- }
49
-
50
- // Inverse of `worldToFloorplanLocalPoint`: floor-plan-local X/Y → world X/Z.
51
- export function floorplanLocalToWorldPoint(
52
- point: Point2D | [number, number],
53
- buildingPosition: readonly [number, number, number],
54
- buildingRotationY: number,
55
- ): { x: number; z: number } {
56
- const localX = Array.isArray(point) ? point[0] : point.x
57
- const localY = Array.isArray(point) ? point[1] : point.y
58
- const cos = Math.cos(buildingRotationY)
59
- const sin = Math.sin(buildingRotationY)
60
-
61
- return {
62
- x: buildingPosition[0] + localX * cos + localY * sin,
63
- z: buildingPosition[2] - localX * sin + localY * cos,
64
- }
65
- }
1
+ import { clampPlanValue, type Point2D, rotatePlanVector } from '@pascal-app/core'
2
+ import type { FloorplanSelectionBounds } from './types'
66
3
 
67
4
  export function getRotatedRectanglePolygon(
68
5
  center: Point2D,
@@ -88,53 +25,6 @@ export function getRotatedRectanglePolygon(
88
25
  })
89
26
  }
90
27
 
91
- export function interpolatePlanPoint(start: Point2D, end: Point2D, t: number): Point2D {
92
- return {
93
- x: start.x + (end.x - start.x) * t,
94
- y: start.y + (end.y - start.y) * t,
95
- }
96
- }
97
-
98
- export function getPlanPointDistance(start: Point2D, end: Point2D): number {
99
- return Math.hypot(end.x - start.x, end.y - start.y)
100
- }
101
-
102
- export function movePlanPointTowards(start: Point2D, end: Point2D, distance: number): Point2D {
103
- const totalDistance = getPlanPointDistance(start, end)
104
- if (totalDistance <= Number.EPSILON || distance <= 0) {
105
- return start
106
- }
107
-
108
- return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
109
- }
110
-
111
- export function getThickPlanLinePolygon(
112
- line: FloorplanLineSegment,
113
- thickness: number,
114
- justification?: WallJustification,
115
- ): Point2D[] {
116
- const dx = line.end.x - line.start.x
117
- const dy = line.end.y - line.start.y
118
- const length = Math.hypot(dx, dy)
119
-
120
- if (length <= Number.EPSILON || thickness <= 0) {
121
- return [line.start, line.end, line.end, line.start]
122
- }
123
-
124
- const { a, b } = getWallFaceOffsets({ thickness, justification })
125
- const normalX = (-dy / length) * a
126
- const normalY = (dx / length) * a
127
-
128
- const backX = (-dy / length) * -b
129
- const backY = (dx / length) * -b
130
- return [
131
- { x: line.start.x + normalX, y: line.start.y + normalY },
132
- { x: line.end.x + normalX, y: line.end.y + normalY },
133
- { x: line.end.x - backX, y: line.end.y - backY },
134
- { x: line.start.x - backX, y: line.start.y - backY },
135
- ]
136
- }
137
-
138
28
  export function getFloorplanSelectionBounds(
139
29
  start: [number, number],
140
30
  end: [number, number],
@@ -6,23 +6,14 @@ export {
6
6
  type FloorplanAlignmentResult,
7
7
  } from './apply-alignment'
8
8
  export {
9
- clampPlanValue,
10
9
  doesPolygonIntersectSelectionBounds,
11
- FLOORPLAN_VIEW_ROTATION_DEG,
12
- floorplanLocalToWorldPoint,
13
10
  getDistanceToWallSegment,
14
11
  getFloorplanSelectionBounds,
15
- getPlanPointDistance,
16
12
  getRotatedRectanglePolygon,
17
- getThickPlanLinePolygon,
18
- interpolatePlanPoint,
19
13
  isPointInsidePolygon,
20
14
  isPointInsidePolygonWithHoles,
21
15
  isPointInsideSelectionBounds,
22
- movePlanPointTowards,
23
16
  pointMatchesWallPlanPoint,
24
- rotatePlanVector,
25
- worldToFloorplanLocalPoint,
26
17
  } from './geometry'
27
18
  export {
28
19
  buildFloorplanItemEntry,
@@ -6,9 +6,10 @@ import {
6
6
  getWallLocalFaceZ,
7
7
  type ItemNode,
8
8
  type LevelNode,
9
+ rotatePlanVector,
9
10
  useLiveTransforms,
10
11
  } from '@pascal-app/core'
11
- import { getRotatedRectanglePolygon, rotatePlanVector } from './geometry'
12
+ import { getRotatedRectanglePolygon } from './geometry'
12
13
  import type { FloorplanItemEntry, FloorplanNodeTransform, LevelDescendantMap } from './types'
13
14
 
14
15
  export function collectLevelDescendants(
@@ -1,11 +1,12 @@
1
1
  import {
2
+ formatLinearMeasurement,
3
+ type LinearUnit,
2
4
  type OpenWallEnd,
3
5
  planJoinOpenWallEnd,
4
6
  runAsSingleSceneHistoryStep,
5
7
  useScene,
6
8
  type WallJoinResult,
7
9
  } from '@pascal-app/core'
8
- import { formatLinearMeasurement, type LinearUnit } from '../measurements'
9
10
 
10
11
  const JOIN_REFUSALS: Record<Extract<WallJoinResult, { ok: false }>['reason'], string> = {
11
12
  'covered-existing-wall': 'An existing wall already covers this span',
@@ -1,8 +1,11 @@
1
1
  import type { AnyNode, AnyNodeId, DoorNode, WallNode, WindowNode, ZoneNode } from '@pascal-app/core'
2
- import { deriveZoneQuantityReport, resolveHostedOpeningFrameDepth } from '@pascal-app/core'
2
+ import {
3
+ deriveZoneQuantityReport,
4
+ type FloorplanSchedule,
5
+ resolveHostedOpeningFrameDepth,
6
+ resolveMarkDetail,
7
+ } from '@pascal-app/core'
3
8
  import { collectFloorplanSchedules } from './floorplan-export'
4
- import type { FloorplanSchedule } from './floorplan-extension'
5
- import { resolveMarkDetail } from './marks'
6
9
 
7
10
  /**
8
11
  * Construction-document schedule DATA — WS3.
@@ -30,7 +30,11 @@ import type {
30
30
  FloorplanGeometry,
31
31
  LiveNodeOverrides,
32
32
  } from '@pascal-app/core'
33
- import { nodeRegistry, resolveBuildingForLevel } from '@pascal-app/core'
33
+ import {
34
+ type FloorplanMetricNotation,
35
+ nodeRegistry,
36
+ resolveBuildingForLevel,
37
+ } from '@pascal-app/core'
34
38
  import {
35
39
  buildContext,
36
40
  collectFloorplanLinkedLevelNodes,
@@ -52,14 +56,12 @@ import {
52
56
  resolveFloorplanExportRotationDeg,
53
57
  resolveFloorplanExportViewState,
54
58
  } from './floorplan-export'
55
- import type { FloorplanMetricNotation } from './floorplan-extension'
56
59
  import { createFloorplanPdfDocument } from './floorplan-pdfkit-document'
57
60
  import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
58
61
 
59
62
  /** PDF user-space units per inch. */
60
63
  export const POINTS_PER_INCH = 72
61
64
 
62
- export type { FloorplanSchedule } from './floorplan-extension'
63
65
  /**
64
66
  * The schedules the kinds on a level contribute
65
67
  * (`def.extensions['pascal:editor/floorplan'].schedule`). Re-exported here so
@@ -316,3 +318,5 @@ async function drawPlate(
316
318
  },
317
319
  )
318
320
  }
321
+
322
+ export type { FloorplanSchedule } from '@pascal-app/core'
@@ -15,7 +15,9 @@ import {
15
15
  isOutdoorRoomType,
16
16
  type LevelNode,
17
17
  migrateSiteMetadata,
18
+ type RoofNode,
18
19
  type RoofSegmentNode,
20
+ roofFaceShapes,
19
21
  type SceneSnapshot,
20
22
  type SiteNode,
21
23
  siteFrame,
@@ -75,7 +77,8 @@ export function findLowestLevel(
75
77
  /**
76
78
  * The roof's outline on the lot — every roof segment's plan rectangle out to
77
79
  * its deck's reach at its eaves and rakes, through the segment's, the roof's
78
- * and the building's turns, unioned into the outer ring(s). Dashed on the site plan: what the eye sees
80
+ * and the building's turns, and every face of another kind (`capabilities.roofs`)
81
+ * by its own outline, unioned into the outer ring(s). Dashed on the site plan: what the eye sees
79
82
  * from above is the roof, and the setback is measured to the wall under it.
80
83
  */
81
84
  export function roofOutlineRings(
@@ -93,6 +96,9 @@ export function roofOutlineRings(
93
96
  }
94
97
  if (roofs.size === 0) return []
95
98
  const rects: Pt[][] = []
99
+ for (const roof of roofs.values())
100
+ for (const face of roofFaceShapes(roof as RoofNode, scene.nodes))
101
+ rects.push(face.outline.map(([x, z]) => toSite(x, z)))
96
102
  for (const node of Object.values(scene.nodes)) {
97
103
  if (node.type !== 'roof-segment') continue
98
104
  const roof = roofs.get(String((node as { parentId?: unknown }).parentId))
@@ -7,8 +7,8 @@ import {
7
7
  resumeSceneHistory,
8
8
  useScene,
9
9
  } from '@pascal-app/core'
10
+ import { FloorplanGeometryRenderer } from '@pascal-app/viewer/ui'
10
11
  import { type PointerEvent as ReactPointerEvent, useCallback, useMemo, useRef } from 'react'
11
- import { FloorplanGeometryRenderer } from '../../../components/editor-2d/renderers/floorplan-geometry-renderer'
12
12
  import { clientToPlan } from '../plan-coords'
13
13
  import { buildSitePlanDrawing } from './build-site-plan-drawing'
14
14
 
@@ -1,5 +1,5 @@
1
1
  import type { WallMode } from '@pascal-app/viewer'
2
- import type { RoomKey } from './room-selection'
2
+ import type { RoomKey } from '@pascal-app/viewer/ui'
3
3
 
4
4
  /**
5
5
  * Follow Pascal changes how the building is shown so the build it follows is in view: the walls cut
@@ -33,7 +33,9 @@ export type GestureSpec = {
33
33
  kind: string
34
34
  /**
35
35
  * Drops the gesture's own in-flight state (draft, preview, listeners,
36
- * pending frames). Never writes the scene. Runs once, on cancel only.
36
+ * pending frames). Runs once, on cancel only. Undo and redo cancel before
37
+ * they jump, so this must record no history: a gesture that previews
38
+ * through scene writes under paused history reverts them here, then resumes.
37
39
  */
38
40
  onCancel: (reason: GestureCancelReason) => void
39
41
  /** A scope the owner begins for the gesture and releases only while it is still this one. */
@@ -32,6 +32,7 @@ import {
32
32
  type ViewerPresentationContribution,
33
33
  viewerPresentationRegistry,
34
34
  } from '@pascal-app/viewer'
35
+ import { nodeDisplayLabel } from '@pascal-app/viewer/ui'
35
36
  import type { Object3D } from 'three'
36
37
  import * as THREE from 'three'
37
38
  import {
@@ -42,7 +43,6 @@ import {
42
43
  import { createExportTextureUtils, type ExportTextureUtils } from './export-texture-utils'
43
44
  import { cloneExportUserData } from './export-user-data'
44
45
  import { isIfcRolePart } from './ifc-parts'
45
- import { nodeDisplayLabel } from './node-display-label'
46
46
  import {
47
47
  type CompressedTextureDecompressor,
48
48
  decompressCanonicalNormalMaps,
@@ -28,6 +28,7 @@ export type ReshapeKind =
28
28
  | 'tangent'
29
29
  // A kind's own reshape (wall 'split'): mounted from `def.affordanceTools[reshape]`.
30
30
  | 'split'
31
+ | 'trim'
31
32
 
32
33
  export type InteractionScope =
33
34
  | { kind: 'idle' }
@@ -1,4 +1,4 @@
1
- import type { LinearUnit } from './measurements'
1
+ import type { LinearUnit } from '@pascal-app/core'
2
2
 
3
3
  const METRES_PER_INCH = 0.0254
4
4
 
@@ -3,6 +3,7 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeId,
5
5
  BuildingNode,
6
+ ElevatorNode,
6
7
  LevelNode,
7
8
  RoofNode,
8
9
  useScene,
@@ -27,10 +28,14 @@ beforeEach(() => {
27
28
  metadata: { role: 'roof' },
28
29
  })
29
30
  const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
30
- const building = BuildingNode.parse({ id: BUILDING, children: [GROUND, roofLevel.id] })
31
+ const elevator = ElevatorNode.parse({ id: 'elevator_lift', parentId: BUILDING })
32
+ const building = BuildingNode.parse({
33
+ id: BUILDING,
34
+ children: [GROUND, roofLevel.id, elevator.id],
35
+ })
31
36
  useScene.setState({
32
37
  nodes: Object.fromEntries(
33
- [building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
38
+ [building, ground, wall, roofLevel, roof, elevator].map((node) => [node.id, node as AnyNode]),
34
39
  ),
35
40
  rootNodeIds: [BUILDING],
36
41
  } as never)
@@ -54,4 +59,15 @@ describe('selectPlacedNode', () => {
54
59
  selectedIds: ['wall_a'],
55
60
  })
56
61
  })
62
+
63
+ test('a building-parented node keeps the floor in view and adopts its building', () => {
64
+ useViewer.getState().resetSelection()
65
+ useViewer.getState().setSelection({ levelId: GROUND })
66
+ selectPlacedNode('elevator_lift' as AnyNodeId)
67
+ expect(useViewer.getState().selection).toMatchObject({
68
+ buildingId: BUILDING,
69
+ levelId: GROUND,
70
+ selectedIds: ['elevator_lift'],
71
+ })
72
+ })
57
73
  })
@@ -1,4 +1,4 @@
1
- import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
1
+ import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
2
2
  import { findLevelAncestorId, useScene } from '@pascal-app/core'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
 
@@ -30,17 +30,40 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
30
30
  }
31
31
  }
32
32
 
33
+ function findBuildingAncestorId(
34
+ nodeId: AnyNodeId,
35
+ nodes: Record<string, AnyNode>,
36
+ ): BuildingNode['id'] | null {
37
+ let current: AnyNode | undefined = nodes[nodeId]
38
+ for (let guard = 0; current && guard < 16; guard++) {
39
+ if (current.type === 'building') return current.id
40
+ current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
41
+ }
42
+ return null
43
+ }
44
+
33
45
  /**
34
46
  * Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
35
47
  * over a storey's walls lands on the level above, which the editor hides while the storey is
36
- * selected, so selecting it there alone would leave it out of sight.
48
+ * selected, so selecting it there alone would leave it out of sight. A node parented to the
49
+ * building itself (an elevator) has no level, so the floor in view stays active when it belongs
50
+ * to that building.
37
51
  */
38
52
  export function selectPlacedNode(nodeId: AnyNodeId) {
39
- const levelId = findLevelAncestorId(nodeId, useScene.getState().nodes)
53
+ const { nodes } = useScene.getState()
54
+ const levelId = findLevelAncestorId(nodeId, nodes) as LevelNode['id'] | null
55
+ const buildingId = findBuildingAncestorId(nodeId, nodes)
40
56
  const { selection, setSelection } = useViewer.getState()
41
- setSelection(
42
- levelId && levelId !== selection.levelId
43
- ? { levelId: levelId as LevelNode['id'], selectedIds: [nodeId] }
44
- : { selectedIds: [nodeId] },
45
- )
57
+ const updates: Partial<typeof selection> = { selectedIds: [nodeId] }
58
+ if (buildingId && buildingId !== selection.buildingId) {
59
+ updates.buildingId = buildingId
60
+ updates.levelId =
61
+ levelId ??
62
+ (selection.levelId && findBuildingAncestorId(selection.levelId, nodes) === buildingId
63
+ ? selection.levelId
64
+ : null)
65
+ } else if (levelId && levelId !== selection.levelId) {
66
+ updates.levelId = levelId
67
+ }
68
+ setSelection(updates)
46
69
  }
@@ -4,7 +4,7 @@ import {
4
4
  linearUnitToMeters,
5
5
  type MetricNotation,
6
6
  metersToLinearUnit,
7
- } from './measurements'
7
+ } from '@pascal-app/core'
8
8
 
9
9
  export function getLinearDisplay(
10
10
  unit: string,
@@ -6,6 +6,7 @@ import {
6
6
  createZoneDivisionContext,
7
7
  generateId,
8
8
  nodeRegistry,
9
+ roomFloorElevation,
9
10
  type SlabNode,
10
11
  useLiveNodeOverrides,
11
12
  useScene,
@@ -29,7 +30,6 @@ import { roomRelativeFloorHeight, stepRoomFloorElevation } from './room-construc
29
30
  import {
30
31
  commitRoomElevation,
31
32
  mezzanineEdgeHandles,
32
- roomFloorElevation,
33
33
  roomPushHandles,
34
34
  runFloorplanMezzanineEdge,
35
35
  runFloorplanRoomPush,
@@ -309,7 +309,7 @@ export function resolveActivePaintMaterialFromSelection(params: {
309
309
  ) {
310
310
  const roofNode = selectedNode as RoofNode
311
311
  const fallbackSegment = (roofNode.children ?? [])
312
- .map((id: AnyNodeId) => nodes[id as AnyNodeId] as RoofSegmentNode | undefined)
312
+ .map((id) => nodes[id as AnyNodeId] as RoofSegmentNode | undefined)
313
313
  .find(
314
314
  (segment: RoofSegmentNode | undefined) =>
315
315
  segment?.type === 'roof-segment' &&
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { measurementCentroid, pointInPolygon2D } from '@pascal-app/core'
3
- import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from './measurement-label'
3
+ import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from '@pascal-app/viewer'
4
4
 
5
5
  describe('measurementPolygonLabelAnchor', () => {
6
6
  test('keeps a concave polygon label inside its visible fill', () => {
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { measurementHint, parseMeasurement } from '../index'
3
2
  import {
4
3
  buildMeasurementAngleArcPoints,
5
4
  cubicMetersToVolumeUnit,
@@ -11,16 +10,19 @@ import {
11
10
  getVolumeUnitLabel,
12
11
  linearControlValueToMeters,
13
12
  linearUnitToMeters,
13
+ metersToLinearUnit,
14
+ parseLinearMeasurementInput,
15
+ squareMetersToAreaUnit,
16
+ } from '@pascal-app/core'
17
+ import {
14
18
  MEASUREMENT_ACTIVE_COLOR,
15
19
  MEASUREMENT_DANGLING_COLOR,
16
20
  MEASUREMENT_FLOORPLAN_COLOR,
17
21
  MEASUREMENT_PERSISTENT_COLOR,
18
22
  measurementFloorplanPresentationColor,
19
23
  measurementPresentationColor,
20
- metersToLinearUnit,
21
- parseLinearMeasurementInput,
22
- squareMetersToAreaUnit,
23
- } from './measurements'
24
+ } from '@pascal-app/viewer'
25
+ import { measurementHint, parseMeasurement } from '../index'
24
26
 
25
27
  describe('measurement presentation', () => {
26
28
  test('uses black at rest, indigo while active, and red for dangling references', () => {
@@ -15,6 +15,7 @@ import {
15
15
  type ZoneNode,
16
16
  } from '@pascal-app/core'
17
17
  import { useViewer } from '@pascal-app/viewer'
18
+ import { RoomSelectionIndex, resolveRoomHit } from '@pascal-app/viewer/ui'
18
19
  import { deleteConfirmationContent } from '../components/editor/delete-confirmation-dialog'
19
20
  import { resolveFloorRegionRoom } from '../components/editor/floor-region-controls'
20
21
  import { MEZZANINE_GESTURE_HINTS, mezzanineGesture } from '../components/ui/helpers/helper-manager'
@@ -23,7 +24,6 @@ import {
23
24
  mezzanineElevationBounds,
24
25
  ROOM_ELEVATION_DRAG_LABEL,
25
26
  } from './room-handle-drag'
26
- import { RoomSelectionIndex, resolveRoomHit } from './room-selection'
27
27
  import { applyRoomPlan } from './room-structure-commands'
28
28
  import { ROOM_MOVE_DRAG_LABEL } from './room-transform-session'
29
29
  import { zoneAtLevelPoint } from './units'
@@ -0,0 +1,115 @@
1
+ import { beforeAll, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ type AnyNodeId,
6
+ isMoveLocked,
7
+ isNodeDuplicable,
8
+ nodeRegistry,
9
+ registerNode,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import { z } from 'zod'
13
+ import { duplicateNodeAndPickUp } from '../components/editor/duplicate-node'
14
+ import { classifyParticipant } from '../components/editor/group-transform-shared'
15
+ import useEditor from '../store/use-editor'
16
+ import useInteractionScope from '../store/use-interaction-scope'
17
+ import { canDirectMoveNode } from './direct-manipulation'
18
+ import { createEditorApi } from './editor-api'
19
+ import { registryMoveDisabled } from './node-action-movement'
20
+ import { copySelectedNodesToEditorClipboard } from './scene-clipboard'
21
+
22
+ // Mirrors a lean-to post: a kind-level 2D mover plus per-node move and duplicate locks.
23
+ const KIND = 'move-lock-test'
24
+
25
+ beforeAll(() => {
26
+ if (nodeRegistry.has(KIND)) return
27
+ registerNode({
28
+ kind: KIND,
29
+ schemaVersion: 1,
30
+ schema: z.object({ type: z.literal(KIND) }) as never,
31
+ category: 'structure',
32
+ defaults: () => ({}),
33
+ capabilities: {
34
+ movable: {
35
+ axes: ['x', 'z'],
36
+ gridSnap: true,
37
+ override: ({ node }) =>
38
+ (node as { metadata?: { locked?: boolean } }).metadata?.locked
39
+ ? { axes: [], gridSnap: false }
40
+ : null,
41
+ },
42
+ duplicable: {
43
+ allow: ({ node }) => !(node as { metadata?: { locked?: boolean } }).metadata?.locked,
44
+ },
45
+ },
46
+ floorplanMoveTarget: () => ({
47
+ affectedIds: [],
48
+ apply() {},
49
+ canCommit: () => false,
50
+ commit() {},
51
+ }),
52
+ renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
53
+ } as AnyNodeDefinition)
54
+ })
55
+
56
+ function node(locked: boolean) {
57
+ return {
58
+ id: locked ? 'move_lock_locked' : 'move_lock_free',
59
+ type: KIND,
60
+ parentId: 'level_move_lock',
61
+ position: [1, 0, 2],
62
+ rotation: 0,
63
+ metadata: locked ? { locked: true } : {},
64
+ } as unknown as AnyNode
65
+ }
66
+
67
+ test('a per-node override lock outranks the kind-level 2D mover', () => {
68
+ expect(isMoveLocked(node(true))).toBe(true)
69
+ expect(isMoveLocked(node(false))).toBe(false)
70
+ expect(registryMoveDisabled(node(true))).toBe(true)
71
+ expect(registryMoveDisabled(node(false))).toBe(false)
72
+ expect(canDirectMoveNode(node(true))).toBe(false)
73
+ })
74
+
75
+ test('group pick-up leaves a locked node behind', () => {
76
+ const locked = node(true)
77
+ const free = node(false)
78
+ const nodes = { [locked.id]: locked, [free.id]: free }
79
+ expect(classifyParticipant(locked, 'level_move_lock', nodes)).toBeNull()
80
+ expect(classifyParticipant(free, 'level_move_lock', nodes)).toBe('scalar')
81
+ })
82
+
83
+ test('every moving-node entry refuses a locked node', () => {
84
+ const api = createEditorApi()
85
+ useEditor.getState().setMovingNode(node(true))
86
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
87
+ api.engageMoveDrag(node(true))
88
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
89
+ expect(useEditor.getState().placementDragMode).toBe(false)
90
+ api.engageMove(node(false))
91
+ expect(useInteractionScope.getState().scope.kind).toBe('moving')
92
+ useEditor.getState().setMovingNode(null)
93
+ })
94
+
95
+ test('a locked node cannot be duplicated or copied', () => {
96
+ const level = {
97
+ id: 'level_move_lock',
98
+ type: 'level',
99
+ parentId: null,
100
+ children: ['move_lock_locked', 'move_lock_free'],
101
+ } as unknown as AnyNode
102
+ const locked = node(true)
103
+ const free = node(false)
104
+ useScene.setState({
105
+ nodes: { [level.id]: level, [locked.id]: locked, [free.id]: free } as never,
106
+ rootNodeIds: [level.id as AnyNodeId],
107
+ })
108
+ expect(isNodeDuplicable(locked)).toBe(false)
109
+ expect(isNodeDuplicable(free)).toBe(true)
110
+ duplicateNodeAndPickUp(locked)
111
+ expect(Object.keys(useScene.getState().nodes)).toHaveLength(3)
112
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
113
+ expect(copySelectedNodesToEditorClipboard([locked.id as AnyNodeId])).toBe(false)
114
+ expect(copySelectedNodesToEditorClipboard([free.id as AnyNodeId])).toBe(true)
115
+ })
@@ -4,12 +4,14 @@ import {
4
4
  cloneNodesInto,
5
5
  collectSubtree,
6
6
  isMovable,
7
+ isMoveLocked,
7
8
  nodeRegistry,
8
9
  useScene,
9
10
  } from '@pascal-app/core'
10
11
  import { copyCreateOps } from './fresh-planar-placement'
11
12
 
12
13
  export function registryMoveDisabled(node: AnyNode): boolean {
14
+ if (isMoveLocked(node)) return true
13
15
  const def = nodeRegistry.get(node.type)
14
16
  return Boolean(
15
17
  def?.capabilities.movable &&
@@ -1,6 +1,6 @@
1
+ import { isTypingTarget } from '@pascal-app/viewer'
1
2
  import { create } from 'zustand'
2
3
  import type { ActivePaintMaterial } from './material-paint'
3
- import { isTypingTarget } from './typing-target'
4
4
 
5
5
  /**
6
6
  * What a paint click does: paint the whole surface under it, draw part of a
@@ -17,6 +17,10 @@ export function isFreshPlacementMetadata(metadata: unknown): boolean {
17
17
  return getPlacementMetadataRecord(metadata).isNew === true
18
18
  }
19
19
 
20
+ export function stripPlacementMetadataFlags(
21
+ metadata: Record<string, unknown>,
22
+ ): Record<string, unknown>
23
+ export function stripPlacementMetadataFlags(metadata: unknown): unknown
20
24
  export function stripPlacementMetadataFlags(metadata: unknown): unknown {
21
25
  if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
22
26
  return metadata