@pascal-app/editor 1.1.0 → 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 (260) 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 +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  84. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  85. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  86. package/src/components/ui/panels/node-display.ts +1 -1
  87. package/src/components/ui/panels/panel-manager.tsx +1 -1
  88. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  89. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  90. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  91. package/src/components/ui/panels/room-name-options.ts +1 -5
  92. package/src/components/ui/panels/room-panel.tsx +6 -8
  93. package/src/components/ui/primitives/color-dot.tsx +2 -2
  94. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  95. package/src/components/ui/primitives/sidebar.tsx +3 -3
  96. package/src/components/ui/right-stack.tsx +1 -1
  97. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  98. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  99. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  100. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  104. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  107. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  109. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  110. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  112. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  114. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  115. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  116. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  117. package/src/components/viewer-overlay.tsx +1 -2
  118. package/src/hooks/use-auto-frame.ts +1 -2
  119. package/src/hooks/use-keyboard.ts +1 -2
  120. package/src/hooks/use-selected-room.ts +5 -7
  121. package/src/i18n/floor-footprint-name.ts +1 -1
  122. package/src/i18n/locales/en/messages.po +180 -182
  123. package/src/i18n/locales/es/messages.po +180 -182
  124. package/src/i18n/locales/fr/messages.po +180 -182
  125. package/src/i18n/locales/ja/messages.po +180 -182
  126. package/src/i18n/locales/zh-CN/messages.po +180 -182
  127. package/src/i18n/locales.test.ts +1 -1
  128. package/src/index.tsx +109 -131
  129. package/src/lib/below-grade.test.tsx +4 -4
  130. package/src/lib/camera-facing.test.ts +1 -1
  131. package/src/lib/camera-facing.ts +2 -3
  132. package/src/lib/editor-level-display.ts +1 -1
  133. package/src/lib/floor-footprints.test.ts +2 -2
  134. package/src/lib/floor-opening-draft.ts +1 -1
  135. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  136. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  137. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  138. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  139. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  140. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  141. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  143. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  144. package/src/lib/floorplan/geometry.ts +2 -112
  145. package/src/lib/floorplan/index.ts +0 -9
  146. package/src/lib/floorplan/items.ts +2 -1
  147. package/src/lib/floorplan/schedules.ts +6 -3
  148. package/src/lib/floorplan/sheet-export.ts +7 -3
  149. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  150. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  151. package/src/lib/follow-pascal-presentation.ts +1 -1
  152. package/src/lib/glb-export.ts +1 -1
  153. package/src/lib/material-paint.ts +1 -1
  154. package/src/lib/measurements.test.ts +2 -1
  155. package/src/lib/mezzanine-selection.test.ts +1 -1
  156. package/src/lib/paint-region-mode.ts +1 -1
  157. package/src/lib/point-ask/session.ts +1 -2
  158. package/src/lib/roof-duplication.test.ts +100 -0
  159. package/src/lib/roof-duplication.ts +30 -5
  160. package/src/lib/room-handle-drag.ts +2 -12
  161. package/src/lib/room-name-catalog.test.ts +2 -1
  162. package/src/lib/room-name-messages.test.ts +2 -1
  163. package/src/lib/room-name-messages.ts +1 -118
  164. package/src/lib/room-name-storage.test.ts +2 -1
  165. package/src/lib/room-selection-commands.ts +2 -2
  166. package/src/lib/room-selection.test.ts +6 -6
  167. package/src/lib/room-transform-session.ts +1 -2
  168. package/src/lib/room-zone-routing.ts +2 -2
  169. package/src/lib/selection-routing.test.ts +0 -28
  170. package/src/lib/selection-routing.ts +1 -40
  171. package/src/lib/sfx-bus.ts +8 -32
  172. package/src/lib/sfx-player.test.ts +72 -1
  173. package/src/lib/sfx-player.ts +49 -337
  174. package/src/lib/viewer-selection.test.ts +38 -10
  175. package/src/lib/wall-pull.test.ts +1 -1
  176. package/src/lib/wall-pull.ts +1 -27
  177. package/src/store/use-editor.tsx +17 -26
  178. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  179. package/src/store/use-wall-region-handles-selection.ts +1 -2
  180. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  181. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  182. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  183. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  184. package/src/components/editor/first-person/ground-holes.ts +0 -130
  185. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  186. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  187. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  188. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  189. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  190. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  191. package/src/components/editor/localized-html.tsx +0 -22
  192. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  193. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  194. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  195. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  196. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  197. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  198. package/src/components/editor-2d/svg-paths.ts +0 -124
  199. package/src/components/ui/action-menu/action-button.tsx +0 -89
  200. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  201. package/src/components/ui/primitives/button.tsx +0 -69
  202. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  203. package/src/components/ui/primitives/popover.tsx +0 -42
  204. package/src/components/ui/primitives/switch.tsx +0 -29
  205. package/src/components/ui/primitives/tooltip.tsx +0 -57
  206. package/src/components/ui/scene-loader.tsx +0 -73
  207. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  208. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  209. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  210. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  211. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  212. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  213. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  214. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  215. package/src/components/viewer/render-settings-menu.tsx +0 -294
  216. package/src/components/viewer/sky-control.tsx +0 -710
  217. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  218. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  219. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  220. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  221. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  222. package/src/components/viewer/viewer-rooms.tsx +0 -248
  223. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  224. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  225. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  226. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  227. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  228. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  229. package/src/components/viewer/viewer-stage.tsx +0 -221
  230. package/src/components/walkthrough-hud.tsx +0 -193
  231. package/src/hooks/use-mobile.ts +0 -19
  232. package/src/i18n/level-name.ts +0 -39
  233. package/src/i18n/scene-theme-name.ts +0 -16
  234. package/src/lib/below-grade.ts +0 -133
  235. package/src/lib/camera-pose.test.ts +0 -237
  236. package/src/lib/camera-pose.ts +0 -172
  237. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  238. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  239. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  240. package/src/lib/floorplan/marks.test.ts +0 -213
  241. package/src/lib/floorplan/marks.ts +0 -238
  242. package/src/lib/measurements.ts +0 -8
  243. package/src/lib/node-display-label.ts +0 -37
  244. package/src/lib/room-assembly-overlay.test.ts +0 -275
  245. package/src/lib/room-assembly-overlay.ts +0 -265
  246. package/src/lib/room-selection.ts +0 -417
  247. package/src/lib/scene-bounds.test.ts +0 -327
  248. package/src/lib/scene-bounds.ts +0 -233
  249. package/src/lib/typing-target.ts +0 -9
  250. package/src/lib/viewer-selection.ts +0 -595
  251. package/src/lib/zone-content.ts +0 -133
  252. package/src/store/camera-pose-store.test.ts +0 -29
  253. package/src/store/camera-pose-store.ts +0 -27
  254. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  255. package/src/store/navigation-sync-pose-store.ts +0 -27
  256. package/src/store/use-audio.test.ts +0 -146
  257. package/src/store/use-audio.tsx +0 -169
  258. package/src/store/use-first-person-hud.ts +0 -40
  259. package/src/store/use-follow-pascal.test.ts +0 -62
  260. package/src/store/use-follow-pascal.ts +0 -53
