@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
|
@@ -1,31 +1,52 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNode,
|
|
4
5
|
type AnyNodeId,
|
|
5
6
|
type BuildingNode,
|
|
6
7
|
type GuideNode,
|
|
7
8
|
type LevelNode,
|
|
8
9
|
type ScanNode,
|
|
10
|
+
type MessageDescriptor,
|
|
9
11
|
useScene,
|
|
12
|
+
usePascalI18n,
|
|
10
13
|
} from '@pascal-app/core'
|
|
11
14
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
Check,
|
|
17
|
+
ChevronDown,
|
|
18
|
+
Eye,
|
|
19
|
+
EyeOff,
|
|
20
|
+
Layers2,
|
|
21
|
+
Plus,
|
|
22
|
+
SquareDashed,
|
|
23
|
+
Trash2,
|
|
24
|
+
Waypoints,
|
|
25
|
+
} from 'lucide-react'
|
|
13
26
|
import { useCallback, useRef, useState } from 'react'
|
|
14
27
|
import { useShallow } from 'zustand/react/shallow'
|
|
15
|
-
import { getLevelDisplayName } from '@pascal-app/core'
|
|
16
28
|
import { createLocalGuideImage } from '../../../lib/local-guide-image'
|
|
17
29
|
import { cn } from '../../../lib/utils'
|
|
18
30
|
import useEditor from '../../../store/use-editor'
|
|
19
31
|
import { useUploadStore } from '../../../store/use-upload'
|
|
32
|
+
import {
|
|
33
|
+
ADJACENT_WALL_COLORS,
|
|
34
|
+
adjacentLevelIds,
|
|
35
|
+
useWallsBelowSetting,
|
|
36
|
+
} from '../../editor-2d/floorplan-adjacent-walls-layer'
|
|
20
37
|
import { SliderControl } from '../controls/slider-control'
|
|
21
38
|
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
39
|
+
import { Switch } from '../primitives/switch'
|
|
22
40
|
import { ActionButton } from './action-button'
|
|
23
41
|
import { useActionMenuPopupSide } from './placement'
|
|
42
|
+
import { localizedLevelName } from '../../../i18n/level-name'
|
|
24
43
|
|
|
25
44
|
const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
|
|
26
45
|
const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif'
|
|
27
|
-
const REFERENCES_EMPTY_TEXT =
|
|
28
|
-
'
|
|
46
|
+
const REFERENCES_EMPTY_TEXT = /* i18n */ {
|
|
47
|
+
id: 'editor.actionMenu.viewToggles.referencesEmpty',
|
|
48
|
+
message: 'Upload GLB meshes as scan references or blueprint images as guide references.',
|
|
49
|
+
}
|
|
29
50
|
|
|
30
51
|
// ── Helper: get guide images for the current level ──────────────────────────
|
|
31
52
|
|
|
@@ -84,6 +105,7 @@ function useLowerReferenceLevels(): LevelNode[] {
|
|
|
84
105
|
// ── Shared upload button for dropdowns ──────────────────────────────────────
|
|
85
106
|
|
|
86
107
|
function UploadButton({ onError }: { onError: (message: string | null) => void }) {
|
|
108
|
+
const i18n = usePascalI18n()
|
|
87
109
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
88
110
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
89
111
|
const setSelection = useViewer((s) => s.setSelection)
|
|
@@ -149,7 +171,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
|
|
|
149
171
|
return (
|
|
150
172
|
<>
|
|
151
173
|
<button
|
|
152
|
-
aria-label=
|
|
174
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.uploadScanOrGuideImage', message: 'Upload scan or guide image' })}
|
|
153
175
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
154
176
|
disabled={isAddingGuide}
|
|
155
177
|
onClick={() => fileInputRef.current?.click()}
|
|
@@ -171,6 +193,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
|
|
|
171
193
|
// ── Guides toggle + dropdown ────────────────────────────────────────────────
|
|
172
194
|
|
|
173
195
|
function GuidesControl() {
|
|
196
|
+
const i18n = usePascalI18n()
|
|
174
197
|
const popupSide = useActionMenuPopupSide()
|
|
175
198
|
const showGuides = useViewer((state) => state.showGuides)
|
|
176
199
|
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
@@ -212,14 +235,18 @@ function GuidesControl() {
|
|
|
212
235
|
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
213
236
|
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
214
237
|
)}
|
|
215
|
-
label={
|
|
238
|
+
label={
|
|
239
|
+
showGuides
|
|
240
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.guidesVisible', message: 'Guides: Visible' })
|
|
241
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.guidesHidden', message: 'Guides: Hidden' })
|
|
242
|
+
}
|
|
216
243
|
onClick={() => setShowGuides(!showGuides)}
|
|
217
244
|
size="icon"
|
|
218
245
|
variant="ghost"
|
|
219
246
|
>
|
|
220
247
|
<div className="relative">
|
|
221
248
|
<img
|
|
222
|
-
alt=
|
|
249
|
+
alt={i18n._({ id: 'editor.actionMenu.viewToggles.guides', message: 'Guides' })}
|
|
223
250
|
className="h-[28px] w-[28px] object-contain"
|
|
224
251
|
src="/icons/floorplan.webp"
|
|
225
252
|
/>
|
|
@@ -233,7 +260,7 @@ function GuidesControl() {
|
|
|
233
260
|
<PopoverTrigger asChild>
|
|
234
261
|
<button
|
|
235
262
|
aria-expanded={isOpen}
|
|
236
|
-
aria-label=
|
|
263
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.guideImageSettings', message: 'Guide image settings' })}
|
|
237
264
|
className={cn(
|
|
238
265
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
239
266
|
showGuides
|
|
@@ -263,10 +290,10 @@ function GuidesControl() {
|
|
|
263
290
|
<img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
|
|
264
291
|
</span>
|
|
265
292
|
<div className="min-w-0 flex-1">
|
|
266
|
-
<p className="font-medium text-foreground text-sm">Guide images</p>
|
|
293
|
+
<p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.guideImages', message: 'Guide images' })}</p>
|
|
267
294
|
{hasGuides && (
|
|
268
295
|
<p className="text-muted-foreground text-xs">
|
|
269
|
-
{
|
|
296
|
+
{i18n._({ id: 'editor.actionMenu.viewToggles.guideCount', message: '{count, plural, one {# guide image on this level} other {# guide images on this level}}', values: { count: guides.length } })}
|
|
270
297
|
</p>
|
|
271
298
|
)}
|
|
272
299
|
</div>
|
|
@@ -303,14 +330,15 @@ function GuidesControl() {
|
|
|
303
330
|
src="/icons/floorplan.webp"
|
|
304
331
|
/>
|
|
305
332
|
<p className="truncate font-medium text-foreground text-sm">
|
|
306
|
-
{guide.name ||
|
|
333
|
+
{guide.name ||
|
|
334
|
+
i18n._({ id: 'editor.actionMenu.viewToggles.guideImageNumber', message: 'Guide image {number}', values: { number: index + 1 } })}
|
|
307
335
|
</p>
|
|
308
336
|
{selectedReferenceId === guide.id && (
|
|
309
337
|
<Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
|
|
310
338
|
)}
|
|
311
339
|
</button>
|
|
312
340
|
<button
|
|
313
|
-
aria-label=
|
|
341
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.deleteGuideImage', message: 'Delete guide image' })}
|
|
314
342
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
|
|
315
343
|
onClick={(event) => {
|
|
316
344
|
event.stopPropagation()
|
|
@@ -325,7 +353,7 @@ function GuidesControl() {
|
|
|
325
353
|
</button>
|
|
326
354
|
</div>
|
|
327
355
|
<SliderControl
|
|
328
|
-
label=
|
|
356
|
+
label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
|
|
329
357
|
max={100}
|
|
330
358
|
min={0}
|
|
331
359
|
onChange={(value) => handleOpacityChange(guide.id, value)}
|
|
@@ -339,7 +367,7 @@ function GuidesControl() {
|
|
|
339
367
|
</div>
|
|
340
368
|
) : (
|
|
341
369
|
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
|
|
342
|
-
{REFERENCES_EMPTY_TEXT}
|
|
370
|
+
{i18n._(REFERENCES_EMPTY_TEXT)}
|
|
343
371
|
</div>
|
|
344
372
|
)}
|
|
345
373
|
</div>
|
|
@@ -351,6 +379,7 @@ function GuidesControl() {
|
|
|
351
379
|
// ── Scans toggle + dropdown ─────────────────────────────────────────────────
|
|
352
380
|
|
|
353
381
|
function ScansControl() {
|
|
382
|
+
const i18n = usePascalI18n()
|
|
354
383
|
const popupSide = useActionMenuPopupSide()
|
|
355
384
|
const showScans = useViewer((state) => state.showScans)
|
|
356
385
|
const setShowScans = useViewer((state) => state.setShowScans)
|
|
@@ -392,13 +421,17 @@ function ScansControl() {
|
|
|
392
421
|
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
393
422
|
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
394
423
|
)}
|
|
395
|
-
label={
|
|
424
|
+
label={
|
|
425
|
+
showScans
|
|
426
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.scansVisible', message: 'Scans: Visible' })
|
|
427
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.scansHidden', message: 'Scans: Hidden' })
|
|
428
|
+
}
|
|
396
429
|
onClick={() => setShowScans(!showScans)}
|
|
397
430
|
size="icon"
|
|
398
431
|
variant="ghost"
|
|
399
432
|
>
|
|
400
433
|
<div className="relative">
|
|
401
|
-
<img alt=
|
|
434
|
+
<img alt={i18n._({ id: 'editor.actionMenu.viewToggles.scans', message: 'Scans' })} className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
|
|
402
435
|
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
|
|
403
436
|
{scans.length}
|
|
404
437
|
</span>
|
|
@@ -409,7 +442,7 @@ function ScansControl() {
|
|
|
409
442
|
<PopoverTrigger asChild>
|
|
410
443
|
<button
|
|
411
444
|
aria-expanded={isOpen}
|
|
412
|
-
aria-label=
|
|
445
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.scanSettings', message: 'Scan settings' })}
|
|
413
446
|
className={cn(
|
|
414
447
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
415
448
|
showScans
|
|
@@ -439,10 +472,10 @@ function ScansControl() {
|
|
|
439
472
|
<img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
|
|
440
473
|
</span>
|
|
441
474
|
<div className="min-w-0 flex-1">
|
|
442
|
-
<p className="font-medium text-foreground text-sm">Scans</p>
|
|
475
|
+
<p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.scans', message: 'Scans' })}</p>
|
|
443
476
|
{hasScans && (
|
|
444
477
|
<p className="text-muted-foreground text-xs">
|
|
445
|
-
{
|
|
478
|
+
{i18n._({ id: 'editor.actionMenu.viewToggles.scanCount', message: '{count, plural, one {# scan on this level} other {# scans on this level}}', values: { count: scans.length } })}
|
|
446
479
|
</p>
|
|
447
480
|
)}
|
|
448
481
|
</div>
|
|
@@ -479,14 +512,15 @@ function ScansControl() {
|
|
|
479
512
|
src="/icons/mesh.webp"
|
|
480
513
|
/>
|
|
481
514
|
<p className="truncate font-medium text-foreground text-sm">
|
|
482
|
-
{scan.name ||
|
|
515
|
+
{scan.name ||
|
|
516
|
+
i18n._({ id: 'editor.actionMenu.viewToggles.scanNumber', message: 'Scan {number}', values: { number: index + 1 } })}
|
|
483
517
|
</p>
|
|
484
518
|
{selectedReferenceId === scan.id && (
|
|
485
519
|
<Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
|
|
486
520
|
)}
|
|
487
521
|
</button>
|
|
488
522
|
<button
|
|
489
|
-
aria-label=
|
|
523
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.deleteScan', message: 'Delete scan' })}
|
|
490
524
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
|
|
491
525
|
onClick={(event) => {
|
|
492
526
|
event.stopPropagation()
|
|
@@ -501,7 +535,7 @@ function ScansControl() {
|
|
|
501
535
|
</button>
|
|
502
536
|
</div>
|
|
503
537
|
<SliderControl
|
|
504
|
-
label=
|
|
538
|
+
label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
|
|
505
539
|
max={100}
|
|
506
540
|
min={0}
|
|
507
541
|
onChange={(value) => handleOpacityChange(scan.id, value)}
|
|
@@ -515,7 +549,7 @@ function ScansControl() {
|
|
|
515
549
|
</div>
|
|
516
550
|
) : (
|
|
517
551
|
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
|
|
518
|
-
{REFERENCES_EMPTY_TEXT}
|
|
552
|
+
{i18n._(REFERENCES_EMPTY_TEXT)}
|
|
519
553
|
</div>
|
|
520
554
|
)}
|
|
521
555
|
</div>
|
|
@@ -532,7 +566,7 @@ function ScansControl() {
|
|
|
532
566
|
function ReferenceListSection({
|
|
533
567
|
title,
|
|
534
568
|
iconSrc,
|
|
535
|
-
|
|
569
|
+
kind,
|
|
536
570
|
emptyText,
|
|
537
571
|
nodes,
|
|
538
572
|
show,
|
|
@@ -541,13 +575,14 @@ function ReferenceListSection({
|
|
|
541
575
|
}: {
|
|
542
576
|
title: string
|
|
543
577
|
iconSrc: string
|
|
544
|
-
|
|
545
|
-
emptyText:
|
|
578
|
+
kind: 'scan' | 'guide'
|
|
579
|
+
emptyText: MessageDescriptor
|
|
546
580
|
nodes: (GuideNode | ScanNode)[]
|
|
547
581
|
show: boolean
|
|
548
582
|
setShow: (show: boolean) => void
|
|
549
583
|
onError: (message: string | null) => void
|
|
550
584
|
}) {
|
|
585
|
+
const i18n = usePascalI18n()
|
|
551
586
|
const setSelection = useViewer((state) => state.setSelection)
|
|
552
587
|
const updateNode = useScene((state) => state.updateNode)
|
|
553
588
|
const deleteNode = useScene((state) => state.deleteNode)
|
|
@@ -574,13 +609,22 @@ function ReferenceListSection({
|
|
|
574
609
|
<p className="font-medium text-foreground text-sm">{title}</p>
|
|
575
610
|
{hasItems && (
|
|
576
611
|
<p className="text-muted-foreground text-xs">
|
|
577
|
-
{
|
|
578
|
-
|
|
612
|
+
{kind === 'scan'
|
|
613
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.scanCount', message: '{count, plural, one {# scan on this level} other {# scans on this level}}', values: { count: nodes.length } })
|
|
614
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.guideCount', message: '{count, plural, one {# guide image on this level} other {# guide images on this level}}', values: { count: nodes.length } })}
|
|
579
615
|
</p>
|
|
580
616
|
)}
|
|
581
617
|
</div>
|
|
582
618
|
<button
|
|
583
|
-
aria-label={
|
|
619
|
+
aria-label={
|
|
620
|
+
kind === 'scan'
|
|
621
|
+
? show
|
|
622
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.hideScans', message: 'Hide scans' })
|
|
623
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.showScans', message: 'Show scans' })
|
|
624
|
+
: show
|
|
625
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.hideGuideImages', message: 'Hide guide images' })
|
|
626
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.showGuideImages', message: 'Show guide images' })
|
|
627
|
+
}
|
|
584
628
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
585
629
|
onClick={() => setShow(!show)}
|
|
586
630
|
type="button"
|
|
@@ -614,14 +658,21 @@ function ReferenceListSection({
|
|
|
614
658
|
src={iconSrc}
|
|
615
659
|
/>
|
|
616
660
|
<p className="truncate font-medium text-foreground text-sm">
|
|
617
|
-
{node.name ||
|
|
661
|
+
{node.name ||
|
|
662
|
+
(kind === 'scan'
|
|
663
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.scanNumber', message: 'Scan {number}', values: { number: index + 1 } })
|
|
664
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.guideImageNumber', message: 'Guide image {number}', values: { number: index + 1 } }))}
|
|
618
665
|
</p>
|
|
619
666
|
{selectedReferenceId === node.id && (
|
|
620
667
|
<Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
|
|
621
668
|
)}
|
|
622
669
|
</button>
|
|
623
670
|
<button
|
|
624
|
-
aria-label={
|
|
671
|
+
aria-label={
|
|
672
|
+
kind === 'scan'
|
|
673
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.deleteScan', message: 'Delete scan' })
|
|
674
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.deleteGuideImage', message: 'Delete guide image' })
|
|
675
|
+
}
|
|
625
676
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
|
|
626
677
|
onClick={(event) => {
|
|
627
678
|
event.stopPropagation()
|
|
@@ -636,7 +687,7 @@ function ReferenceListSection({
|
|
|
636
687
|
</button>
|
|
637
688
|
</div>
|
|
638
689
|
<SliderControl
|
|
639
|
-
label=
|
|
690
|
+
label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
|
|
640
691
|
max={100}
|
|
641
692
|
min={0}
|
|
642
693
|
onChange={(value) =>
|
|
@@ -652,7 +703,7 @@ function ReferenceListSection({
|
|
|
652
703
|
</div>
|
|
653
704
|
) : (
|
|
654
705
|
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-3 text-muted-foreground text-sm">
|
|
655
|
-
{emptyText}
|
|
706
|
+
{i18n._(emptyText)}
|
|
656
707
|
</div>
|
|
657
708
|
)}
|
|
658
709
|
</div>
|
|
@@ -660,6 +711,7 @@ function ReferenceListSection({
|
|
|
660
711
|
}
|
|
661
712
|
|
|
662
713
|
function ReferencesControl() {
|
|
714
|
+
const i18n = usePascalI18n()
|
|
663
715
|
const popupSide = useActionMenuPopupSide()
|
|
664
716
|
const showScans = useViewer((state) => state.showScans)
|
|
665
717
|
const setShowScans = useViewer((state) => state.setShowScans)
|
|
@@ -689,14 +741,18 @@ function ReferencesControl() {
|
|
|
689
741
|
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
690
742
|
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
691
743
|
)}
|
|
692
|
-
label={
|
|
744
|
+
label={
|
|
745
|
+
anyVisible
|
|
746
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.referencesVisible', message: 'References: Visible' })
|
|
747
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.referencesHidden', message: 'References: Hidden' })
|
|
748
|
+
}
|
|
693
749
|
onClick={toggleAll}
|
|
694
750
|
size="icon"
|
|
695
751
|
variant="ghost"
|
|
696
752
|
>
|
|
697
753
|
<div className="relative">
|
|
698
754
|
<img
|
|
699
|
-
alt=
|
|
755
|
+
alt={i18n._({ id: 'editor.actionMenu.viewToggles.references', message: 'References' })}
|
|
700
756
|
className="h-[28px] w-[28px] object-contain"
|
|
701
757
|
src="/icons/floorplan.webp"
|
|
702
758
|
/>
|
|
@@ -709,7 +765,7 @@ function ReferencesControl() {
|
|
|
709
765
|
<PopoverTrigger asChild>
|
|
710
766
|
<button
|
|
711
767
|
aria-expanded={isOpen}
|
|
712
|
-
aria-label=
|
|
768
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.referenceSettings', message: 'Reference settings' })}
|
|
713
769
|
className={cn(
|
|
714
770
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
715
771
|
anyVisible
|
|
@@ -743,22 +799,22 @@ function ReferencesControl() {
|
|
|
743
799
|
emptyText={REFERENCES_EMPTY_TEXT}
|
|
744
800
|
iconSrc="/icons/mesh.webp"
|
|
745
801
|
nodes={scans}
|
|
746
|
-
|
|
802
|
+
kind="scan"
|
|
747
803
|
onError={setUploadError}
|
|
748
804
|
setShow={setShowScans}
|
|
749
805
|
show={showScans}
|
|
750
|
-
title=
|
|
806
|
+
title={i18n._({ id: 'editor.actionMenu.viewToggles.scans', message: 'Scans' })}
|
|
751
807
|
/>
|
|
752
808
|
<div className="h-px bg-border/45" />
|
|
753
809
|
<ReferenceListSection
|
|
754
810
|
emptyText={REFERENCES_EMPTY_TEXT}
|
|
755
811
|
iconSrc="/icons/floorplan.webp"
|
|
756
812
|
nodes={guides}
|
|
757
|
-
|
|
813
|
+
kind="guide"
|
|
758
814
|
onError={setUploadError}
|
|
759
815
|
setShow={setShowGuides}
|
|
760
816
|
show={showGuides}
|
|
761
|
-
title=
|
|
817
|
+
title={i18n._({ id: 'editor.actionMenu.viewToggles.guideImages', message: 'Guide images' })}
|
|
762
818
|
/>
|
|
763
819
|
</div>
|
|
764
820
|
</PopoverContent>
|
|
@@ -769,6 +825,7 @@ function ReferencesControl() {
|
|
|
769
825
|
// ── Reference floor control ────────────────────────────────────────────────────────────────────
|
|
770
826
|
|
|
771
827
|
function ReferenceFloorControl() {
|
|
828
|
+
const i18n = usePascalI18n()
|
|
772
829
|
const popupSide = useActionMenuPopupSide()
|
|
773
830
|
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
|
774
831
|
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
|
@@ -780,7 +837,7 @@ function ReferenceFloorControl() {
|
|
|
780
837
|
const lowerLevels = useLowerReferenceLevels()
|
|
781
838
|
const hasLowerLevels = lowerLevels.length > 0
|
|
782
839
|
const selectedLevel = lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null
|
|
783
|
-
const selectedLevelName = selectedLevel ?
|
|
840
|
+
const selectedLevelName = selectedLevel ? localizedLevelName(selectedLevel, i18n, i18n.getLocale()) : null
|
|
784
841
|
|
|
785
842
|
return (
|
|
786
843
|
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
|
@@ -795,8 +852,8 @@ function ReferenceFloorControl() {
|
|
|
795
852
|
disabled={!hasLowerLevels}
|
|
796
853
|
label={
|
|
797
854
|
selectedLevelName && showReferenceFloor
|
|
798
|
-
?
|
|
799
|
-
: 'Reference floor'
|
|
855
|
+
? i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloorSelectedlevelname', message: 'Reference floor: {selectedLevelName}', values: { selectedLevelName } })
|
|
856
|
+
: i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloor', message: 'Reference floor' })
|
|
800
857
|
}
|
|
801
858
|
onClick={() => {
|
|
802
859
|
if (hasLowerLevels) toggleReferenceFloor()
|
|
@@ -815,7 +872,7 @@ function ReferenceFloorControl() {
|
|
|
815
872
|
<PopoverTrigger asChild>
|
|
816
873
|
<button
|
|
817
874
|
aria-expanded={isOpen}
|
|
818
|
-
aria-label=
|
|
875
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloorSettings', message: 'Reference floor settings' })}
|
|
819
876
|
className={cn(
|
|
820
877
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
821
878
|
showReferenceFloor && selectedLevel
|
|
@@ -846,13 +903,13 @@ function ReferenceFloorControl() {
|
|
|
846
903
|
<Layers2 className="h-4 w-4" />
|
|
847
904
|
</span>
|
|
848
905
|
<div className="min-w-0 flex-1">
|
|
849
|
-
<p className="font-medium text-foreground text-sm">Reference floor</p>
|
|
906
|
+
<p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloor', message: 'Reference floor' })}</p>
|
|
850
907
|
{selectedLevelName && (
|
|
851
908
|
<p className="truncate text-muted-foreground text-xs">{selectedLevelName}</p>
|
|
852
909
|
)}
|
|
853
910
|
</div>
|
|
854
911
|
<button
|
|
855
|
-
aria-label={showReferenceFloor ? 'Hide reference floor' : 'Show reference floor'}
|
|
912
|
+
aria-label={showReferenceFloor ? i18n._({ id: 'editor.actionMenu.viewToggles.hideReferenceFloor', message: 'Hide reference floor' }) : i18n._({ id: 'editor.actionMenu.viewToggles.showReferenceFloor', message: 'Show reference floor' })}
|
|
856
913
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
|
|
857
914
|
disabled={!hasLowerLevels}
|
|
858
915
|
onClick={toggleReferenceFloor}
|
|
@@ -871,7 +928,7 @@ function ReferenceFloorControl() {
|
|
|
871
928
|
<div className="max-h-44 space-y-1 overflow-y-auto rounded-xl border border-border/45 bg-background/60 p-1.5">
|
|
872
929
|
{lowerLevels.map((level, index) => {
|
|
873
930
|
const isSelected = referenceFloorOffset === index + 1
|
|
874
|
-
const levelName =
|
|
931
|
+
const levelName = localizedLevelName(level, i18n, i18n.getLocale())
|
|
875
932
|
return (
|
|
876
933
|
<button
|
|
877
934
|
className={cn(
|
|
@@ -896,14 +953,14 @@ function ReferenceFloorControl() {
|
|
|
896
953
|
)}
|
|
897
954
|
/>
|
|
898
955
|
<span className="min-w-0 flex-1 truncate">{levelName}</span>
|
|
899
|
-
<span className="text-[10px] text-muted-foreground">{index + 1}
|
|
956
|
+
<span className="text-[10px] text-muted-foreground">{i18n._({ id: 'editor.actionMenu.viewToggles.levelsBelow', message: '{count} below', values: { count: index + 1 } })}</span>
|
|
900
957
|
</button>
|
|
901
958
|
)
|
|
902
959
|
})}
|
|
903
960
|
</div>
|
|
904
961
|
|
|
905
962
|
<SliderControl
|
|
906
|
-
label=
|
|
963
|
+
label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
|
|
907
964
|
max={0.8}
|
|
908
965
|
min={0.1}
|
|
909
966
|
onChange={setReferenceFloorOpacity}
|
|
@@ -914,7 +971,7 @@ function ReferenceFloorControl() {
|
|
|
914
971
|
</>
|
|
915
972
|
) : (
|
|
916
973
|
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
|
|
917
|
-
No lower floor available.
|
|
974
|
+
{i18n._({ id: 'editor.actionMenu.viewToggles.noLowerFloorAvailable', message: 'No lower floor available.' })}
|
|
918
975
|
</div>
|
|
919
976
|
)}
|
|
920
977
|
</div>
|
|
@@ -923,9 +980,156 @@ function ReferenceFloorControl() {
|
|
|
923
980
|
)
|
|
924
981
|
}
|
|
925
982
|
|
|
983
|
+
// ── Walls below / above control ─────────────────────────────────────────────
|
|
984
|
+
|
|
985
|
+
function useAdjacentLevelNames() {
|
|
986
|
+
const i18n = usePascalI18n()
|
|
987
|
+
const levelId = useViewer((s) => s.selection.levelId)
|
|
988
|
+
return useScene(
|
|
989
|
+
useShallow((state) => {
|
|
990
|
+
const nodes = state.nodes as Record<string, AnyNode>
|
|
991
|
+
const ids = adjacentLevelIds(nodes, levelId)
|
|
992
|
+
const name = (id: string | null) => {
|
|
993
|
+
const level = id ? nodes[id] : undefined
|
|
994
|
+
return level?.type === 'level' ? localizedLevelName(level, i18n, i18n.getLocale()) : null
|
|
995
|
+
}
|
|
996
|
+
return { below: name(ids.below), above: name(ids.above) }
|
|
997
|
+
}),
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* What the "Walls below / above" button turns each side to: the sides that have a level all on, or
|
|
1003
|
+
* all off when one shows. A side with no level keeps its setting, which persists for the levels
|
|
1004
|
+
* that have one: turning the walls below on from the top storey must not trace walls above later.
|
|
1005
|
+
*/
|
|
1006
|
+
export function adjacentWallsToggle(
|
|
1007
|
+
active: boolean,
|
|
1008
|
+
levels: { below: string | null; above: string | null },
|
|
1009
|
+
): { below?: boolean; above?: boolean } {
|
|
1010
|
+
return {
|
|
1011
|
+
...(levels.below ? { below: !active } : {}),
|
|
1012
|
+
...(levels.above ? { above: !active } : {}),
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* "Walls below / above": the plan traces the walls of the neighbouring levels as dashed outlines,
|
|
1018
|
+
* blue for the level below (on a roof level, the top storey), magenta for the level above.
|
|
1019
|
+
*/
|
|
1020
|
+
function AdjacentWallsControl() {
|
|
1021
|
+
const i18n = usePascalI18n()
|
|
1022
|
+
const popupSide = useActionMenuPopupSide()
|
|
1023
|
+
const levelId = useViewer((s) => s.selection.levelId)
|
|
1024
|
+
const { shown: showBelow, setShown: setShowBelow } = useWallsBelowSetting(levelId)
|
|
1025
|
+
const showAbove = useEditor((state) => state.showWallsAbove)
|
|
1026
|
+
const setShowAbove = useEditor((state) => state.setShowWallsAbove)
|
|
1027
|
+
const names = useAdjacentLevelNames()
|
|
1028
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
1029
|
+
const active = (showBelow && !!names.below) || (showAbove && !!names.above)
|
|
1030
|
+
const available = !!(names.below || names.above)
|
|
1031
|
+
const rows = [
|
|
1032
|
+
{
|
|
1033
|
+
key: 'below',
|
|
1034
|
+
label: i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelow', message: 'Walls below' }),
|
|
1035
|
+
level: names.below,
|
|
1036
|
+
checked: showBelow,
|
|
1037
|
+
set: setShowBelow,
|
|
1038
|
+
color: ADJACENT_WALL_COLORS.below,
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
key: 'above',
|
|
1042
|
+
label: i18n._({ id: 'editor.actionMenu.viewToggles.wallsAbove', message: 'Walls above' }),
|
|
1043
|
+
level: names.above,
|
|
1044
|
+
checked: showAbove,
|
|
1045
|
+
set: setShowAbove,
|
|
1046
|
+
color: ADJACENT_WALL_COLORS.above,
|
|
1047
|
+
},
|
|
1048
|
+
] as const
|
|
1049
|
+
|
|
1050
|
+
return (
|
|
1051
|
+
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
|
1052
|
+
<div className="flex items-center">
|
|
1053
|
+
<ActionButton
|
|
1054
|
+
className={cn(
|
|
1055
|
+
'rounded-r-none p-0',
|
|
1056
|
+
active
|
|
1057
|
+
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
1058
|
+
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
1059
|
+
)}
|
|
1060
|
+
disabled={!available}
|
|
1061
|
+
label={active ? i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelowAndAboveShown', message: 'Walls below and above: shown' }) : i18n._({ id: 'editor.actionMenu.viewToggles.showWallsBelowAndAbove', message: 'Show walls below and above' })}
|
|
1062
|
+
onClick={() => {
|
|
1063
|
+
const next = adjacentWallsToggle(active, names)
|
|
1064
|
+
if (next.below !== undefined) setShowBelow(next.below)
|
|
1065
|
+
if (next.above !== undefined) setShowAbove(next.above)
|
|
1066
|
+
}}
|
|
1067
|
+
size="icon"
|
|
1068
|
+
variant="ghost"
|
|
1069
|
+
>
|
|
1070
|
+
<SquareDashed className="h-4 w-4" />
|
|
1071
|
+
</ActionButton>
|
|
1072
|
+
<PopoverTrigger asChild>
|
|
1073
|
+
<button
|
|
1074
|
+
aria-expanded={isOpen}
|
|
1075
|
+
aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelowAndAboveSettings', message: 'Walls below and above settings' })}
|
|
1076
|
+
className={cn(
|
|
1077
|
+
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
1078
|
+
active
|
|
1079
|
+
? 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
|
|
1080
|
+
: 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
|
|
1081
|
+
)}
|
|
1082
|
+
disabled={!available}
|
|
1083
|
+
type="button"
|
|
1084
|
+
>
|
|
1085
|
+
<ChevronDown className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')} />
|
|
1086
|
+
</button>
|
|
1087
|
+
</PopoverTrigger>
|
|
1088
|
+
</div>
|
|
1089
|
+
<PopoverContent
|
|
1090
|
+
align="center"
|
|
1091
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
1092
|
+
side={popupSide}
|
|
1093
|
+
sideOffset={14}
|
|
1094
|
+
>
|
|
1095
|
+
<div className="space-y-3" data-adjacent-walls-settings>
|
|
1096
|
+
<div>
|
|
1097
|
+
<p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelowAndAbove', message: 'Walls below and above' })}</p>
|
|
1098
|
+
<p className="text-muted-foreground text-xs">
|
|
1099
|
+
{i18n._({ id: 'editor.actionMenu.viewToggles.dashedOutlinesOfTheNeighbouringLevels', message: 'Dashed outlines of the neighbouring levels’ walls, to line walls up and check a roof against the walls under it.' })}
|
|
1100
|
+
</p>
|
|
1101
|
+
</div>
|
|
1102
|
+
{rows.map((row) => (
|
|
1103
|
+
<label className="flex items-center gap-3" key={row.key}>
|
|
1104
|
+
<span
|
|
1105
|
+
aria-hidden
|
|
1106
|
+
className="h-0 w-5 shrink-0 border-t-2 border-dashed"
|
|
1107
|
+
style={{ borderColor: row.color }}
|
|
1108
|
+
/>
|
|
1109
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
1110
|
+
<span className="text-foreground text-sm">{row.label}</span>
|
|
1111
|
+
<span className="truncate text-muted-foreground text-xs">
|
|
1112
|
+
{row.level ?? (row.key === 'below' ? i18n._({ id: 'editor.actionMenu.viewToggles.noLevelBelow', message: 'No level below' }) : i18n._({ id: 'editor.actionMenu.viewToggles.noLevelAbove', message: 'No level above' }))}
|
|
1113
|
+
</span>
|
|
1114
|
+
</span>
|
|
1115
|
+
<Switch
|
|
1116
|
+
aria-label={row.label}
|
|
1117
|
+
checked={row.checked}
|
|
1118
|
+
disabled={!row.level}
|
|
1119
|
+
onCheckedChange={row.set}
|
|
1120
|
+
/>
|
|
1121
|
+
</label>
|
|
1122
|
+
))}
|
|
1123
|
+
</div>
|
|
1124
|
+
</PopoverContent>
|
|
1125
|
+
</Popover>
|
|
1126
|
+
)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
926
1129
|
// ── Riser diagram control ────────────────────────────────────────────────────
|
|
927
1130
|
|
|
928
1131
|
function RiserControl() {
|
|
1132
|
+
const i18n = usePascalI18n()
|
|
929
1133
|
const isRiserOpen = useEditor((state) => state.isRiserOpen)
|
|
930
1134
|
const toggleRiserOpen = useEditor((state) => state.toggleRiserOpen)
|
|
931
1135
|
|
|
@@ -936,7 +1140,7 @@ function RiserControl() {
|
|
|
936
1140
|
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
937
1141
|
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
938
1142
|
)}
|
|
939
|
-
label=
|
|
1143
|
+
label={i18n._({ id: 'editor.actionMenu.viewToggles.riserDiagram', message: 'Riser diagram' })}
|
|
940
1144
|
onClick={toggleRiserOpen}
|
|
941
1145
|
size="icon"
|
|
942
1146
|
variant="ghost"
|
|
@@ -962,6 +1166,7 @@ export function ViewToggles() {
|
|
|
962
1166
|
<ScansControl />
|
|
963
1167
|
<GuidesControl />
|
|
964
1168
|
<ReferenceFloorControl />
|
|
1169
|
+
<AdjacentWallsControl />
|
|
965
1170
|
<RiserControl />
|
|
966
1171
|
</div>
|
|
967
1172
|
)
|