@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.
- package/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +11 -11
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +40 -1671
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurements.ts +0 -8
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- 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
|
|
152
|
-
(
|
|
176
|
+
const missingChildId = [...segmentIds, ...faceClones.map(({ node }) => node.id)].find(
|
|
177
|
+
(childId) => !createdChildIds.includes(childId as AnyNodeId),
|
|
153
178
|
)
|
|
154
|
-
if (
|
|
179
|
+
if (missingChildId) {
|
|
155
180
|
throw new Error(
|
|
156
|
-
`Duplicated roof "${createdRoof.id}" is missing cloned
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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,
|
package/src/lib/sfx-bus.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
unsubscribeAudio = null
|
|
130
|
+
unsubscribeInteractions?.()
|
|
131
|
+
unsubscribeInteractions = null
|
|
156
132
|
sfxBusInitialized = false
|
|
157
133
|
}
|
|
158
134
|
disposeSFX()
|