@@ -0,0 +1,100 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ type AnyNodeId,
6
+ BaseNode,
7
+ LevelNode,
8
+ nodeRegistry,
9
+ nodeType,
10
+ objectId,
11
+ RoofNode,
12
+ RoofSegmentNode,
13
+ registerNode,
14
+ useScene,
15
+ } from '@pascal-app/core'
16
+ import { useViewer } from '@pascal-app/viewer'
17
+ import { z } from 'zod'
18
+ import { duplicateRoofSubtree } from './roof-duplication'
19
+
20
+ // A plugin's solved roof plane: a face of the roof holding it (`capabilities.roofs`).
21
+ const RoofPlane = BaseNode.extend({
22
+ id: objectId('roofplane'),
23
+ type: nodeType('test:roof-plane'),
24
+ polygon: z.array(z.tuple([z.number(), z.number()])),
25
+ })
26
+ const roofPlanes = {
27
+ kind: 'test:roof-plane',
28
+ schemaVersion: 1,
29
+ schema: RoofPlane,
30
+ category: 'structure',
31
+ defaults: () => ({}),
32
+ capabilities: {
33
+ roofs: {
34
+ outline: (node: AnyNode) => (node as unknown as z.infer<typeof RoofPlane>).polygon,
35
+ heightAt: () => 2.9,
36
+ },
37
+ },
38
+ } as unknown as AnyNodeDefinition
39
+
40
+ const LEVEL_ID = 'level_roof-duplicate' as AnyNodeId
41
+ const ROOF_ID = 'roof_original' as AnyNodeId
42
+ const SEGMENT_ID = 'rseg_original' as AnyNodeId
43
+ const PLANE_ID = 'roofplane_original' as AnyNodeId
44
+ const POLYGON: [number, number][] = [
45
+ [0, 0],
46
+ [8, 0],
47
+ [8, 3],
48
+ ]
49
+
50
+ let restoreRegistry = () => {}
51
+ const previousScene = useScene.getState()
52
+
53
+ beforeEach(() => {
54
+ restoreRegistry = nodeRegistry._snapshot()
55
+ registerNode(roofPlanes)
56
+ useScene.setState({
57
+ nodes: {
58
+ [LEVEL_ID]: LevelNode.parse({ id: LEVEL_ID, children: [ROOF_ID] }) as AnyNode,
59
+ [ROOF_ID]: RoofNode.parse({
60
+ id: ROOF_ID,
61
+ parentId: LEVEL_ID,
62
+ children: [SEGMENT_ID, PLANE_ID],
63
+ }) as AnyNode,
64
+ [SEGMENT_ID]: RoofSegmentNode.parse({ id: SEGMENT_ID, parentId: ROOF_ID }) as AnyNode,
65
+ [PLANE_ID]: RoofPlane.parse({
66
+ id: PLANE_ID,
67
+ parentId: ROOF_ID,
68
+ polygon: POLYGON,
69
+ }) as unknown as AnyNode,
70
+ },
71
+ rootNodeIds: [LEVEL_ID],
72
+ collections: {},
73
+ dirtyNodes: new Set(),
74
+ } as never)
75
+ })
76
+
77
+ afterEach(() => {
78
+ restoreRegistry()
79
+ useViewer.getState().setSelection({ selectedIds: [] })
80
+ useScene.setState(previousScene)
81
+ useScene.temporal.getState().clear()
82
+ })
83
+
84
+ describe('duplicateRoofSubtree', () => {
85
+ test("copies the roof's faces of other kinds with it, as they stand in its frame", () => {
86
+ const { roof } = duplicateRoofSubtree(ROOF_ID, { mode: 'select' })
87
+ const nodes = useScene.getState().nodes
88
+ const copied = (nodes[roof.id as AnyNodeId] as RoofNode).children.map(
89
+ (id) => nodes[id as AnyNodeId]!,
90
+ )
91
+
92
+ expect(copied.map((node): string => node.type)).toEqual(['roof-segment', 'test:roof-plane'])
93
+ const plane = copied[1] as unknown as z.infer<typeof RoofPlane>
94
+ expect(plane.id).not.toBe(PLANE_ID)
95
+ expect(plane.id.startsWith('roofplane_')).toBe(true)
96
+ expect(plane.parentId).toBe(roof.id)
97
+ expect(plane.polygon).toEqual(POLYGON)
98
+ expect((nodes[ROOF_ID] as RoofNode).children).toEqual([SEGMENT_ID, PLANE_ID])
99
+ })
100
+ })
@@ -1,13 +1,16 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNode,
4
5
  type AnyNodeId,
