@pascal-app/editor 1.0.6 → 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 +18 -15
- 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
|
@@ -6,20 +6,22 @@ import {
|
|
|
6
6
|
nodeRegistry,
|
|
7
7
|
type ParamField,
|
|
8
8
|
type ParametricDescriptor,
|
|
9
|
-
useScene,
|
|
9
|
+
useScene, usePascalI18n,
|
|
10
10
|
} from '@pascal-app/core'
|
|
11
11
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
12
|
import { useCallback, useMemo } from 'react'
|
|
13
13
|
import { useShallow } from 'zustand/react/shallow'
|
|
14
|
-
import { getNodePanelModel } from '../../../lib/panel-rows'
|
|
14
|
+
import { getNodePanelModel, type MultiPanelControl } from '../../../lib/panel-rows'
|
|
15
15
|
import { selectionMatchesSessionGroup } from '../../../lib/session-groups'
|
|
16
16
|
import useSessionGroups from '../../../store/use-session-groups'
|
|
17
17
|
import { PanelSection } from '../controls/panel-section'
|
|
18
18
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
19
|
+
import { SelectControl } from '../controls/select-control'
|
|
19
20
|
import { SliderControl } from '../controls/slider-control'
|
|
20
21
|
import { resolveUniqueSelectionIds } from './homogeneous-selection'
|
|
21
22
|
import {
|
|
22
23
|
commitMultiNodeFields,
|
|
24
|
+
fieldTargetIds,
|
|
23
25
|
fieldVisibleForAll,
|
|
24
26
|
firstNumericFieldValue,
|
|
25
27
|
firstVec3FieldValue,
|
|
@@ -32,8 +34,10 @@ import { getTypeDisplay } from './node-display'
|
|
|
32
34
|
import { ParametricFieldControl } from './parametric-field-control'
|
|
33
35
|
import { PanelWrapper } from './panel-wrapper'
|
|
34
36
|
import { formatSelectionBreakdown } from './selection-breakdown'
|
|
37
|
+
import { inspectorGroupDescriptor } from '../../../i18n/node-messages'
|
|
35
38
|
|
|
36
39
|
export function MultiParametricInspector({ footer }: { footer?: React.ReactNode }) {
|
|
40
|
+
const i18n = usePascalI18n()
|
|
37
41
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
38
42
|
const setSelection = useViewer((s) => s.setSelection)
|
|
39
43
|
const nodeIds = useScene(
|
|
@@ -62,16 +66,14 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
|
|
|
62
66
|
|
|
63
67
|
if (nodeIds.length < 2 || !nodeType || !parametrics) return null
|
|
64
68
|
|
|
65
|
-
const display = getTypeDisplay(nodeType)
|
|
69
|
+
const display = getTypeDisplay(nodeType, i18n)
|
|
66
70
|
const title = matchedGroup ? `${matchedGroup.label} · ${breakdown}` : breakdown || display.label
|
|
67
71
|
|
|
68
72
|
return (
|
|
69
73
|
<PanelWrapper footer={footer} icon={display.icon} onClose={handleClose} title={title} width={320}>
|
|
70
74
|
{matchedGroup && (
|
|
71
75
|
<div className="border-border/50 border-b px-3 py-2 text-muted-foreground text-xs">
|
|
72
|
-
{
|
|
73
|
-
project.
|
|
74
|
-
</div>
|
|
76
|
+
{i18n._({ id: 'editor.panels.multiParametricInspector.labelSessionOnlyPlainClickReselects', message: '{label} (session only). Plain click reselects all members. Not saved with the project.', values: { label: matchedGroup.label } })}</div>
|
|
75
77
|
)}
|
|
76
78
|
{parametrics.groups.map((group, gi) => (
|
|
77
79
|
<MultiGroupFields
|
|
@@ -81,13 +83,20 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
|
|
|
81
83
|
nodeIds={nodeIds}
|
|
82
84
|
nodeType={nodeType}
|
|
83
85
|
parametrics={parametrics}
|
|
84
|
-
title={group.label}
|
|
86
|
+
title={i18n._(inspectorGroupDescriptor(group.label))}
|
|
85
87
|
>
|
|
86
88
|
{hasKindControls ? (
|
|
87
89
|
<MultiKindControls nodeIds={nodeIds} nodeType={nodeType} section={group.label} />
|
|
88
90
|
) : null}
|
|
89
91
|
</MultiGroupFields>
|
|
90
92
|
))}
|
|
93
|
+
{hasKindControls ? (
|
|
94
|
+
<MultiKindSections
|
|
95
|
+
groupTitles={parametrics.groups.map((group) => group.label)}
|
|
96
|
+
nodeIds={nodeIds}
|
|
97
|
+
nodeType={nodeType}
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
91
100
|
<div className="border-border/50 border-t p-3">
|
|
92
101
|
<MultiSelectionActions />
|
|
93
102
|
</div>
|
|
@@ -95,10 +104,20 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
|
|
|
95
104
|
)
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
function useMultiKindControls(nodeIds: AnyNodeId[], nodeType: AnyNode['type']) {
|
|
108
|
+
const nodes = useScene((s) => s.nodes)
|
|
109
|
+
return useMemo(() => {
|
|
110
|
+
const model = getNodePanelModel(nodeRegistry.get(nodeType))
|
|
111
|
+
if (!model?.multiControls) return []
|
|
112
|
+
const selection = nodeIds.flatMap((id) => (nodes[id] ? [nodes[id]!] : []))
|
|
113
|
+
return model.multiControls({ selection, nodes })
|
|
114
|
+
}, [nodeIds, nodeType, nodes])
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
/**
|
|
99
118
|
* The kind's own multi-selection controls for one section (the wall's
|
|
100
|
-
* Reference), from its panel model — the same
|
|
101
|
-
*
|
|
119
|
+
* Reference), from its panel model — the same control the single panel
|
|
120
|
+
* shows, mixed when the selection disagrees.
|
|
102
121
|
*/
|
|
103
122
|
function MultiKindControls({
|
|
104
123
|
nodeIds,
|
|
@@ -109,32 +128,84 @@ function MultiKindControls({
|
|
|
109
128
|
nodeType: AnyNode['type']
|
|
110
129
|
section: string
|
|
111
130
|
}) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!model?.multiControls) return []
|
|
116
|
-
const selection = nodeIds.flatMap((id) => (nodes[id] ? [nodes[id]!] : []))
|
|
117
|
-
return model.multiControls({ selection, nodes }).filter((control) => control.section === section)
|
|
118
|
-
}, [nodeIds, nodeType, nodes, section])
|
|
131
|
+
const controls = useMultiKindControls(nodeIds, nodeType).filter(
|
|
132
|
+
(control) => control.section === section,
|
|
133
|
+
)
|
|
119
134
|
return (
|
|
120
135
|
<>
|
|
121
136
|
{controls.map((control) => (
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
<MultiKindControlView control={control} key={control.id} />
|
|
138
|
+
))}
|
|
139
|
+
</>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The kind's controls whose section is none of the parametrics groups, each section its own. */
|
|
144
|
+
function MultiKindSections({
|
|
145
|
+
groupTitles,
|
|
146
|
+
nodeIds,
|
|
147
|
+
nodeType,
|
|
148
|
+
}: {
|
|
149
|
+
groupTitles: readonly string[]
|
|
150
|
+
nodeIds: AnyNodeId[]
|
|
151
|
+
nodeType: AnyNode['type']
|
|
152
|
+
}) {
|
|
153
|
+
const controls = useMultiKindControls(nodeIds, nodeType).filter(
|
|
154
|
+
(control) => !groupTitles.includes(control.section),
|
|
155
|
+
)
|
|
156
|
+
const sections = [...new Set(controls.map((control) => control.section))]
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
{sections.map((section) => (
|
|
160
|
+
<PanelSection key={section} title={section}>
|
|
161
|
+
{controls
|
|
162
|
+
.filter((control) => control.section === section)
|
|
163
|
+
.map((control) => (
|
|
164
|
+
<MultiKindControlView control={control} key={control.id} />
|
|
165
|
+
))}
|
|
166
|
+
</PanelSection>
|
|
133
167
|
))}
|
|
134
168
|
</>
|
|
135
169
|
)
|
|
136
170
|
}
|
|
137
171
|
|
|
172
|
+
function MultiKindControlView({ control }: { control: MultiPanelControl }) {
|
|
173
|
+
if (control.kind === 'readout') {
|
|
174
|
+
return (
|
|
175
|
+
<div className="flex items-center justify-between gap-2 px-2 py-1.5">
|
|
176
|
+
<span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
177
|
+
{control.label}
|
|
178
|
+
</span>
|
|
179
|
+
<span className="truncate text-[11px] text-muted-foreground tabular-nums">
|
|
180
|
+
{control.detail ? `${control.value} · ${control.detail}` : control.value}
|
|
181
|
+
</span>
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (control.kind === 'select') {
|
|
186
|
+
return (
|
|
187
|
+
<SelectControl
|
|
188
|
+
label={control.label}
|
|
189
|
+
mixed={control.value === null}
|
|
190
|
+
onChange={control.onChange}
|
|
191
|
+
options={control.options.map((option) => ({ ...option }))}
|
|
192
|
+
value={control.value ?? ''}
|
|
193
|
+
/>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
return (
|
|
197
|
+
<div className="flex flex-col gap-1">
|
|
198
|
+
<div className="px-1 font-medium text-[10px] text-muted-foreground/80">{control.label}</div>
|
|
199
|
+
<SegmentedControl
|
|
200
|
+
mixed={control.value === null}
|
|
201
|
+
onChange={control.onChange}
|
|
202
|
+
options={control.options.map((option) => ({ ...option }))}
|
|
203
|
+
value={control.value ?? ''}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
138
209
|
function MultiGroupFields({
|
|
139
210
|
defaultExpanded,
|
|
140
211
|
title,
|
|
@@ -154,9 +225,16 @@ function MultiGroupFields({
|
|
|
154
225
|
}) {
|
|
155
226
|
const genericFields = fields.filter((field) => field.kind !== 'custom')
|
|
156
227
|
const anyVisible = useScene((s) =>
|
|
157
|
-
genericFields.some((field) =>
|
|
158
|
-
|
|
159
|
-
|
|
228
|
+
genericFields.some((field) => {
|
|
229
|
+
const { visibleIf, appliesTo } = field as {
|
|
230
|
+
visibleIf?: (n: AnyNode) => boolean
|
|
231
|
+
appliesTo?: (n: AnyNode) => boolean
|
|
232
|
+
}
|
|
233
|
+
return (
|
|
234
|
+
fieldVisibleForAll(nodeIds, visibleIf, s.nodes) &&
|
|
235
|
+
fieldTargetIds(nodeIds, appliesTo, s.nodes).length > 0
|
|
236
|
+
)
|
|
237
|
+
}),
|
|
160
238
|
)
|
|
161
239
|
if ((genericFields.length === 0 || !anyVisible) && !children) return null
|
|
162
240
|
return (
|
|
@@ -195,7 +273,7 @@ function MultiGroupFields({
|
|
|
195
273
|
|
|
196
274
|
function MultiFieldRenderer({
|
|
197
275
|
field,
|
|
198
|
-
nodeIds,
|
|
276
|
+
nodeIds: selectedIds,
|
|
199
277
|
parametrics,
|
|
200
278
|
}: {
|
|
201
279
|
field: ParamField<AnyNode>
|
|
@@ -203,11 +281,21 @@ function MultiFieldRenderer({
|
|
|
203
281
|
parametrics: ParametricDescriptor<AnyNode>
|
|
204
282
|
}) {
|
|
205
283
|
const key = String(field.key)
|
|
284
|
+
// A field that names the nodes it `appliesTo` reads and edits only those.
|
|
285
|
+
const appliesTo = (field as { appliesTo?: (n: AnyNode) => boolean }).appliesTo
|
|
286
|
+
const nodeIds = useScene(
|
|
287
|
+
useShallow((s) => fieldTargetIds(selectedIds, appliesTo, s.nodes)),
|
|
288
|
+
)
|
|
206
289
|
const visible = useScene((s) =>
|
|
207
290
|
fieldVisibleForAll(nodeIds, (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf, s.nodes),
|
|
208
291
|
)
|
|
209
|
-
const
|
|
210
|
-
const
|
|
292
|
+
const fallback = field.kind === 'number' ? field.default : undefined
|
|
293
|
+
const reduced = useScene(
|
|
294
|
+
useShallow((s) => reduceFieldValue(nodeIds, key, s.nodes, fallback)),
|
|
295
|
+
)
|
|
296
|
+
const numericOrigin = useScene((s) =>
|
|
297
|
+
firstNumericFieldValue(nodeIds, key, s.nodes, fallback ?? 0),
|
|
298
|
+
)
|
|
211
299
|
const vecOrigin = useScene(useShallow((s) => firstVec3FieldValue(nodeIds, key, s.nodes)))
|
|
212
300
|
|
|
213
301
|
const preview = useCallback(
|
|
@@ -223,7 +311,7 @@ function MultiFieldRenderer({
|
|
|
223
311
|
[nodeIds, parametrics],
|
|
224
312
|
)
|
|
225
313
|
|
|
226
|
-
if (!visible) return null
|
|
314
|
+
if (!visible || nodeIds.length === 0) return null
|
|
227
315
|
|
|
228
316
|
if (field.kind === 'vec3') {
|
|
229
317
|
return (
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type BuildingNode,
|
|
7
7
|
resolveBuildingForLevel,
|
|
8
8
|
runAsSingleSceneHistoryStep,
|
|
9
|
-
useScene,
|
|
9
|
+
useScene, usePascalI18n,
|
|
10
|
+
type PascalI18n,
|
|
10
11
|
} from '@pascal-app/core'
|
|
11
12
|
import { unwrapCorner, windowMergeOffer } from '@pascal-app/core/building'
|
|
12
13
|
import { useViewer } from '@pascal-app/viewer'
|
|
@@ -57,36 +58,71 @@ function mergeRefusalNote(
|
|
|
57
58
|
refusal: { code: string; details: Record<string, unknown> },
|
|
58
59
|
nodes: Record<string, AnyNode>,
|
|
59
60
|
length: (meters: number) => string,
|
|
61
|
+
i18n: PascalI18n,
|
|
60
62
|
): string {
|
|
61
63
|
const { blockingId, limit } = refusal.details as { blockingId?: string; limit?: number }
|
|
62
64
|
const reach = length(limit ?? 0)
|
|
63
65
|
const blocking = nodes[blockingId ?? '']
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
: blocking.type === 'item'
|
|
67
|
-
? 'an item'
|
|
68
|
-
: `a ${blocking.type}`
|
|
66
|
+
// A wall's children: the openings and wall-hosted items the merge can run into.
|
|
67
|
+
const kind = blocking?.type ?? 'unknown'
|
|
69
68
|
switch (refusal.code) {
|
|
70
69
|
case 'window_already_joined':
|
|
71
|
-
return
|
|
70
|
+
return i18n._({
|
|
71
|
+
id: 'editor.panels.mergeRefusal.alreadyJoined',
|
|
72
|
+
message: 'One of them already wraps another corner. Unwrap it first.',
|
|
73
|
+
})
|
|
72
74
|
case 'window_not_on_wall':
|
|
73
|
-
return
|
|
75
|
+
return i18n._({
|
|
76
|
+
id: 'editor.panels.mergeRefusal.notOnWall',
|
|
77
|
+
message: 'A roof or dormer window cannot join another window.',
|
|
78
|
+
})
|
|
74
79
|
case 'windows_not_adjacent':
|
|
75
|
-
return
|
|
80
|
+
return i18n._({
|
|
81
|
+
id: 'editor.panels.mergeRefusal.notAdjacent',
|
|
82
|
+
message: 'They are too far apart. Windows merge with at most {reach} between them.',
|
|
83
|
+
values: { reach },
|
|
84
|
+
})
|
|
76
85
|
case 'opening_between':
|
|
77
|
-
return
|
|
86
|
+
return i18n._({
|
|
87
|
+
id: 'editor.panels.mergeRefusal.between',
|
|
88
|
+
message:
|
|
89
|
+
'{kind, select, item {There is an item between them.} door {There is a door between them.} window {There is a window between them.} shelf {There is a shelf between them.} other {There is something between them.}}',
|
|
90
|
+
values: { kind },
|
|
91
|
+
})
|
|
78
92
|
case 'walls_not_meeting':
|
|
79
|
-
return
|
|
93
|
+
return i18n._({
|
|
94
|
+
id: 'editor.panels.mergeRefusal.wallsNotMeeting',
|
|
95
|
+
message: 'Their walls do not meet at a corner.',
|
|
96
|
+
})
|
|
80
97
|
case 'corner_angle':
|
|
81
|
-
return
|
|
98
|
+
return i18n._({
|
|
99
|
+
id: 'editor.panels.mergeRefusal.cornerAngle',
|
|
100
|
+
message: 'Their walls meet at too sharp or too flat an angle for a corner window.',
|
|
101
|
+
})
|
|
82
102
|
case 'window_not_at_corner':
|
|
83
|
-
return
|
|
103
|
+
return i18n._({
|
|
104
|
+
id: 'editor.panels.mergeRefusal.notAtCorner',
|
|
105
|
+
message: 'One of them is too far from the corner. A window joins one within {reach} of it.',
|
|
106
|
+
values: { reach },
|
|
107
|
+
})
|
|
84
108
|
case 'height_exceeds_wall':
|
|
85
|
-
return
|
|
109
|
+
return i18n._({
|
|
110
|
+
id: 'editor.panels.mergeRefusal.heightExceedsWall',
|
|
111
|
+
message:
|
|
112
|
+
'The window selected first is taller than the other wall. Select the other one first.',
|
|
113
|
+
})
|
|
86
114
|
case 'opening_overlap':
|
|
87
|
-
return
|
|
115
|
+
return i18n._({
|
|
116
|
+
id: 'editor.panels.mergeRefusal.overlap',
|
|
117
|
+
message:
|
|
118
|
+
'{kind, select, item {Sliding them to the corner would overlap an item.} door {Sliding them to the corner would overlap a door.} window {Sliding them to the corner would overlap a window.} shelf {Sliding them to the corner would overlap a shelf.} other {Sliding them to the corner would overlap something.}}',
|
|
119
|
+
values: { kind },
|
|
120
|
+
})
|
|
88
121
|
default:
|
|
89
|
-
return
|
|
122
|
+
return i18n._({
|
|
123
|
+
id: 'editor.panels.mergeRefusal.cannotMerge',
|
|
124
|
+
message: 'These windows cannot be merged.',
|
|
125
|
+
})
|
|
90
126
|
}
|
|
91
127
|
}
|
|
92
128
|
|
|
@@ -97,6 +133,7 @@ function mergeRefusalNote(
|
|
|
97
133
|
* Unwrap instead; two that cannot join, the reason.
|
|
98
134
|
*/
|
|
99
135
|
function MergeWindowsAction() {
|
|
136
|
+
const i18n = usePascalI18n()
|
|
100
137
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
101
138
|
const nodes = useScene((s) => s.nodes)
|
|
102
139
|
const readOnly = useScene((s) => s.readOnly)
|
|
@@ -110,7 +147,7 @@ function MergeWindowsAction() {
|
|
|
110
147
|
className="basis-full whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-40"
|
|
111
148
|
disabled={readOnly}
|
|
112
149
|
icon={<Split className="h-4 w-4" />}
|
|
113
|
-
label=
|
|
150
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.unwrapTheCorner', message: 'Unwrap the corner' })}
|
|
114
151
|
onClick={() => {
|
|
115
152
|
writeWindowChanges({
|
|
116
153
|
create: null,
|
|
@@ -119,13 +156,16 @@ function MergeWindowsAction() {
|
|
|
119
156
|
})
|
|
120
157
|
triggerSFX('sfx:structure-build')
|
|
121
158
|
}}
|
|
122
|
-
title=
|
|
159
|
+
title={i18n._({ id: 'editor.panels.multiSelectionPanel.unwrapTheCornerBothWindowsStay', message: 'Unwrap the corner: both windows stay, each framed at the corner' })}
|
|
123
160
|
/>
|
|
124
161
|
)
|
|
125
162
|
const { merge } = offer
|
|
126
163
|
const note = offer.refusal
|
|
127
|
-
? mergeRefusalNote(
|
|
128
|
-
|
|
164
|
+
? mergeRefusalNote(
|
|
165
|
+
offer.refusal,
|
|
166
|
+
nodes,
|
|
167
|
+
(meters) => formatLinearMeasurement(meters, unit, metricNotation),
|
|
168
|
+
i18n,
|
|
129
169
|
)
|
|
130
170
|
: null
|
|
131
171
|
return (
|
|
@@ -134,7 +174,7 @@ function MergeWindowsAction() {
|
|
|
134
174
|
className="basis-full whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-40"
|
|
135
175
|
disabled={readOnly || !merge}
|
|
136
176
|
icon={<Merge className="h-4 w-4" />}
|
|
137
|
-
label=
|
|
177
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.mergeWindows', message: 'Merge windows' })}
|
|
138
178
|
onClick={() => {
|
|
139
179
|
if (!merge) return
|
|
140
180
|
runAsSingleSceneHistoryStep(useScene, () =>
|
|
@@ -150,8 +190,8 @@ function MergeWindowsAction() {
|
|
|
150
190
|
title={
|
|
151
191
|
note ??
|
|
152
192
|
(merge?.kind === 'corner'
|
|
153
|
-
? 'Merge windows into a corner window: both take the height, sill and type of the one you selected first'
|
|
154
|
-
: 'Merge windows into one across both: it keeps the type and style of the one you selected first')
|
|
193
|
+
? i18n._({ id: 'editor.panels.multiSelectionPanel.mergeWindowsIntoACornerWindow', message: 'Merge windows into a corner window: both take the height, sill and type of the one you selected first' })
|
|
194
|
+
: i18n._({ id: 'editor.panels.multiSelectionPanel.mergeWindowsIntoOneAcrossBoth', message: 'Merge windows into one across both: it keeps the type and style of the one you selected first' }))
|
|
155
195
|
}
|
|
156
196
|
/>
|
|
157
197
|
{note ? <p className="basis-full px-1 text-muted-foreground text-xs">{note}</p> : null}
|
|
@@ -160,6 +200,7 @@ function MergeWindowsAction() {
|
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
export function MultiSelectionActions() {
|
|
203
|
+
const i18n = usePascalI18n()
|
|
163
204
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
164
205
|
const sessionGroups = useSessionGroups((s) => s.groups)
|
|
165
206
|
const sceneNodes = useScene((s) => s.nodes)
|
|
@@ -182,39 +223,39 @@ export function MultiSelectionActions() {
|
|
|
182
223
|
<ActionButton
|
|
183
224
|
className="basis-full whitespace-nowrap"
|
|
184
225
|
icon={<Building2 className="h-4 w-4" />}
|
|
185
|
-
label=
|
|
226
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.selectBuilding', message: 'Select building' })}
|
|
186
227
|
onClick={() => selectBuilding(buildingId)}
|
|
187
|
-
title=
|
|
228
|
+
title={i18n._({ id: 'editor.panels.multiSelectionPanel.selectTheWholeBuildingToMove', message: 'Select the whole building to move all its levels' })}
|
|
188
229
|
/>
|
|
189
230
|
)}
|
|
190
231
|
{showGroup && (
|
|
191
232
|
<ActionButton
|
|
192
233
|
className="min-w-20 px-2"
|
|
193
234
|
icon={<Group className="h-4 w-4" />}
|
|
194
|
-
label=
|
|
235
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.group', message: 'Group' })}
|
|
195
236
|
onClick={() => groupCurrentSelection()}
|
|
196
|
-
title=
|
|
237
|
+
title={i18n._({ id: 'editor.panels.multiSelectionPanel.groupCtrlCmdG', message: 'Group (Ctrl/Cmd+G)' })}
|
|
197
238
|
/>
|
|
198
239
|
)}
|
|
199
240
|
{showUngroup && (
|
|
200
241
|
<ActionButton
|
|
201
242
|
className="min-w-20 px-2"
|
|
202
243
|
icon={<Ungroup className="h-4 w-4" />}
|
|
203
|
-
label=
|
|
244
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.ungroup', message: 'Ungroup' })}
|
|
204
245
|
onClick={() => ungroupCurrentSelection()}
|
|
205
|
-
title=
|
|
246
|
+
title={i18n._({ id: 'editor.panels.multiSelectionPanel.ungroupCtrlCmdShiftG', message: 'Ungroup (Ctrl/Cmd+Shift+G)' })}
|
|
206
247
|
/>
|
|
207
248
|
)}
|
|
208
249
|
<ActionButton
|
|
209
250
|
className="min-w-20 px-2"
|
|
210
251
|
icon={<Copy className="h-4 w-4" />}
|
|
211
|
-
label=
|
|
252
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.duplicate', message: 'Duplicate' })}
|
|
212
253
|
onClick={() => duplicateSelectionAndPickUp()}
|
|
213
254
|
/>
|
|
214
255
|
<ActionButton
|
|
215
256
|
className="min-w-20 px-2 border-red-500/40 text-red-700 dark:text-red-200 hover:bg-red-500/15"
|
|
216
257
|
icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
|
|
217
|
-
label=
|
|
258
|
+
label={i18n._({ id: 'editor.panels.multiSelectionPanel.delete', message: 'Delete' })}
|
|
218
259
|
onClick={() => deleteSelection()}
|
|
219
260
|
/>
|
|
220
261
|
</ActionGroup>
|
|
@@ -225,6 +266,7 @@ export function MultiSelectionActions() {
|
|
|
225
266
|
* Docked multi-selection panel. Includes Group / Ungroup for session selection sets.
|
|
226
267
|
*/
|
|
227
268
|
export function MultiSelectionPanel({ footer }: { footer?: React.ReactNode }) {
|
|
269
|
+
const i18n = usePascalI18n()
|
|
228
270
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
229
271
|
const setSelection = useViewer((s) => s.setSelection)
|
|
230
272
|
const sessionGroups = useSessionGroups((s) => s.groups)
|
|
@@ -246,16 +288,14 @@ export function MultiSelectionPanel({ footer }: { footer?: React.ReactNode }) {
|
|
|
246
288
|
title={
|
|
247
289
|
matchedGroup
|
|
248
290
|
? `${matchedGroup.label} · ${selectedIds.length}`
|
|
249
|
-
:
|
|
291
|
+
: i18n._({ id: 'editor.panels.multiSelectionPanel.lengthSelected', message: '{length} selected', values: { length: selectedIds.length } })
|
|
250
292
|
}
|
|
251
293
|
width={320}
|
|
252
294
|
>
|
|
253
295
|
{breakdown && <div className="px-3 py-3 text-muted-foreground text-xs">{breakdown}</div>}
|
|
254
296
|
{matchedGroup && (
|
|
255
297
|
<div className="border-border/50 border-t px-3 py-2 text-muted-foreground text-xs">
|
|
256
|
-
{
|
|
257
|
-
project.
|
|
258
|
-
</div>
|
|
298
|
+
{i18n._({ id: 'editor.panels.multiSelectionPanel.labelSessionOnlyPlainClickReselects', message: '{label} (session only). Plain click reselects all members. Not saved with the project.', values: { label: matchedGroup.label } })}</div>
|
|
259
299
|
)}
|
|
260
300
|
<div className="border-border/50 border-t p-3">
|
|
261
301
|
<MultiSelectionActions />
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import { type AnyNode, scriptImages } from '@pascal-app/core'
|
|
1
|
+
import { type AnyNode, type MessageDescriptor, type PascalI18n, scriptImages } from '@pascal-app/core'
|
|
2
|
+
import { displayRoomName } from '../../../lib/room-name-messages'
|
|
2
3
|
|
|
3
4
|
export type NodeDisplay = {
|
|
4
5
|
icon: string
|
|
5
6
|
label: string
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
const TYPE_DEFAULTS: Record<string,
|
|
9
|
-
item: { icon: '/icons/item.webp', label: 'Item' },
|
|
10
|
-
wall: { icon: '/icons/wall.webp', label: 'Wall' },
|
|
11
|
-
door: { icon: '/icons/door.webp', label: 'Door' },
|
|
12
|
-
window: { icon: '/icons/window.webp', label: 'Window' },
|
|
13
|
-
slab: { icon: '/icons/floor.webp', label: 'Slab' },
|
|
14
|
-
ceiling: { icon: '/icons/ceiling.webp', label: 'Ceiling' },
|
|
15
|
-
column: { icon: '/icons/column.webp', label: 'Column' },
|
|
16
|
-
elevator: { icon: '/icons/elevator.webp', label: 'Elevator' },
|
|
17
|
-
fence: { icon: '/icons/fence.webp', label: 'Fence' },
|
|
18
|
-
roof: { icon: '/icons/roof.webp', label: 'Roof' },
|
|
19
|
-
'roof-segment': { icon: '/icons/roof.webp', label: 'Roof segment' },
|
|
20
|
-
stair: { icon: '/icons/stairs.webp', label: 'Stair' },
|
|
21
|
-
'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
|
|
22
|
-
scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
|
|
23
|
-
guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
|
|
24
|
-
zone: { icon: '/icons/zone.webp', label: 'Zone' },
|
|
9
|
+
const TYPE_DEFAULTS: Record<string, { icon: string; label: MessageDescriptor }> = {
|
|
10
|
+
item: { icon: '/icons/item.webp', label: /* i18n */ { id: 'editor.nodeDisplay.item', message: 'Item' } },
|
|
11
|
+
wall: { icon: '/icons/wall.webp', label: /* i18n */ { id: 'editor.nodeDisplay.wall', message: 'Wall' } },
|
|
12
|
+
door: { icon: '/icons/door.webp', label: /* i18n */ { id: 'editor.nodeDisplay.door', message: 'Door' } },
|
|
13
|
+
window: { icon: '/icons/window.webp', label: /* i18n */ { id: 'editor.nodeDisplay.window', message: 'Window' } },
|
|
14
|
+
slab: { icon: '/icons/floor.webp', label: /* i18n */ { id: 'editor.nodeDisplay.slab', message: 'Slab' } },
|
|
15
|
+
ceiling: { icon: '/icons/ceiling.webp', label: /* i18n */ { id: 'editor.nodeDisplay.ceiling', message: 'Ceiling' } },
|
|
16
|
+
column: { icon: '/icons/column.webp', label: /* i18n */ { id: 'editor.nodeDisplay.column', message: 'Column' } },
|
|
17
|
+
elevator: { icon: '/icons/elevator.webp', label: /* i18n */ { id: 'editor.nodeDisplay.elevator', message: 'Elevator' } },
|
|
18
|
+
fence: { icon: '/icons/fence.webp', label: /* i18n */ { id: 'editor.nodeDisplay.fence', message: 'Fence' } },
|
|
19
|
+
roof: { icon: '/icons/roof.webp', label: /* i18n */ { id: 'editor.nodeDisplay.roof', message: 'Roof' } },
|
|
20
|
+
'roof-segment': { icon: '/icons/roof.webp', label: /* i18n */ { id: 'editor.nodeDisplay.roofSegment', message: 'Roof segment' } },
|
|
21
|
+
stair: { icon: '/icons/stairs.webp', label: /* i18n */ { id: 'editor.nodeDisplay.stair', message: 'Stair' } },
|
|
22
|
+
'stair-segment': { icon: '/icons/stairs.webp', label: /* i18n */ { id: 'editor.nodeDisplay.stairSegment', message: 'Stair segment' } },
|
|
23
|
+
scan: { icon: '/icons/mesh.webp', label: /* i18n */ { id: 'editor.nodeDisplay.scan', message: '3D Scan' } },
|
|
24
|
+
guide: { icon: '/icons/floorplan.webp', label: /* i18n */ { id: 'editor.nodeDisplay.guide', message: 'Guide image' } },
|
|
25
|
+
zone: { icon: '/icons/zone.webp', label: /* i18n */ { id: 'editor.nodeDisplay.zone', message: 'Zone' } },
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function getTypeDisplay(type: string): NodeDisplay {
|
|
28
|
-
|
|
28
|
+
export function getTypeDisplay(type: string, i18n: PascalI18n): NodeDisplay {
|
|
29
|
+
const known = TYPE_DEFAULTS[type]
|
|
30
|
+
return known ? { icon: known.icon, label: i18n._(known.label) } : { icon: '/icons/select.webp', label: type }
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
|
|
32
|
-
if (!node)
|
|
33
|
-
|
|
33
|
+
export function getNodeDisplay(node: AnyNode | null | undefined, i18n: PascalI18n): NodeDisplay {
|
|
34
|
+
if (!node)
|
|
35
|
+
return {
|
|
36
|
+
icon: '/icons/select.webp',
|
|
37
|
+
label: i18n._({ id: 'editor.nodeDisplay.selection', message: 'Selection' }),
|
|
38
|
+
}
|
|
39
|
+
const fallback = getTypeDisplay(node.type, i18n)
|
|
34
40
|
const scripted = scriptImages(node)?.thumbnail
|
|
35
41
|
// Item nodes carry an asset with its own thumbnail/name
|
|
36
42
|
if (node.type === 'item') {
|
|
@@ -40,7 +46,12 @@ export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
if (node.type === 'zone' && node.spaceRole === 'room') {
|
|
43
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
icon: '/icons/kitchen.webp',
|
|
51
|
+
label:
|
|
52
|
+
(node.name && displayRoomName(node.name, i18n)) ||
|
|
53
|
+
i18n._({ id: 'editor.nodeDisplay.room', message: 'Room' }),
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
return {
|
|
46
57
|
icon: scripted ?? fallback.icon,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type StairSegmentNode,
|
|
20
20
|
useScene,
|
|
21
21
|
type WallNode,
|
|
22
|
-
type WindowNode,
|
|
22
|
+
type WindowNode, usePascalI18n,
|
|
23
23
|
} from '@pascal-app/core'
|
|
24
24
|
import { useViewer } from '@pascal-app/viewer'
|
|
25
25
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
@@ -107,6 +107,7 @@ function MobilePanelLayer({
|
|
|
107
107
|
panel: React.ReactNode
|
|
108
108
|
isReference: boolean
|
|
109
109
|
}) {
|
|
110
|
+
const i18n = usePascalI18n()
|
|
110
111
|
const setSelection = useViewer((s) => s.setSelection)
|
|
111
112
|
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
|
112
113
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
@@ -146,7 +147,7 @@ function MobilePanelLayer({
|
|
|
146
147
|
|
|
147
148
|
if (!(node || isReference)) return null
|
|
148
149
|
|
|
149
|
-
const display = getNodeDisplay(node)
|
|
150
|
+
const display = getNodeDisplay(node, i18n)
|
|
150
151
|
|
|
151
152
|
return (
|
|
152
153
|
<>
|
|
@@ -180,8 +181,9 @@ function MobileMultiPanelLayer({
|
|
|
180
181
|
panel: React.ReactNode
|
|
181
182
|
type: string | null
|
|
182
183
|
}) {
|
|
184
|
+
const i18n = usePascalI18n()
|
|
183
185
|
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
|
184
|
-
const display = type ? getTypeDisplay(type) : { icon: '/icons/select.webp', label: 'Selection' }
|
|
186
|
+
const display = type ? getTypeDisplay(type, i18n) : { icon: '/icons/select.webp', label: i18n._({ id: 'editor.panels.panelManager.selection', message: 'Selection' }) }
|
|
185
187
|
const title = breakdown || display.label
|
|
186
188
|
|
|
187
189
|
useEffect(() => {
|