@pascal-app/editor 1.0.5 → 1.0.7
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 +20 -17
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- 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/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- 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/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +30 -14
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +464 -65
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +61 -4
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15117 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +20 -5
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +14 -5
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +42 -0
- package/src/lib/window-interaction.ts +47 -5
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +48 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-wall-snap-indicator.ts +15 -2
|
@@ -4,12 +4,13 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
area,
|
|
7
|
-
getLevelDisplayName,
|
|
8
7
|
runAsSingleSceneHistoryStep,
|
|
9
8
|
useScene,
|
|
10
|
-
type ZoneNode,
|
|
9
|
+
type ZoneNode, usePascalI18n,
|
|
10
|
+
type PascalI18n,
|
|
11
|
+
useLocale,
|
|
11
12
|
} from '@pascal-app/core'
|
|
12
|
-
import { setRoomFloorScript } from '@pascal-app/core/agent-operations'
|
|
13
|
+
import { type SceneChanges, setRoomFloorScript } from '@pascal-app/core/agent-operations'
|
|
13
14
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
15
|
import { Check, ChevronDown, Plus } from 'lucide-react'
|
|
15
16
|
import { type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
@@ -23,12 +24,11 @@ import { mezzanineElevationBounds } from '../../../lib/room-handle-drag'
|
|
|
23
24
|
import type { RoomSelectionRecord } from '../../../lib/room-selection'
|
|
24
25
|
import {
|
|
25
26
|
baseRoomName,
|
|
26
|
-
numberedRoomName,
|
|
27
27
|
type RoomNameSection,
|
|
28
28
|
roomNameSections,
|
|
29
29
|
roomNameUseCount,
|
|
30
30
|
} from '../../../lib/room-name-catalog'
|
|
31
|
-
import {
|
|
31
|
+
import { requestRoomDeletion } from '../../../lib/room-structure-commands'
|
|
32
32
|
import { addZoneToNewUnit, assignZoneToUnit, zoneUnits } from '../../../lib/units'
|
|
33
33
|
import { cn } from '../../../lib/utils'
|
|
34
34
|
import useEditor from '../../../store/use-editor'
|
|
@@ -53,23 +53,49 @@ import {
|
|
|
53
53
|
RoomConstructionRows,
|
|
54
54
|
roomInsideHeight,
|
|
55
55
|
} from './room-construction-rows'
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
commitRoomNameEdit,
|
|
58
|
+
commitRoomNamePick,
|
|
59
|
+
type RoomNameOption,
|
|
60
|
+
roomNameOptions,
|
|
61
|
+
} from './room-name-options'
|
|
62
|
+
import { localizedLevelName } from '../../../i18n/level-name'
|
|
63
|
+
import { messageWithSlot } from '../../../i18n/message-slot'
|
|
64
|
+
import {
|
|
65
|
+
displayRoomName,
|
|
66
|
+
localizedRoomNameSections,
|
|
67
|
+
} from '../../../lib/room-name-messages'
|
|
57
68
|
|
|
58
69
|
/** "Room · Ground floor", "Mezzanine · in Kitchen", "Room · not enclosed". */
|
|
59
70
|
export function roomKicker(
|
|
60
71
|
nodes: Record<string, AnyNode>,
|
|
61
72
|
zoneId: string,
|
|
73
|
+
i18n: PascalI18n,
|
|
74
|
+
locale: string,
|
|
62
75
|
options: { mezzanine?: boolean; open?: boolean } = {},
|
|
63
76
|
): string {
|
|
64
77
|
const zone = nodes[zoneId]
|
|
65
78
|
if (options.mezzanine && zone?.type === 'zone') {
|
|
66
79
|
const host = zone.hostZoneId ? nodes[zone.hostZoneId] : undefined
|
|
67
80
|
const hostName = host?.type === 'zone' ? host.name?.trim() : ''
|
|
68
|
-
return hostName
|
|
81
|
+
return hostName
|
|
82
|
+
? i18n._({
|
|
83
|
+
id: 'editor.panels.roomPanel.kickerMezzanineIn',
|
|
84
|
+
message: 'Mezzanine · in {room}',
|
|
85
|
+
values: { room: displayRoomName(hostName, i18n) },
|
|
86
|
+
})
|
|
87
|
+
: i18n._({ id: 'editor.panels.roomPanel.mezzanine', message: 'Mezzanine' })
|
|
69
88
|
}
|
|
70
|
-
if (options.open)
|
|
89
|
+
if (options.open)
|
|
90
|
+
return i18n._({ id: 'editor.panels.roomPanel.kickerNotEnclosed', message: 'Room · not enclosed' })
|
|
71
91
|
const level = zone?.parentId ? nodes[zone.parentId] : undefined
|
|
72
|
-
return level?.type === 'level'
|
|
92
|
+
return level?.type === 'level'
|
|
93
|
+
? i18n._({
|
|
94
|
+
id: 'editor.panels.roomPanel.kickerLevel',
|
|
95
|
+
message: 'Room · {level}',
|
|
96
|
+
values: { level: localizedLevelName(level, i18n, locale) },
|
|
97
|
+
})
|
|
98
|
+
: i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' })
|
|
73
99
|
}
|
|
74
100
|
|
|
75
101
|
export function RoomPanel({
|
|
@@ -79,8 +105,15 @@ export function RoomPanel({
|
|
|
79
105
|
room: RoomSelectionRecord
|
|
80
106
|
onClose?: () => void
|
|
81
107
|
}) {
|
|
108
|
+
const i18n = usePascalI18n()
|
|
109
|
+
const locale = useLocale()
|
|
82
110
|
const nodes = useScene((state) => state.nodes)
|
|
83
|
-
const
|
|
111
|
+
const catalogVersion = i18n.getVersion()
|
|
112
|
+
const sections = useMemo(
|
|
113
|
+
() => localizedRoomNameSections(roomNameSections(nodes, room.zoneId), i18n),
|
|
114
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-translate on a catalog change
|
|
115
|
+
[i18n, catalogVersion, nodes, room.zoneId],
|
|
116
|
+
)
|
|
84
117
|
const name = useScene((state) => state.nodes[room.zoneId as AnyNodeId]?.name ?? '')
|
|
85
118
|
const visible = useInteractionScope(
|
|
86
119
|
(state) => resolveOverlayPolicy(state.scope).conflictingControls === 'shown',
|
|
@@ -89,9 +122,9 @@ export function RoomPanel({
|
|
|
89
122
|
const mezzanine = !!room.mezzanine
|
|
90
123
|
return (
|
|
91
124
|
<PanelWrapper
|
|
92
|
-
kicker={roomKicker(nodes, room.zoneId, { mezzanine })}
|
|
125
|
+
kicker={roomKicker(nodes, room.zoneId, i18n, locale, { mezzanine })}
|
|
93
126
|
onClose={onClose}
|
|
94
|
-
title={name.trim() || (mezzanine ? 'Mezzanine' : 'Room')}
|
|
127
|
+
title={(name.trim() && displayRoomName(name, i18n)) || (mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanine', message: 'Mezzanine' }) : i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' }))}
|
|
95
128
|
titleContent={
|
|
96
129
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
97
130
|
<RoomColorDot zoneId={room.zoneId} />
|
|
@@ -125,17 +158,27 @@ export function RoomPanel({
|
|
|
125
158
|
* needs no closed outline — its name, its unit, deleting it.
|
|
126
159
|
*/
|
|
127
160
|
export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: () => void }) {
|
|
161
|
+
const i18n = usePascalI18n()
|
|
162
|
+
const locale = useLocale()
|
|
128
163
|
const nodes = useScene((state) => state.nodes)
|
|
129
|
-
const
|
|
164
|
+
const catalogVersion = i18n.getVersion()
|
|
165
|
+
const sections = useMemo(
|
|
166
|
+
() => localizedRoomNameSections(roomNameSections(nodes, zoneId), i18n),
|
|
167
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-translate on a catalog change
|
|
168
|
+
[i18n, catalogVersion, nodes, zoneId],
|
|
169
|
+
)
|
|
130
170
|
const name = useScene((state) => state.nodes[zoneId as AnyNodeId]?.name ?? '')
|
|
131
171
|
const unit = useViewer((s) => s.unit)
|
|
132
172
|
const zone = nodes[zoneId as AnyNodeId]
|
|
133
173
|
if (zone?.type !== 'zone') return null
|
|
134
174
|
return (
|
|
135
175
|
<PanelWrapper
|
|
136
|
-
kicker={roomKicker(nodes, zoneId, { open: true })}
|
|
176
|
+
kicker={roomKicker(nodes, zoneId, i18n, locale, { open: true })}
|
|
137
177
|
onClose={onClose}
|
|
138
|
-
title={
|
|
178
|
+
title={
|
|
179
|
+
(name.trim() && displayRoomName(name, i18n)) ||
|
|
180
|
+
i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' })
|
|
181
|
+
}
|
|
139
182
|
titleContent={
|
|
140
183
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
141
184
|
<RoomColorDot zoneId={zoneId} />
|
|
@@ -151,8 +194,11 @@ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: ()
|
|
|
151
194
|
>
|
|
152
195
|
<div className="flex flex-col px-4 pt-3 pb-4 text-sm" data-open-room-panel={zoneId}>
|
|
153
196
|
<p className="text-muted-foreground text-xs">
|
|
154
|
-
{
|
|
155
|
-
|
|
197
|
+
{i18n._({
|
|
198
|
+
id: 'editor.panels.roomPanel.openRoomArea',
|
|
199
|
+
message: '{area} · Close its walls to build its floor and ceiling.',
|
|
200
|
+
values: { area: formatAreaLabel(area([{ outer: zone.polygon, holes: zone.holes }]), unit, 2) },
|
|
201
|
+
})}
|
|
156
202
|
</p>
|
|
157
203
|
<RoomUnitRow zoneId={zoneId} />
|
|
158
204
|
<button
|
|
@@ -161,7 +207,7 @@ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: ()
|
|
|
161
207
|
onClick={() => requestRoomDeletion(zoneId)}
|
|
162
208
|
type="button"
|
|
163
209
|
>
|
|
164
|
-
Delete room
|
|
210
|
+
{i18n._({ id: 'editor.panels.roomPanel.deleteRoom', message: 'Delete room' })}
|
|
165
211
|
</button>
|
|
166
212
|
</div>
|
|
167
213
|
</PanelWrapper>
|
|
@@ -170,16 +216,17 @@ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: ()
|
|
|
170
216
|
|
|
171
217
|
/** "Unit: Flat A ›" — which unit the room belongs to; change it, add it to one, or take it out. */
|
|
172
218
|
export function RoomUnitRow({ zoneId }: { zoneId: string }) {
|
|
219
|
+
const i18n = usePascalI18n()
|
|
173
220
|
const nodes = useScene((scene) => scene.nodes)
|
|
174
221
|
const { buildingId, units, current } = useMemo(() => zoneUnits(nodes, zoneId), [nodes, zoneId])
|
|
175
222
|
if (!buildingId) return null
|
|
176
223
|
return (
|
|
177
224
|
<div className="mt-3 flex min-h-8 items-center justify-between gap-3" data-room-unit={zoneId}>
|
|
178
|
-
<span className="text-muted-foreground text-xs">Unit</span>
|
|
225
|
+
<span className="text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' })}</span>
|
|
179
226
|
<DropdownMenu>
|
|
180
227
|
<DropdownMenuTrigger asChild>
|
|
181
228
|
<button
|
|
182
|
-
aria-label=
|
|
229
|
+
aria-label={i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' })}
|
|
183
230
|
className="flex min-w-0 items-center gap-1.5 rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
|
|
184
231
|
type="button"
|
|
185
232
|
>
|
|
@@ -191,7 +238,7 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
|
|
|
191
238
|
/>
|
|
192
239
|
)}
|
|
193
240
|
<span className="truncate">
|
|
194
|
-
{current ? current.name || 'Unit' : units.length ? 'None' : 'Add to a unit'}
|
|
241
|
+
{current ? current.name || i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' }) : units.length ? i18n._({ id: 'editor.panels.roomPanel.none', message: 'None' }) : i18n._({ id: 'editor.panels.roomPanel.addToAUnit', message: 'Add to a unit' })}
|
|
195
242
|
</span>
|
|
196
243
|
<ChevronDown className="size-3 shrink-0 text-muted-foreground" />
|
|
197
244
|
</button>
|
|
@@ -207,17 +254,23 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
|
|
|
207
254
|
className="size-2 shrink-0 rounded-full"
|
|
208
255
|
style={{ backgroundColor: unit.color }}
|
|
209
256
|
/>
|
|
210
|
-
<span className="flex-1 truncate">{unit.name || 'Unit'}</span>
|
|
257
|
+
<span className="flex-1 truncate">{unit.name || i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' })}</span>
|
|
211
258
|
{unit.id === current?.id && <Check className="size-3.5" />}
|
|
212
259
|
</DropdownMenuItem>
|
|
213
260
|
))}
|
|
214
261
|
{units.length > 0 && <DropdownMenuSeparator />}
|
|
215
262
|
<DropdownMenuItem onSelect={() => addZoneToNewUnit(zoneId as ZoneNode['id'])}>
|
|
216
|
-
New unit
|
|
263
|
+
{i18n._({ id: 'editor.panels.roomPanel.newUnit', message: 'New unit' })}
|
|
217
264
|
</DropdownMenuItem>
|
|
218
265
|
{current && (
|
|
219
266
|
<DropdownMenuItem onSelect={() => assignZoneToUnit(zoneId as ZoneNode['id'], null)}>
|
|
220
|
-
|
|
267
|
+
{current.name
|
|
268
|
+
? i18n._({
|
|
269
|
+
id: 'editor.panels.roomPanel.removeFromUnitNamed',
|
|
270
|
+
message: 'Remove from {unit}',
|
|
271
|
+
values: { unit: current.name },
|
|
272
|
+
})
|
|
273
|
+
: i18n._({ id: 'editor.panels.roomPanel.removeFromUnit', message: 'Remove from unit' })}
|
|
221
274
|
</DropdownMenuItem>
|
|
222
275
|
)}
|
|
223
276
|
</DropdownMenuContent>
|
|
@@ -226,72 +279,94 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
|
|
|
226
279
|
)
|
|
227
280
|
}
|
|
228
281
|
|
|
229
|
-
/**
|
|
230
|
-
* "Scripted finish": a floor a script builds (it owns the floor's colours, so Paint leaves it),
|
|
231
|
-
* with its code to read and a way to take it off, back to the floor's own finish.
|
|
232
|
-
*/
|
|
282
|
+
/** The room's floor a script builds, with its code to read and a way to take it off. */
|
|
233
283
|
function RoomFloorScriptRow({ zoneId }: { zoneId: string }) {
|
|
234
284
|
const sha = useScene((scene) => {
|
|
235
285
|
const zone = scene.nodes[zoneId as AnyNodeId]
|
|
236
286
|
return zone?.type === 'zone' ? zone.floor?.script?.script : undefined
|
|
237
287
|
})
|
|
288
|
+
if (!sha) return null
|
|
289
|
+
return (
|
|
290
|
+
<ScriptedFinishRow
|
|
291
|
+
data-room-floor-script={zoneId}
|
|
292
|
+
remove={() =>
|
|
293
|
+
setRoomFloorScript(
|
|
294
|
+
useScene.getState().nodes,
|
|
295
|
+
{ zoneId, remove: true },
|
|
296
|
+
{ activeLevelId: null },
|
|
297
|
+
).changes
|
|
298
|
+
}
|
|
299
|
+
sha={sha}
|
|
300
|
+
/>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* "Scripted finish": a floor a script builds (a room's floor, a slab's surface; it owns the floor's
|
|
306
|
+
* colours, so Paint leaves it), with its code to read and a way to take it off, back to the floor's
|
|
307
|
+
* own finish: `remove` answers the removal's changes, applied as one undo step.
|
|
308
|
+
*/
|
|
309
|
+
export function ScriptedFinishRow({
|
|
310
|
+
sha,
|
|
311
|
+
remove,
|
|
312
|
+
...attributes
|
|
313
|
+
}: { sha: string; remove: () => SceneChanges | undefined } & Record<`data-${string}`, string>) {
|
|
314
|
+
const i18n = usePascalI18n()
|
|
238
315
|
const [code, setCode] = useState<{ sha: string; text: string } | null>(null)
|
|
239
316
|
const [open, setOpen] = useState(false)
|
|
240
|
-
|
|
317
|
+
const unreadable = i18n._({
|
|
318
|
+
id: 'editor.panels.scriptUnreadable',
|
|
319
|
+
message: 'The script could not be read.',
|
|
320
|
+
})
|
|
241
321
|
const toggleCode = () => {
|
|
242
322
|
setOpen((shown) => !shown)
|
|
243
323
|
if (code?.sha === sha) return
|
|
244
324
|
finishScriptCode(sha).then(
|
|
245
|
-
(text) => setCode({ sha, text: text ??
|
|
246
|
-
() => setCode({ sha, text:
|
|
325
|
+
(text) => setCode({ sha, text: text ?? unreadable }),
|
|
326
|
+
() => setCode({ sha, text: unreadable }),
|
|
247
327
|
)
|
|
248
328
|
}
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
useScene.getState().nodes,
|
|
252
|
-
{ zoneId, remove: true },
|
|
253
|
-
{ activeLevelId: null },
|
|
254
|
-
)
|
|
329
|
+
const removeScript = () => {
|
|
330
|
+
const changes = remove()
|
|
255
331
|
runAsSingleSceneHistoryStep(useScene, () => {
|
|
256
332
|
for (const { id, data } of changes?.update ?? [])
|
|
257
333
|
useScene.getState().updateNode(id as AnyNodeId, data)
|
|
258
334
|
})
|
|
259
335
|
}
|
|
260
336
|
return (
|
|
261
|
-
<div className="mt-3 flex flex-col gap-2"
|
|
337
|
+
<div className="mt-3 flex flex-col gap-2" {...attributes}>
|
|
262
338
|
<div className="flex min-h-8 items-center justify-between gap-3">
|
|
263
|
-
<span className="text-muted-foreground text-xs">Scripted finish</span>
|
|
339
|
+
<span className="text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomPanel.scriptedFinish', message: 'Scripted finish' })}</span>
|
|
264
340
|
<div className="flex items-center gap-1.5">
|
|
265
341
|
<button
|
|
266
342
|
className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
|
|
267
343
|
onClick={toggleCode}
|
|
268
344
|
type="button"
|
|
269
345
|
>
|
|
270
|
-
{open ? 'Hide code' : 'View code'}
|
|
346
|
+
{open ? i18n._({ id: 'editor.panels.roomPanel.hideCode', message: 'Hide code' }) : i18n._({ id: 'editor.panels.roomPanel.viewCode', message: 'View code' })}
|
|
271
347
|
</button>
|
|
272
348
|
<button
|
|
273
349
|
className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
|
|
274
|
-
onClick={
|
|
350
|
+
onClick={removeScript}
|
|
275
351
|
type="button"
|
|
276
352
|
>
|
|
277
|
-
Remove
|
|
353
|
+
{i18n._({ id: 'editor.panels.roomPanel.remove', message: 'Remove' })}
|
|
278
354
|
</button>
|
|
279
355
|
</div>
|
|
280
356
|
</div>
|
|
281
357
|
{open && (
|
|
282
358
|
<pre className="max-h-60 overflow-auto whitespace-pre rounded-lg border border-border/50 p-2 font-mono text-[11px] text-foreground">
|
|
283
|
-
{code?.sha === sha ? code.text : 'Loading…'}
|
|
359
|
+
{code?.sha === sha ? code.text : i18n._({ id: 'editor.panels.roomPanel.loading', message: 'Loading…' })}
|
|
284
360
|
</pre>
|
|
285
361
|
)}
|
|
286
362
|
</div>
|
|
287
363
|
)
|
|
288
364
|
}
|
|
289
365
|
|
|
290
|
-
const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
|
|
291
|
-
|
|
292
366
|
/** "2 doors · 3 windows" (the level is in the kicker). */
|
|
293
367
|
function RoomSubline({ room }: { room: RoomSelectionRecord }) {
|
|
294
|
-
const
|
|
368
|
+
const i18n = usePascalI18n()
|
|
369
|
+
const counts = useScene((scene) => {
|
|
295
370
|
const walls = new Set(room.boundaryWallIds)
|
|
296
371
|
let doors = 0
|
|
297
372
|
let windows = 0
|
|
@@ -302,7 +377,14 @@ function RoomSubline({ room }: { room: RoomSelectionRecord }) {
|
|
|
302
377
|
if (node.type === 'door') doors++
|
|
303
378
|
else windows++
|
|
304
379
|
}
|
|
305
|
-
return `${
|
|
380
|
+
return `${doors}:${windows}`
|
|
381
|
+
})
|
|
382
|
+
const [doors, windows] = counts.split(':').map(Number)
|
|
383
|
+
const text = i18n._({
|
|
384
|
+
id: 'editor.panels.roomPanel.openingCounts',
|
|
385
|
+
message:
|
|
386
|
+
'{doors, plural, one {{doorCount} door} other {{doorCount} doors}} · {windows, plural, one {{windowCount} window} other {{windowCount} windows}}',
|
|
387
|
+
values: { doors, windows, doorCount: String(doors), windowCount: String(windows) },
|
|
306
388
|
})
|
|
307
389
|
return (
|
|
308
390
|
<p className="text-muted-foreground text-xs" data-room-subline>
|
|
@@ -323,6 +405,7 @@ function ringPerimeter(ring: readonly (readonly [number, number])[]) {
|
|
|
323
405
|
|
|
324
406
|
/** Area, perimeter and inside height, in the user's units. */
|
|
325
407
|
function RoomStats({ room }: { room: RoomSelectionRecord }) {
|
|
408
|
+
const i18n = usePascalI18n()
|
|
326
409
|
const unit = useViewer((s) => s.unit)
|
|
327
410
|
const metricNotation = useViewer((s) => s.metricNotation)
|
|
328
411
|
const nodes = useScene((scene) => scene.nodes)
|
|
@@ -333,10 +416,10 @@ function RoomStats({ room }: { room: RoomSelectionRecord }) {
|
|
|
333
416
|
: 0
|
|
334
417
|
const { height } = roomInsideHeight(room, nodes)
|
|
335
418
|
const stats = [
|
|
336
|
-
{ label: 'Area', value: formatAreaLabel(room.area, unit, 2) },
|
|
337
|
-
{ label: 'Perimeter', value: formatLinearMeasurement(perimeter, unit, metricNotation) },
|
|
419
|
+
{ label: i18n._({ id: 'editor.panels.roomPanel.area', message: 'Area' }), value: formatAreaLabel(room.area, unit, 2) },
|
|
420
|
+
{ label: i18n._({ id: 'editor.panels.roomPanel.perimeter', message: 'Perimeter' }), value: formatLinearMeasurement(perimeter, unit, metricNotation) },
|
|
338
421
|
{
|
|
339
|
-
label: 'Height',
|
|
422
|
+
label: i18n._({ id: 'editor.panels.roomPanel.height', message: 'Height' }),
|
|
340
423
|
value: height === null ? '—' : formatLinearMeasurement(height, unit, metricNotation),
|
|
341
424
|
},
|
|
342
425
|
]
|
|
@@ -363,6 +446,7 @@ function RoomStats({ room }: { room: RoomSelectionRecord }) {
|
|
|
363
446
|
* it when there is headroom. No walls of its own, so no walls row.
|
|
364
447
|
*/
|
|
365
448
|
function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
|
|
449
|
+
const i18n = usePascalI18n()
|
|
366
450
|
const zoneId = room.zoneId
|
|
367
451
|
const nodes = useScene((scene) => scene.nodes)
|
|
368
452
|
const unit = useViewer((s) => s.unit)
|
|
@@ -385,12 +469,12 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
|
|
|
385
469
|
setExpanded((current) => (current === part ? null : part))
|
|
386
470
|
return (
|
|
387
471
|
<div className="flex flex-col" data-mezzanine-rows={zoneId}>
|
|
388
|
-
<h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">Built with</h3>
|
|
472
|
+
<h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomPanel.builtWith', message: 'Built with' })}</h3>
|
|
389
473
|
<div className="flex flex-col gap-1">
|
|
390
474
|
<ConstructionPart
|
|
391
475
|
expanded={expanded === 'floor'}
|
|
392
476
|
icon="/icons/floor.webp"
|
|
393
|
-
label=
|
|
477
|
+
label={i18n._({ id: 'editor.panels.roomPanel.floor', message: 'Floor' })}
|
|
394
478
|
meta={`${length(elevation)} up`}
|
|
395
479
|
on
|
|
396
480
|
onToggleExpanded={toggle('floor')}
|
|
@@ -410,12 +494,12 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
|
|
|
410
494
|
}}
|
|
411
495
|
type="button"
|
|
412
496
|
>
|
|
413
|
-
Add stairs
|
|
497
|
+
{i18n._({ id: 'editor.panels.roomPanel.addStairs', message: 'Add stairs' })}
|
|
414
498
|
</button>
|
|
415
499
|
}
|
|
416
500
|
expanded={false}
|
|
417
501
|
icon="/icons/stairs.webp"
|
|
418
|
-
label=
|
|
502
|
+
label={i18n._({ id: 'editor.panels.roomPanel.stairs', message: 'Stairs' })}
|
|
419
503
|
meta="Up from the floor below"
|
|
420
504
|
on
|
|
421
505
|
onToggleExpanded={() => {}}
|
|
@@ -424,22 +508,26 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
|
|
|
424
508
|
<ConstructionPart
|
|
425
509
|
expanded={expanded === 'ceiling'}
|
|
426
510
|
icon="/icons/ceiling.webp"
|
|
427
|
-
label=
|
|
511
|
+
label={i18n._({ id: 'editor.panels.roomPanel.ceiling', message: 'Ceiling' })}
|
|
428
512
|
meta={
|
|
429
513
|
hasCeiling
|
|
430
514
|
? ceiling === null
|
|
431
|
-
? 'Over the mezzanine'
|
|
432
|
-
:
|
|
433
|
-
|
|
515
|
+
? i18n._({ id: 'editor.panels.roomPanel.overTheMezzanine', message: 'Over the mezzanine' })
|
|
516
|
+
: i18n._({
|
|
517
|
+
id: 'editor.panels.roomPanel.headroom',
|
|
518
|
+
message: '{height} headroom',
|
|
519
|
+
values: { height: length(ceiling) },
|
|
520
|
+
})
|
|
521
|
+
: i18n._({ id: 'editor.panels.roomPanel.notEnoughHeadroom', message: 'Not enough headroom' })
|
|
434
522
|
}
|
|
435
523
|
on
|
|
436
524
|
onToggleExpanded={toggle('ceiling')}
|
|
437
525
|
part="ceiling"
|
|
438
526
|
>
|
|
439
527
|
{hasCeiling ? (
|
|
440
|
-
<PartField label=
|
|
528
|
+
<PartField label={i18n._({ id: 'editor.panels.roomPanel.shape', message: 'Shape' })}>
|
|
441
529
|
<button className={pill} onClick={() => startCeilingEdit(zoneId)} type="button">
|
|
442
|
-
Edit shape
|
|
530
|
+
{i18n._({ id: 'editor.panels.roomPanel.editShape', message: 'Edit shape' })}
|
|
443
531
|
</button>
|
|
444
532
|
</PartField>
|
|
445
533
|
) : undefined}
|
|
@@ -459,6 +547,7 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
|
|
|
459
547
|
* the label tint, never a finish (finishes are painted).
|
|
460
548
|
*/
|
|
461
549
|
function RoomColorDot({ zoneId }: { zoneId: string }) {
|
|
550
|
+
const i18n = usePascalI18n()
|
|
462
551
|
const color = useScene((state) => {
|
|
463
552
|
const zone = state.nodes[zoneId as AnyNodeId]
|
|
464
553
|
return zone?.type === 'zone' ? zone.color : undefined
|
|
@@ -466,7 +555,7 @@ function RoomColorDot({ zoneId }: { zoneId: string }) {
|
|
|
466
555
|
return (
|
|
467
556
|
<ColorDot
|
|
468
557
|
color={color ?? '#3b82f6'}
|
|
469
|
-
label=
|
|
558
|
+
label={i18n._({ id: 'editor.panels.roomPanel.roomColourUsedInThePlan', message: 'Room colour — used in the plan and labels' })}
|
|
470
559
|
onChange={(next) => useScene.getState().updateNode(zoneId as AnyNodeId, { color: next })}
|
|
471
560
|
side="bottom"
|
|
472
561
|
/>
|
|
@@ -492,30 +581,33 @@ function RoomName({
|
|
|
492
581
|
sections: readonly RoomNameSection[]
|
|
493
582
|
mezzanine: boolean
|
|
494
583
|
}) {
|
|
584
|
+
const i18n = usePascalI18n()
|
|
495
585
|
const nodes = useScene((state) => state.nodes)
|
|
496
|
-
|
|
586
|
+
// The field shows the name in the UI language; the scene keeps what was stored.
|
|
587
|
+
const display = displayRoomName(name, i18n)
|
|
588
|
+
const [value, setValue] = useState(display)
|
|
497
589
|
const [open, setOpen] = useState(false)
|
|
498
590
|
const [typing, setTyping] = useState(false)
|
|
499
591
|
const [active, setActive] = useState(-1)
|
|
500
|
-
const valueRef = useRef(
|
|
592
|
+
const valueRef = useRef(display)
|
|
501
593
|
// Enter/click commit and then blur, and blur commits too; this keeps it one rename.
|
|
502
|
-
const committedRef = useRef(
|
|
503
|
-
const originalRef = useRef(
|
|
594
|
+
const committedRef = useRef(display)
|
|
595
|
+
const originalRef = useRef(display)
|
|
504
596
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
505
597
|
const listId = useId()
|
|
506
|
-
const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing)
|
|
598
|
+
const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing, name)
|
|
507
599
|
const expanded = open && options.length > 0
|
|
508
600
|
const activeId = expanded && options[active] ? `${listId}-${active}` : undefined
|
|
509
|
-
const current = baseRoomName(
|
|
601
|
+
const current = baseRoomName(name).toLowerCase()
|
|
510
602
|
|
|
511
603
|
// A rename from elsewhere (undo, a collaborator, the plan) lands in the field
|
|
512
604
|
// unless the user is editing it (the field stays mounted across renames).
|
|
513
605
|
useEffect(() => {
|
|
514
606
|
if (document.activeElement === inputRef.current) return
|
|
515
|
-
valueRef.current =
|
|
516
|
-
committedRef.current =
|
|
517
|
-
setValue(
|
|
518
|
-
}, [
|
|
607
|
+
valueRef.current = display
|
|
608
|
+
committedRef.current = display
|
|
609
|
+
setValue(display)
|
|
610
|
+
}, [display])
|
|
519
611
|
|
|
520
612
|
// Opening and the arrow keys bring the active row into view; the pointer
|
|
521
613
|
// only highlights (scrolling under it would move the row being clicked).
|
|
@@ -541,7 +633,7 @@ function RoomName({
|
|
|
541
633
|
setTyping(false)
|
|
542
634
|
setOpen(true)
|
|
543
635
|
const all = roomNameOptions(valueRef.current, sections, false).options
|
|
544
|
-
const here = baseRoomName(
|
|
636
|
+
const here = baseRoomName(name).toLowerCase()
|
|
545
637
|
scrollToActive.current = true
|
|
546
638
|
setActive(Math.max(0, all.findIndex((option) => option.value.toLowerCase() === here)))
|
|
547
639
|
}
|
|
@@ -551,15 +643,14 @@ function RoomName({
|
|
|
551
643
|
}
|
|
552
644
|
const commit = () => {
|
|
553
645
|
const next = valueRef.current
|
|
554
|
-
|
|
646
|
+
const committed = committedRef.current
|
|
555
647
|
committedRef.current = next
|
|
556
|
-
|
|
648
|
+
commitRoomNameEdit(zoneId, name, committed, next, i18n)
|
|
557
649
|
}
|
|
558
650
|
const pick = (option: RoomNameOption) => {
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
commit()
|
|
651
|
+
const shown = commitRoomNamePick(zoneId, name, option, i18n)
|
|
652
|
+
type(shown)
|
|
653
|
+
committedRef.current = shown
|
|
563
654
|
setOpen(false)
|
|
564
655
|
setTyping(false)
|
|
565
656
|
// A pick is the answer: the field lets go. To fine-tune the name, click it
|
|
@@ -583,7 +674,9 @@ function RoomName({
|
|
|
583
674
|
event.preventDefault()
|
|
584
675
|
event.stopPropagation()
|
|
585
676
|
const chosen = expanded ? options[active] : undefined
|
|
586
|
-
|
|
677
|
+
// Enter on the typed row commits the field, as blur does; only a click
|
|
678
|
+
// on it uses the text exactly as typed.
|
|
679
|
+
if (chosen?.kind === 'suggestion') pick(chosen)
|
|
587
680
|
else inputRef.current?.blur()
|
|
588
681
|
} else if (event.key === 'Escape') {
|
|
589
682
|
event.preventDefault()
|
|
@@ -624,10 +717,9 @@ function RoomName({
|
|
|
624
717
|
) : used > 0 ? (
|
|
625
718
|
<span
|
|
626
719
|
className="shrink-0 whitespace-nowrap font-mono text-[11px] text-muted-foreground"
|
|
627
|
-
title=
|
|
720
|
+
title={i18n._({ id: 'editor.panels.roomPanel.pickingItAddsANumber', message: 'Picking it adds a number' })}
|
|
628
721
|
>
|
|
629
|
-
{used} on this level
|
|
630
|
-
</span>
|
|
722
|
+
{i18n._({ id: 'editor.panels.roomPanel.usedOnThisLevel', message: '{used} on this level', values: { used } })}</span>
|
|
631
723
|
) : null}
|
|
632
724
|
</div>
|
|
633
725
|
)
|
|
@@ -645,19 +737,27 @@ function RoomName({
|
|
|
645
737
|
)}
|
|
646
738
|
data-guide-target="room-name"
|
|
647
739
|
data-room-name-combobox
|
|
648
|
-
title={
|
|
740
|
+
title={
|
|
741
|
+
name.trim()
|
|
742
|
+
? i18n._({
|
|
743
|
+
id: 'editor.panels.roomPanel.renameNamed',
|
|
744
|
+
message: '{name} · Rename',
|
|
745
|
+
values: { name: displayRoomName(name, i18n) },
|
|
746
|
+
})
|
|
747
|
+
: i18n._({ id: 'editor.panels.roomPanel.rename', message: 'Rename' })
|
|
748
|
+
}
|
|
649
749
|
>
|
|
650
750
|
<input
|
|
651
751
|
aria-activedescendant={activeId}
|
|
652
752
|
aria-autocomplete="list"
|
|
653
753
|
aria-controls={expanded ? listId : undefined}
|
|
654
754
|
aria-expanded={expanded}
|
|
655
|
-
aria-label={mezzanine ? 'Mezzanine name' : 'Room name'}
|
|
755
|
+
aria-label={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanineName', message: 'Mezzanine name' }) : i18n._({ id: 'editor.panels.roomPanel.roomName', message: 'Room name' })}
|
|
656
756
|
autoComplete="off"
|
|
657
757
|
className="min-w-0 flex-1 text-ellipsis bg-transparent p-0 font-semibold text-foreground text-sm leading-snug tracking-tight outline-none placeholder:text-foreground"
|
|
658
758
|
data-room-name
|
|
659
759
|
maxLength={40}
|
|
660
|
-
placeholder={mezzanine ? 'Mezzanine' : 'Room'}
|
|
760
|
+
placeholder={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanine', message: 'Mezzanine' }) : i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' })}
|
|
661
761
|
ref={inputRef}
|
|
662
762
|
role="combobox"
|
|
663
763
|
spellCheck={false}
|
|
@@ -672,9 +772,8 @@ function RoomName({
|
|
|
672
772
|
type(event.target.value)
|
|
673
773
|
setTyping(true)
|
|
674
774
|
setOpen(true)
|
|
675
|
-
const next = roomNameOptions(event.target.value, sections, true)
|
|
676
775
|
scrollToActive.current = true
|
|
677
|
-
setActive(
|
|
776
|
+
setActive(roomNameOptions(event.target.value, sections, true, name).active)
|
|
678
777
|
}}
|
|
679
778
|
onFocus={() => {
|
|
680
779
|
originalRef.current = valueRef.current
|
|
@@ -689,7 +788,7 @@ function RoomName({
|
|
|
689
788
|
}}
|
|
690
789
|
/>
|
|
691
790
|
<button
|
|
692
|
-
aria-label={mezzanine ? 'Show mezzanine names' : 'Show room names'}
|
|
791
|
+
aria-label={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.showMezzanineNames', message: 'Show mezzanine names' }) : i18n._({ id: 'editor.panels.roomPanel.showRoomNames', message: 'Show room names' })}
|
|
693
792
|
className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
694
793
|
data-room-name-open
|
|
695
794
|
tabIndex={-1}
|
|
@@ -712,7 +811,7 @@ function RoomName({
|
|
|
712
811
|
</PopoverAnchor>
|
|
713
812
|
<PopoverContent
|
|
714
813
|
align="start"
|
|
715
|
-
aria-label={mezzanine ? 'Mezzanine names' : 'Room names'}
|
|
814
|
+
aria-label={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanineNames', message: 'Mezzanine names' }) : i18n._({ id: 'editor.panels.roomPanel.roomNames', message: 'Room names' })}
|
|
716
815
|
className="flex max-h-85 w-(--radix-popover-trigger-width) min-w-60 flex-col overflow-hidden p-0"
|
|
717
816
|
id={listId}
|
|
718
817
|
role="listbox"
|
|
@@ -752,12 +851,12 @@ function RoomName({
|
|
|
752
851
|
<div className="flex shrink-0 flex-wrap gap-x-3 gap-y-1 border-border border-t px-3 py-2 text-[11px] text-muted-foreground">
|
|
753
852
|
{(
|
|
754
853
|
[
|
|
755
|
-
[['↑', '↓'], 'Browse'],
|
|
756
|
-
[['Enter'], 'Pick'],
|
|
757
|
-
[['Esc'], 'Revert'],
|
|
854
|
+
[['↑', '↓'], i18n._({ id: 'editor.panels.roomPanel.browse', message: 'Browse' })],
|
|
855
|
+
[['Enter'], i18n._({ id: 'editor.panels.roomPanel.pick', message: 'Pick' })],
|
|
856
|
+
[['Esc'], i18n._({ id: 'editor.panels.roomPanel.revert', message: 'Revert' })],
|
|
758
857
|
] as const
|
|
759
858
|
).map(([keys, action]) => (
|
|
760
|
-
<span className="flex items-center gap-1" key={
|
|
859
|
+
<span className="flex items-center gap-1" key={keys[0]}>
|
|
761
860
|
{keys.map((key) => (
|
|
762
861
|
<ShortcutToken className="h-4 min-w-4 justify-center px-1 text-[9.5px]" key={key} value={key} />
|
|
763
862
|
))}
|
|
@@ -771,20 +870,28 @@ function RoomName({
|
|
|
771
870
|
}
|
|
772
871
|
|
|
773
872
|
function RoomNameOptionLabel({ option }: { option: RoomNameOption }) {
|
|
873
|
+
const i18n = usePascalI18n()
|
|
774
874
|
if (option.kind === 'typed') {
|
|
775
875
|
return (
|
|
776
876
|
<span className="min-w-0 flex-1 truncate">
|
|
777
|
-
|
|
877
|
+
{messageWithSlot(
|
|
878
|
+
i18n,
|
|
879
|
+
/* i18n */ { id: 'editor.panels.roomPanel.useTyped', message: '{use} “{name}”', values: { name: option.value } },
|
|
880
|
+
'use',
|
|
881
|
+
<span className="text-muted-foreground">
|
|
882
|
+
{i18n._({ id: 'editor.panels.roomPanel.use', message: 'Use' })}
|
|
883
|
+
</span>,
|
|
884
|
+
)}
|
|
778
885
|
</span>
|
|
779
886
|
)
|
|
780
887
|
}
|
|
781
|
-
const {
|
|
782
|
-
if (!match) return <span className="min-w-0 flex-1 truncate">{
|
|
888
|
+
const { label, match } = option
|
|
889
|
+
if (!match) return <span className="min-w-0 flex-1 truncate">{label}</span>
|
|
783
890
|
return (
|
|
784
891
|
<span className="min-w-0 flex-1 truncate">
|
|
785
|
-
{
|
|
786
|
-
<span className="font-semibold underline decoration-muted-foreground underline-offset-3">{
|
|
787
|
-
{
|
|
892
|
+
{label.slice(0, match.start)}
|
|
893
|
+
<span className="font-semibold underline decoration-muted-foreground underline-offset-3">{label.slice(match.start, match.end)}</span>
|
|
894
|
+
{label.slice(match.end)}
|
|
788
895
|
</span>
|
|
789
896
|
)
|
|
790
897
|
}
|