5
6
  collectionIdsOf,
6
7
  generateId,
8
+ nodeRegistry,
7
9
  type RoofNode,
8
10
  RoofNode as RoofNodeSchema,
9
11
  type RoofSegmentNode,
10
12
  RoofSegmentNode as RoofSegmentNodeSchema,
13
+ roofFaceOf,
11
14
  sceneRegistry,
12
15
  useScene,
13
16
  } from '@pascal-app/core'
@@ -121,6 +124,23 @@ export function duplicateRoofSubtree(
121
124
  segmentSourceIds.push(childNode.id)
122
125
  }
123
126
 
127
+ // Its faces of other kinds (`capabilities.roofs`) are copied by their own schema, in the copy's
128
+ // frame as in the source's; a face that does not parse fails the copy, as a segment does.
129
+ const faceClones: { node: AnyNode; sourceId: AnyNodeId }[] = []
130
+ for (const childId of sourceRoof.children ?? []) {
131
+ const childNode = scene.nodes[childId as AnyNodeId]
132
+ const schema =
133
+ childNode && roofFaceOf(childNode) ? nodeRegistry.get(childNode.type)?.schema : undefined
134
+ if (!(childNode && schema)) continue
135
+ const { id: _id, ...fields } = structuredClone(childNode)
136
+ const node = schema.parse({
137
+ ...fields,
138
+ parentId: roofClone.id,
139
+ metadata: buildDuplicateMetadata(childNode.metadata),
140
+ }) as AnyNode
141
+ faceClones.push({ node, sourceId: childNode.id })
142
+ }
143
+
124
144
  // Each copy is in the collections its source is in.
125
145
  scene.createNodes([
126
146
  { node: roofClone, parentId, collectionIds: collectionIdsOf(scene.collections, sourceRoofId) },
@@ -129,6 +149,11 @@ export function duplicateRoofSubtree(
129
149
  parentId: roofClone.id as AnyNodeId,
130
150
  collectionIds: collectionIdsOf(scene.collections, segmentSourceIds[index]!),
131
151
  })),
152
+ ...faceClones.map(({ node, sourceId }) => ({
153
+ node,
154
+ parentId: roofClone.id as AnyNodeId,
155
+ collectionIds: collectionIdsOf(scene.collections, sourceId),
156
+ })),
132
157
  ])
133
158
 
134
159
  const nextScene = useScene.getState()
