@pascal-app/editor 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/package.json +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/node-icon-image.test.tsx +46 -0
  84. package/src/components/ui/node-icon-image.tsx +33 -1
  85. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  86. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  87. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  88. package/src/components/ui/panels/node-display.ts +1 -1
  89. package/src/components/ui/panels/panel-manager.tsx +1 -1
  90. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  91. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  92. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  93. package/src/components/ui/panels/room-name-options.ts +1 -5
  94. package/src/components/ui/panels/room-panel.tsx +6 -8
  95. package/src/components/ui/primitives/color-dot.tsx +2 -2
  96. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  97. package/src/components/ui/primitives/sidebar.tsx +3 -3
  98. package/src/components/ui/right-stack.tsx +1 -1
  99. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  100. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  107. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  109. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  110. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  112. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  113. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  114. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  115. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  116. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  117. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  118. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  119. package/src/components/viewer-overlay.tsx +1 -2
  120. package/src/hooks/use-auto-frame.ts +1 -2
  121. package/src/hooks/use-keyboard.ts +1 -2
  122. package/src/hooks/use-selected-room.ts +5 -7
  123. package/src/i18n/floor-footprint-name.ts +1 -1
  124. package/src/i18n/locales/en/messages.po +180 -182
  125. package/src/i18n/locales/es/messages.po +180 -182
  126. package/src/i18n/locales/fr/messages.po +180 -182
  127. package/src/i18n/locales/ja/messages.po +180 -182
  128. package/src/i18n/locales/zh-CN/messages.po +180 -182
  129. package/src/i18n/locales.test.ts +1 -1
  130. package/src/index.tsx +109 -131
  131. package/src/lib/below-grade.test.tsx +4 -4
  132. package/src/lib/camera-facing.test.ts +1 -1
  133. package/src/lib/camera-facing.ts +2 -3
  134. package/src/lib/editor-level-display.ts +1 -1
  135. package/src/lib/floor-footprints.test.ts +2 -2
  136. package/src/lib/floor-opening-draft.ts +1 -1
  137. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  138. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  139. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  140. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  141. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  142. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  144. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  145. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  146. package/src/lib/floorplan/geometry.ts +2 -112
  147. package/src/lib/floorplan/index.ts +0 -9
  148. package/src/lib/floorplan/items.ts +2 -1
  149. package/src/lib/floorplan/schedules.ts +6 -3
  150. package/src/lib/floorplan/sheet-export.ts +7 -3
  151. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  152. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  153. package/src/lib/follow-pascal-presentation.ts +1 -1
  154. package/src/lib/glb-export.ts +1 -1
  155. package/src/lib/material-paint.ts +1 -1
  156. package/src/lib/measurements.test.ts +2 -1
  157. package/src/lib/mezzanine-selection.test.ts +1 -1
  158. package/src/lib/paint-region-mode.ts +1 -1
  159. package/src/lib/point-ask/session.ts +1 -2
  160. package/src/lib/roof-duplication.test.ts +100 -0
  161. package/src/lib/roof-duplication.ts +30 -5
  162. package/src/lib/room-handle-drag.ts +2 -12
  163. package/src/lib/room-name-catalog.test.ts +2 -1
  164. package/src/lib/room-name-messages.test.ts +2 -1
  165. package/src/lib/room-name-messages.ts +1 -118
  166. package/src/lib/room-name-storage.test.ts +2 -1
  167. package/src/lib/room-selection-commands.ts +2 -2
  168. package/src/lib/room-selection.test.ts +6 -6
  169. package/src/lib/room-transform-session.ts +1 -2
  170. package/src/lib/room-zone-routing.ts +2 -2
  171. package/src/lib/selection-routing.test.ts +0 -28
  172. package/src/lib/selection-routing.ts +1 -40
  173. package/src/lib/sfx-bus.ts +8 -32
  174. package/src/lib/sfx-player.test.ts +72 -1
  175. package/src/lib/sfx-player.ts +49 -337
  176. package/src/lib/viewer-selection.test.ts +38 -10
  177. package/src/lib/wall-pull.test.ts +1 -1
  178. package/src/lib/wall-pull.ts +1 -27
  179. package/src/store/use-editor.tsx +17 -26
  180. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  181. package/src/store/use-wall-region-handles-selection.ts +1 -2
  182. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  183. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  184. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  185. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  186. package/src/components/editor/first-person/ground-holes.ts +0 -130
  187. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  188. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  189. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  190. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  191. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  192. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  193. package/src/components/editor/localized-html.tsx +0 -22
  194. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  195. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  196. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  197. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  198. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  199. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  200. package/src/components/editor-2d/svg-paths.ts +0 -124
  201. package/src/components/ui/action-menu/action-button.tsx +0 -89
  202. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  203. package/src/components/ui/primitives/button.tsx +0 -69
  204. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  205. package/src/components/ui/primitives/popover.tsx +0 -42
  206. package/src/components/ui/primitives/switch.tsx +0 -29
  207. package/src/components/ui/primitives/tooltip.tsx +0 -57
  208. package/src/components/ui/scene-loader.tsx +0 -73
  209. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  210. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  211. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  212. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  213. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  214. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  215. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  216. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  217. package/src/components/viewer/render-settings-menu.tsx +0 -294
  218. package/src/components/viewer/sky-control.tsx +0 -710
  219. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  220. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  221. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  222. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  223. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  224. package/src/components/viewer/viewer-rooms.tsx +0 -248
  225. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  226. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  227. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  228. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  229. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  230. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  231. package/src/components/viewer/viewer-stage.tsx +0 -221
  232. package/src/components/walkthrough-hud.tsx +0 -193
  233. package/src/hooks/use-mobile.ts +0 -19
  234. package/src/i18n/level-name.ts +0 -39
  235. package/src/i18n/scene-theme-name.ts +0 -16
  236. package/src/lib/below-grade.ts +0 -133
  237. package/src/lib/camera-pose.test.ts +0 -237
  238. package/src/lib/camera-pose.ts +0 -172
  239. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  240. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  241. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  242. package/src/lib/floorplan/marks.test.ts +0 -213
  243. package/src/lib/floorplan/marks.ts +0 -238
  244. package/src/lib/measurements.ts +0 -8
  245. package/src/lib/node-display-label.ts +0 -37
  246. package/src/lib/room-assembly-overlay.test.ts +0 -275
  247. package/src/lib/room-assembly-overlay.ts +0 -265
  248. package/src/lib/room-selection.ts +0 -417
  249. package/src/lib/scene-bounds.test.ts +0 -327
  250. package/src/lib/scene-bounds.ts +0 -233
  251. package/src/lib/typing-target.ts +0 -9
  252. package/src/lib/viewer-selection.ts +0 -595
  253. package/src/lib/zone-content.ts +0 -133
  254. package/src/store/camera-pose-store.test.ts +0 -29
  255. package/src/store/camera-pose-store.ts +0 -27
  256. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  257. package/src/store/navigation-sync-pose-store.ts +0 -27
  258. package/src/store/use-audio.test.ts +0 -146
  259. package/src/store/use-audio.tsx +0 -169
  260. package/src/store/use-first-person-hud.ts +0 -40
  261. package/src/store/use-follow-pascal.test.ts +0 -62
  262. 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()