@@ -148,12 +173,12 @@ export function duplicateRoofSubtree(
148
173
 
149
174
  const segmentIds = segmentClones.map((segment) => segment.id)
150
175
  const createdChildIds = (createdRoof.children ?? []) as AnyNodeId[]
151
- const missingSegmentId = segmentIds.find(
152
- (segmentId) => !createdChildIds.includes(segmentId as AnyNodeId),
176
+ const missingChildId = [...segmentIds, ...faceClones.map(({ node }) => node.id)].find(
177
+ (childId) => !createdChildIds.includes(childId as AnyNodeId),
153
178
  )
154
- if (missingSegmentId) {
179
+ if (missingChildId) {
155
180
  throw new Error(
156
- `Duplicated roof "${createdRoof.id}" is missing cloned segment "${missingSegmentId}"`,
181
+ `Duplicated roof "${createdRoof.id}" is missing cloned child "${missingChildId}"`,
157
182
  )
158
183
  }
159
184
 
@@ -206,7 +231,7 @@ export function clearRoofDuplicateMetadata(
206
231
 
207
232
  for (const childId of roofNode.children ?? []) {
208
233
  const childNode = scene.nodes[childId as AnyNodeId]
209
- if (childNode?.type !== 'roof-segment') {
234
+ if (!childNode) {
210
235
  continue
211
236
  }
212
237
 
@@ -34,8 +34,9 @@ import {
34
34
  useLiveNodeOverrides,
35
35
  useScene,
36
36
  type WallNode,
37
+ type WallPushArrowPayload,
37
38
  } from '@pascal-app/core'
38
- import { useViewer } from '@pascal-app/viewer'
39
+ import { isTypingTarget, useViewer } from '@pascal-app/viewer'
39
40
  import type { Ray } from 'three'
40
41
  import { create } from 'zustand'
41
42
  import { ARROW_COLOR } from '../components/editor/handles/handle-arrow'
@@ -64,7 +65,6 @@ import { applyRoomPlan } from './room-structure-commands'
64
65
  import { levelSeparatorLines, separatorWallLine } from './separator-lines'
65
66
  import { sfxEmitter } from './sfx-bus'
66
67
  import { type SpatialPointerId, spatialPointerInput } from './spatial-pointer-input'
67
- import { isTypingTarget } from './typing-target'
68
68
  import {
69
69
  nearestWallParallelTarget,
70
70
  publishWallParallelGuide,
@@ -1469,16 +1469,6 @@ export function wallPointAt(wall: WallNode, t: number): Point {
1469
1469
  return [frame.point.x, frame.point.y]
1470
1470
  }
1471
1471
 
1472
- /**
1473
- * The affordance a floor-plan `move-arrow` carries to be a wall push arrow;
1474
- * its payload is a `WallPushArrowPayload`. The floor plan starts
1475
- * `runFloorplanWallPush` for it instead of a registry affordance session.
1476
- */
1477
- export const WALL_PUSH_AFFORDANCE = 'wall-push'
1478
-
1479
- /** A wall's or an invisible wall's side arrow: `wallId` is either. */
1480
- export type WallPushArrowPayload = { wallId: string; side: 'a' | 'b' }
1481
-
1482
1472
  type PlanPointer = (clientX: number, clientY: number) => readonly [number, number] | null
1483
1473
 
1484
1474
  /** The pointer's plan position measured along an arrow's outward normal from its spot. */
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { type AnyNode, ZoneNode } from '@pascal-app/core'
3
+ import { ROOM_NAME_MESSAGES } from '@pascal-app/viewer/ui'
3
4
  import {
4
5
  MEZZANINE_NAME_SECTION,
5
6
  numberedRoomName,
@@ -8,7 +9,7 @@ import {
8
9
  roomNameSections,
9
10
  roomNameUseCount,
10
11
  } from './room-name-catalog'
11
- import { ROOM_NAME_MESSAGES, ROOM_NAME_SECTION_TITLES } from './room-name-messages'
12
+ import { ROOM_NAME_SECTION_TITLES } from './room-name-messages'
12
13
 
13
14
  const square: [number, number][] = [
14
15
  [0, 0],
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { type AnyNode, type MessageDescriptor, ZoneNode } from '@pascal-app/core'
3
+ import { displayRoomName, editedRoomName } from '@pascal-app/viewer/ui'
3
4
  import { roomNameOptions } from '../components/ui/panels/room-name-options'
4
5
  import { roomNameSections } from './room-name-catalog'
5
- import { displayRoomName, editedRoomName, localizedRoomNameSections } from './room-name-messages'
6
+ import { localizedRoomNameSections } from './room-name-messages'
6
7
 
7
8
  const FRENCH: Record<string, string> = {
8
9
  'editor.roomName.bedroom': 'Chambre',
@@ -1,14 +1,7 @@
1
1
  import type { MessageDescriptor } from '@pascal-app/core'
2
+ import { type DescriptorTranslator, displayRoomName } from '@pascal-app/viewer/ui'
2
3
  import type { RoomNameSection } from './room-name-catalog'
3
4
 
4
- /**
5
- * The room-name catalog in the UI language. The catalog stays English (it is
6
- * matched, numbered and stored as data); the combobox and room labels show these.
7
- * `room-name-catalog.test.ts` fails when a catalog name has no entry here.
8
- */
9
- /** Anything that translates a descriptor: a `PascalI18n`, or a stand-in in tests. */
10
- export type DescriptorTranslator = { _(descriptor: MessageDescriptor): string }
11
-
12
5
  export const ROOM_NAME_SECTION_TITLES: Record<string, MessageDescriptor> = {
13
6
  living: /* i18n */ { id: 'editor.roomName.section.living', message: 'Living' },
14
7
  sleeping: /* i18n */ { id: 'editor.roomName.section.sleeping', message: 'Sleeping' },
@@ -20,72 +13,6 @@ export const ROOM_NAME_SECTION_TITLES: Record<string, MessageDescriptor> = {
20
13
  mezzanine: /* i18n */ { id: 'editor.roomName.section.mezzanine', message: 'Mezzanine' },
21
14
  }
22
15
 
23
- export const ROOM_NAME_MESSAGES: Record<string, MessageDescriptor> = {
24
- 'Living room': /* i18n */ { id: 'editor.roomName.livingRoom', message: 'Living room' },
25
- 'Family room': /* i18n */ { id: 'editor.roomName.familyRoom', message: 'Family room' },
26
- 'Dining room': /* i18n */ { id: 'editor.roomName.diningRoom', message: 'Dining room' },
27
- Kitchen: /* i18n */ { id: 'editor.roomName.kitchen', message: 'Kitchen' },
28
- Kitchenette: /* i18n */ { id: 'editor.roomName.kitchenette', message: 'Kitchenette' },
29
- Pantry: /* i18n */ { id: 'editor.roomName.pantry', message: 'Pantry' },
30
- Lounge: /* i18n */ { id: 'editor.roomName.lounge', message: 'Lounge' },
31
- Den: /* i18n */ { id: 'editor.roomName.den', message: 'Den' },
32
- Library: /* i18n */ { id: 'editor.roomName.library', message: 'Library' },
33
- Playroom: /* i18n */ { id: 'editor.roomName.playroom', message: 'Playroom' },
34
- 'Home cinema': /* i18n */ { id: 'editor.roomName.homeCinema', message: 'Home cinema' },
35
- 'Games room': /* i18n */ { id: 'editor.roomName.gamesRoom', message: 'Games room' },
36
- Sunroom: /* i18n */ { id: 'editor.roomName.sunroom', message: 'Sunroom' },
37
- Bedroom: /* i18n */ { id: 'editor.roomName.bedroom', message: 'Bedroom' },
38
- 'Primary bedroom': /* i18n */ {
39
- id: 'editor.roomName.primaryBedroom',
40
- message: 'Primary bedroom',
41
- },
42
- 'Guest room': /* i18n */ { id: 'editor.roomName.guestRoom', message: 'Guest room' },
43
- "Kids' room": /* i18n */ { id: 'editor.roomName.kidsRoom', message: "Kids' room" },
44
- Nursery: /* i18n */ { id: 'editor.roomName.nursery', message: 'Nursery' },
45
- 'Dressing room': /* i18n */ { id: 'editor.roomName.dressingRoom', message: 'Dressing room' },
46
- 'Walk-in closet': /* i18n */ { id: 'editor.roomName.walkInCloset', message: 'Walk-in closet' },
47
- Bathroom: /* i18n */ { id: 'editor.roomName.bathroom', message: 'Bathroom' },
48
- 'En-suite': /* i18n */ { id: 'editor.roomName.enSuite', message: 'En-suite' },
49
- 'Shower room': /* i18n */ { id: 'editor.roomName.showerRoom', message: 'Shower room' },
50
- Toilet: /* i18n */ { id: 'editor.roomName.toilet', message: 'Toilet' },
51
- 'Powder room': /* i18n */ { id: 'editor.roomName.powderRoom', message: 'Powder room' },
52
- Office: /* i18n */ { id: 'editor.roomName.office', message: 'Office' },
53
- Study: /* i18n */ { id: 'editor.roomName.study', message: 'Study' },
54
- Studio: /* i18n */ { id: 'editor.roomName.studio', message: 'Studio' },
55
- Workshop: /* i18n */ { id: 'editor.roomName.workshop', message: 'Workshop' },
56
- Gym: /* i18n */ { id: 'editor.roomName.gym', message: 'Gym' },
57
- Entrance: /* i18n */ { id: 'editor.roomName.entrance', message: 'Entrance' },
58
- Hallway: /* i18n */ { id: 'editor.roomName.hallway', message: 'Hallway' },
59
- Corridor: /* i18n */ { id: 'editor.roomName.corridor', message: 'Corridor' },
60
- Landing: /* i18n */ { id: 'editor.roomName.landing', message: 'Landing' },
61
- Stairwell: /* i18n */ { id: 'editor.roomName.stairwell', message: 'Stairwell' },
62
- Mudroom: /* i18n */ { id: 'editor.roomName.mudroom', message: 'Mudroom' },
63
- Laundry: /* i18n */ { id: 'editor.roomName.laundry', message: 'Laundry' },
64
- 'Utility room': /* i18n */ { id: 'editor.roomName.utilityRoom', message: 'Utility room' },
65
- Storage: /* i18n */ { id: 'editor.roomName.storage', message: 'Storage' },
66
- Closet: /* i18n */ { id: 'editor.roomName.closet', message: 'Closet' },
67
- 'Boiler room': /* i18n */ { id: 'editor.roomName.boilerRoom', message: 'Boiler room' },
68
- Cellar: /* i18n */ { id: 'editor.roomName.cellar', message: 'Cellar' },
69
- Basement: /* i18n */ { id: 'editor.roomName.basement', message: 'Basement' },
70
- Attic: /* i18n */ { id: 'editor.roomName.attic', message: 'Attic' },
71
- Garage: /* i18n */ { id: 'editor.roomName.garage', message: 'Garage' },
72
- Garden: /* i18n */ { id: 'editor.roomName.garden', message: 'Garden' },
73
- Patio: /* i18n */ { id: 'editor.roomName.patio', message: 'Patio' },
74
- Terrace: /* i18n */ { id: 'editor.roomName.terrace', message: 'Terrace' },
75
- Balcony: /* i18n */ { id: 'editor.roomName.balcony', message: 'Balcony' },
76
- Porch: /* i18n */ { id: 'editor.roomName.porch', message: 'Porch' },
77
- Veranda: /* i18n */ { id: 'editor.roomName.veranda', message: 'Veranda' },
78
- Deck: /* i18n */ { id: 'editor.roomName.deck', message: 'Deck' },
79
- Courtyard: /* i18n */ { id: 'editor.roomName.courtyard', message: 'Courtyard' },
80
- 'Pool area': /* i18n */ { id: 'editor.roomName.poolArea', message: 'Pool area' },
81
- Carport: /* i18n */ { id: 'editor.roomName.carport', message: 'Carport' },
82
- 'Garden shed': /* i18n */ { id: 'editor.roomName.gardenShed', message: 'Garden shed' },
83
- Mezzanine: /* i18n */ { id: 'editor.roomName.mezzanine', message: 'Mezzanine' },
84
- Loft: /* i18n */ { id: 'editor.roomName.loft', message: 'Loft' },
85
- 'Reading nook': /* i18n */ { id: 'editor.roomName.readingNook', message: 'Reading nook' },
86
- Gallery: /* i18n */ { id: 'editor.roomName.gallery', message: 'Gallery' },
87
- }
88
-
89
16
  export function localizedRoomNameSections(
90
17
  sections: readonly RoomNameSection[],
91
18
  i18n: DescriptorTranslator,
@@ -101,47 +28,3 @@ export function localizedRoomNameSections(
101
28
  }
102
29
  })
103
30
  }
104
-
105
- /**
106
- * A room name in the UI language. Catalog names stay English in the scene
107
- * (numbering, MCP and exports read them); a catalog name with the number the
108
- * picker adds shows translated with that number ("Bedroom 2" → "Chambre 2").
109
- * Anything else the user typed shows as written.
110
- */
111
- export function displayRoomName(name: string, i18n: DescriptorTranslator): string {
112
- const { lead, base, number, trail } = roomNameParts(name)
113
- const label = catalogLabel(base, i18n)
114
- return label === undefined ? name : `${lead}${label}${number}${trail}`
115
- }
116
-
117
- /**
118
- * What an edit of a room's name field stores. The field shows the room's own
119
- * catalog name translated ("Lounge 2" as "Salon 2"); when the edit only
120
- * changes, adds or removes that name's number ("Salon 3"), it stores the
121
- * English name with the new number ("Lounge 3"), as editing it in English
122
- * would. Any other text, an exact translation of a different name included, is
123
- * stored exactly as typed.
124
- */
125
- export function editedRoomName(stored: string, text: string, i18n: DescriptorTranslator): string {
126
- const own = roomNameParts(stored).base
127
- const label = catalogLabel(own, i18n)
128
- const edited = roomNameParts(text)
129
- if (label === undefined || edited.base !== label) return text
130
- return `${edited.lead}${own}${edited.number}${edited.trail}`
131
- }
132
-
133
- function roomNameParts(name: string) {
134
- const match = /^(\s*)(.*?)(\s+\d+)?(\s*)$/.exec(name)
135
- return {
136
- lead: match?.[1] ?? '',
137
- base: match?.[2] ?? '',
138
- number: match?.[3] ?? '',
139
- trail: match?.[4] ?? '',
140
- }
141
- }
142
-
143
- function catalogLabel(english: string, i18n: DescriptorTranslator): string | undefined {
144
- // Own keys only: a room named "constructor" or "__proto__" is free text.
145
- if (!(english && Object.hasOwn(ROOM_NAME_MESSAGES, english))) return undefined
146
- return i18n._(ROOM_NAME_MESSAGES[english]!)
147
- }
@@ -15,6 +15,7 @@ import {
15
15
  } from '@pascal-app/core'
16
16
  import { AGENT_OPERATIONS, applyAgentOutcome } from '@pascal-app/core/agent-operations'
17
17
  import { createAgentHost, liveSceneRuntime } from '@pascal-app/core/runtime'
18
+ import { displayRoomName } from '@pascal-app/viewer/ui'
18
19
  import {
19
20
  commitRoomNameEdit,
20
21
  commitRoomNamePick,
@@ -22,7 +23,7 @@ import {
22
23
  roomNameOptions,
23
24
  } from '../components/ui/panels/room-name-options'
24
25
  import { roomNameSections } from './room-name-catalog'
25
- import { displayRoomName, localizedRoomNameSections } from './room-name-messages'
26
+ import { localizedRoomNameSections } from './room-name-messages'
26
27
  import { applyRoomPlan, renameRoom } from './room-structure-commands'
27
28
  import { copySelectedNodesToEditorClipboard, pasteEditorClipboardToLevel } from './scene-clipboard'
28
29
 
@@ -1,9 +1,9 @@
1
1
  import { type AnyNodeId, type BuildingNode, type LevelNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
+ import type { SelectionModifierKeys } from '@pascal-app/viewer/ui'
4
+ import { type RoomKey, shouldSelectRoom } from '@pascal-app/viewer/ui'
3
5
  import useEditor from '../store/use-editor'
4
6
  import useSessionGroups from '../store/use-session-groups'
5
- import { type RoomKey, shouldSelectRoom } from './room-selection'
6
- import type { SelectionModifierKeys } from './selection-routing'
7
7
 
8
8
  export function selectRoom(room: RoomKey) {
9
9
  const level = useScene.getState().nodes[room.levelId as AnyNodeId]
@@ -24,6 +24,12 @@ import {
24
24
  } from '@pascal-app/core'
25
25
  import { migrateCeilingRoomLinks, migrateRoomZones } from '@pascal-app/core/scene-migrations'
26
26
  import { useViewer } from '@pascal-app/viewer'
27
+ import {
28
+ type RoomKey,
29
+ RoomSelectionIndex,
30
+ resolveRoomHit,
31
+ shouldSelectRoom,
32
+ } from '@pascal-app/viewer/ui'
27
33
  import { _roots, act } from '@react-three/fiber'
28
34
  import { createElement } from 'react'
29
35
  import { Group, type LineSegments } from 'three'
@@ -37,12 +43,6 @@ import useSessionGroups from '../store/use-session-groups'
37
43
  import { withSelectionHarness } from '../test-utils/selection-harness'
38
44
  import { startRoomDivide } from './room-divide-session'
39
45
  import { runRoomHandleDrag } from './room-handle-drag'
40
- import {
41
- type RoomKey,
42
- RoomSelectionIndex,
43
- resolveRoomHit,
44
- shouldSelectRoom,
45
- } from './room-selection'
46
46
  import { selectRoom, selectRoomFromHit, shouldInterceptRoom } from './room-selection-commands'
47
47
 
48
48
  const levelId = 'level_room_selection'
@@ -10,7 +10,7 @@ import {
10
10
  useScene,
11
11
  type ZoneTransformPlan,
12
12
  } from '@pascal-app/core'
13
- import { useViewer } from '@pascal-app/viewer'
13
+ import { isTypingTarget, useViewer } from '@pascal-app/viewer'
14
14
  import { Plane, Vector2, Vector3 } from 'three'
15
15
  import { create } from 'zustand'
16
16
  import { levelFrame } from '../components/editor/group-transform-shared'
@@ -23,7 +23,6 @@ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
23
23
  import { isHistoryShortcut } from './history'
24
24
  import { mezzanineConflictMessage } from './mezzanine-messages'
25
25
  import { sfxEmitter } from './sfx-bus'
26
- import { isTypingTarget } from './typing-target'
27
26
 
28
27
  export const ROOM_MOVE_DRAG_LABEL = 'room-move'
29
28
 
@@ -1,11 +1,11 @@
1
1
  import { type AnyNode, type AnyNodeId, useScene, type ZoneNode } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
+ import { type RoomKey, type SelectionModifierKeys, sameRoom } from '@pascal-app/viewer/ui'
3
4
  import { getRoomSelectionIndex, hoverRoomFromHit } from '../hooks/use-selected-room'
4
5
  import useEditor from '../store/use-editor'
5
6
  import { expandSessionSelectionForNode } from '../store/use-session-groups'
6
- import { type RoomKey, sameRoom } from './room-selection'
7
7
  import { selectRoom } from './room-selection-commands'
8
- import { resolveSelectedIdsForNodeClick, type SelectionModifierKeys } from './selection-routing'
8
+ import { resolveSelectedIdsForNodeClick } from './selection-routing'
9
9
 
10
10
  // A room is one thing to the user whichever way they reach it: the canvas, the
11
11
  // Rooms list, the plan's fill or its label. Rooms the room index knows (closed,
@@ -18,7 +18,6 @@ import {
18
18
  resolveCanvasBuildingId,
19
19
  resolveCanvasSelectionNode,
20
20
  resolveSelectedIdsForNodeClick,
21
- selectionModifiersFromEvent,
22
21
  shouldPreserveSelectedRoofHostTarget,
23
22
  } from './selection-routing'
24
23
 
@@ -249,33 +248,6 @@ describe('deleteNodeFromCanvas', () => {
249
248
  })
250
249
  })
251
250
 
252
- describe('selectionModifiersFromEvent', () => {
253
- test('falls back to tracked modifier state when the click event omits keys', () => {
254
- expect(
255
- selectionModifiersFromEvent({}, { meta: false, ctrl: true, shift: false, alt: false }),
256
- ).toEqual({
257
- meta: false,
258
- ctrl: true,
259
- shift: false,
260
- alt: false,
261
- })
262
- })
263
-
264
- test('prefers explicit event key state over stale tracked modifiers', () => {
265
- expect(
266
- selectionModifiersFromEvent(
267
- { metaKey: false, ctrlKey: false, shiftKey: false, altKey: false },
268
- { meta: true, ctrl: true, shift: true, alt: true },
269
- ),
270
- ).toEqual({
271
- meta: false,
272
- ctrl: false,
273
- shift: false,
274
- alt: false,
275
- })
276
- })
277
- })
278
-
279
251
  describe('resolveCanvasSelectionNode', () => {
280
252
  // Mirrors the cabinet-module setup: modules proxy to their run for grouped
281
253
  // move / rotate, but declare `selectionProxy.bypassDirectPick` so a direct
@@ -8,6 +8,7 @@ import {
8
8
  useScene,
9
9
  } from '@pascal-app/core'
10
10
  import { useViewer } from '@pascal-app/viewer'
11
+ import type { SelectionModifierKeys } from '@pascal-app/viewer/ui'
11
12
  import useEditor from '../store/use-editor'
12
13
  import { emitDeleteSFX } from './sfx-bus'
13
14
 
@@ -43,14 +44,6 @@ export function enterBuildingFromPlanHit(hitId: string | null): boolean {
43
44
  return node ? enterBuildingFromCanvas(node) : false
44
45
  }
45
46
 
46
- export type SelectionModifierKeys = {
47
- meta: boolean
48
- ctrl: boolean
49
- shift: boolean
50
- /** Alt alone: select one session-group member without expanding. */
51
- alt: boolean
52
- }
53
-
54
47
  /** A Delete-mode click on a plan entry: the sledgehammer, not a selection. */
55
48
  /**
56
49
  * A canvas click (2D or 3D) selected `node`: publish it on
@@ -111,38 +104,6 @@ export function isSelectionModifierActive(keys: SelectionModifierKeys): boolean
111
104
  return keys.meta || keys.ctrl || keys.shift
112
105
  }
113
106
 
114
- export function selectionModifiersFromEvent(
115
- event?: {
116
- metaKey?: boolean
117
- ctrlKey?: boolean
118
- shiftKey?: boolean
119
- altKey?: boolean
120
- nativeEvent?: {
121
- metaKey?: boolean
122
- ctrlKey?: boolean
123
- shiftKey?: boolean
124
- altKey?: boolean
125
- }
126
- } | null,
127
- fallback?: Partial<SelectionModifierKeys>,
128
- ): SelectionModifierKeys {
129
- const fromEvent = (
130
- key: keyof SelectionModifierKeys,
131
- eventKey: 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey',
132
- ) => {
133
- if (typeof event?.[eventKey] === 'boolean') return event[eventKey]
134
- if (typeof event?.nativeEvent?.[eventKey] === 'boolean') return event.nativeEvent[eventKey]
135
- return Boolean(fallback?.[key])
136
- }
137
-
138
- return {
139
- meta: fromEvent('meta', 'metaKey'),
140
- ctrl: fromEvent('ctrl', 'ctrlKey'),
141
- shift: fromEvent('shift', 'shiftKey'),
142
- alt: fromEvent('alt', 'altKey'),
143
- }
144
- }
145
-
146
107
  export function resolveSelectedIdsForNodeClick({
147
108
  baseSelectedIds,
148
109
  currentSelectedIds,
@@ -1,11 +1,11 @@
1
- import { emitter, type InteractionSound, type TerrainVerb } from '@pascal-app/core'
1
+ import type { TerrainVerb } from '@pascal-app/core'
2
+ import { initInteractionSounds } from '@pascal-app/viewer/sfx'
2
3
  import mitt from 'mitt'
3
- import useAudio from '../store/use-audio'
4
4
  import {
5
5
  disposeSFX,
6
6
  type LoopSFXName,
7
7
  playSFX,
8
- type SFXName,
8
+ preloadSFX,
9
9
  startLoopSFX,
10
10
  stopLoopSFX,
11
11
  updateSFXVolumes,
@@ -73,30 +73,7 @@ const TERRAIN_LOOP_BY_VERB = {
73
73
  } as const satisfies Record<TerrainVerb, LoopSFXName>
74
74
  const handleTerrainSculptStart = (verb: TerrainVerb) => startLoopSFX(TERRAIN_LOOP_BY_VERB[verb])
75
75
  const handleTerrainSculptStop = () => stopLoopSFX()
76
- let unsubscribeAudio: (() => void) | null = null
77
-
78
- /** Windows ride the door recordings, softer: a sash weighs less than a leaf. */
79
- const INTERACTION_SFX: Record<InteractionSound, [SFXName, number]> = {
80
- 'door-open': ['doorOpen', 1],
81
- 'door-swing': ['doorSwing', 1],
82
- 'door-close': ['doorClose', 1],
83
- 'window-open': ['doorOpen', 0.6],
84
- 'window-swing': ['doorSwing', 0.6],
85
- 'window-close': ['doorClose', 0.6],
86
- garage: ['garageDoor', 1],
87
- 'shutters-open': ['shuttersOpen', 1],
88
- 'shutters-close': ['shuttersClose', 1],
89
- // Cabinet doors and drawers: the panel recordings, as the marketing house plays its cabinets.
90
- 'panel-open': ['shuttersOpen', 0.75],
91
- 'panel-swing': ['doorSwing', 0.45],
92
- 'panel-close': ['shuttersClose', 0.75],
93
- 'switch-on': ['switchOn', 1],
94
- 'switch-off': ['switchOff', 1],
95
- }
96
- const handleInteractionSound = (sound: InteractionSound) => {
97
- const [name, volumeMultiplier] = INTERACTION_SFX[sound]
98
- playSFX(name, { volumeMultiplier })
99
- }
76
+ let unsubscribeInteractions: (() => void) | null = null
100
77
 
101
78
  /**
102
79
  * Initialize SFX Bus - connects SFX events to actual sound playback.
@@ -124,8 +101,8 @@ export function initSFXBus() {
124
101
  sfxEmitter.on('sfx:turn-needs-input', handleTurnNeedsInput)
125
102
  sfxEmitter.on('sfx:terrain-sculpt-start', handleTerrainSculptStart)
126
103
  sfxEmitter.on('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
127
- emitter.on('interaction:sound', handleInteractionSound)
128
- unsubscribeAudio = useAudio.subscribe(updateSFXVolumes)
104
+ unsubscribeInteractions = initInteractionSounds()
105
+ preloadSFX()
129
106
  updateSFXVolumes()
130
107
  }
131
108
 
@@ -150,9 +127,8 @@ export function disposeSFXBus() {
150
127
  sfxEmitter.off('sfx:turn-needs-input', handleTurnNeedsInput)
151
128
  sfxEmitter.off('sfx:terrain-sculpt-start', handleTerrainSculptStart)
152
129
  sfxEmitter.off('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
153
- emitter.off('interaction:sound', handleInteractionSound)
154
- unsubscribeAudio?.()
155
- unsubscribeAudio = null
130
+ unsubscribeInteractions?.()
131
+ unsubscribeInteractions = null
156
132
  sfxBusInitialized = false
157
133
  }
158
134
  disposeSFX()