@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,7 +6,7 @@ import {
|
|
|
6
6
|
floorPlacedCollides,
|
|
7
7
|
nodeRegistry,
|
|
8
8
|
type TerrainVerb,
|
|
9
|
-
useScene,
|
|
9
|
+
useScene, usePascalI18n,
|
|
10
10
|
} from '@pascal-app/core'
|
|
11
11
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
12
|
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
terrainHudTitle,
|
|
43
43
|
toolHudTitle,
|
|
44
44
|
} from '../../../lib/hud-title'
|
|
45
|
-
import type { ReshapeKind } from '../../../lib/interaction/scope'
|
|
45
|
+
import type { InteractionScope, ReshapeKind } from '../../../lib/interaction/scope'
|
|
46
46
|
import {
|
|
47
47
|
type MezzanineShape,
|
|
48
48
|
useMezzanineDraft,
|
|
@@ -94,7 +94,7 @@ function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
|
|
|
94
94
|
: 'Move corner'
|
|
95
95
|
return [
|
|
96
96
|
{ keys: ['Drag'], label: action },
|
|
97
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
97
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
98
98
|
]
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -106,8 +106,8 @@ function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
|
|
|
106
106
|
function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualShortcutHint[] {
|
|
107
107
|
if (sampling) {
|
|
108
108
|
return [
|
|
109
|
-
{ keys: ['Click'], label: 'Pick target height' },
|
|
110
|
-
{ keys: ['Esc'], label: 'Cancel picking' },
|
|
109
|
+
{ keys: ['Click'], label: /* i18n */ { id: 'editor.helpers.helperManager.pickTargetHeight', message: 'Pick target height' } },
|
|
110
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancelPicking', message: 'Cancel picking' } },
|
|
111
111
|
]
|
|
112
112
|
}
|
|
113
113
|
const action =
|
|
@@ -122,8 +122,8 @@ function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualSho
|
|
|
122
122
|
{ keys: ['Drag'], label: action },
|
|
123
123
|
// Nested, so the two render as alternatives ("[ / ]") rather than a chord —
|
|
124
124
|
// a flat `['[', ']']` joins with "+" and would read as "press both".
|
|
125
|
-
{ keys: [['[', ']']], label: 'Brush size' },
|
|
126
|
-
{ keys: ['Esc'], label: 'Cancel stroke' },
|
|
125
|
+
{ keys: [['[', ']']], label: /* i18n */ { id: 'editor.helpers.helperManager.brushSize', message: 'Brush size' } },
|
|
126
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancelStroke', message: 'Cancel stroke' } },
|
|
127
127
|
]
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -141,12 +141,12 @@ const EMPTY_CONTEXTUAL_HINTS: ContextualShortcutHint[] = []
|
|
|
141
141
|
export function paintHints(mode: PaintMode): ContextualShortcutHint[] {
|
|
142
142
|
if (mode === 'pick')
|
|
143
143
|
return [
|
|
144
|
-
{ keys: ['Left click'], label: 'Paint with this material' },
|
|
145
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
144
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.paintWithThisMaterial', message: 'Paint with this material' } },
|
|
145
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
146
146
|
]
|
|
147
147
|
if (mode === 'erase')
|
|
148
|
-
return [{ keys: ['Left click'], label: 'Remove a painted part, or reset a surface' }]
|
|
149
|
-
if (mode === 'surface') return [{ keys: [['Alt', 'Option']], label: 'Hold to pick a material' }]
|
|
148
|
+
return [{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeAPaintedPartOrReset', message: 'Remove a painted part, or reset a surface' } }]
|
|
149
|
+
if (mode === 'surface') return [{ keys: [['Alt', 'Option']], label: /* i18n */ { id: 'editor.helpers.helperManager.holdToPickAMaterial', message: 'Hold to pick a material' } }]
|
|
150
150
|
return []
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -156,43 +156,45 @@ export function paintRegionHints(
|
|
|
156
156
|
hovering = true,
|
|
157
157
|
): ContextualShortcutHint[] {
|
|
158
158
|
const gesture: Record<typeof mode, ContextualShortcutHint> = {
|
|
159
|
-
rectangle: { keys: ['Drag'], label: 'Press anywhere, drag to the opposite corner' },
|
|
160
|
-
polygon: { keys: ['Left click'], label: 'Add point · click the first to close' },
|
|
159
|
+
rectangle: { keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.pressAnywhereDragToTheOpposite', message: 'Press anywhere, drag to the opposite corner' } },
|
|
160
|
+
polygon: { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPointClickTheFirstTo', message: 'Add point · click the first to close' } },
|
|
161
161
|
}
|
|
162
162
|
// Off anything the sub-mode draws on, the HUD says where to go instead.
|
|
163
163
|
if (!hovering)
|
|
164
164
|
return [
|
|
165
165
|
...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
|
|
166
166
|
{ keys: ['Hover'], label: paintRegionHoverHint(mode), active: true },
|
|
167
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
167
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
168
168
|
]
|
|
169
169
|
return [
|
|
170
170
|
...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
|
|
171
171
|
gesture[mode],
|
|
172
|
-
...(mode === 'polygon' ? [{ keys: ['Backspace'], label: 'Remove last point' }] : []),
|
|
173
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
172
|
+
...(mode === 'polygon' ? [{ keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } }] : []),
|
|
173
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
174
174
|
]
|
|
175
175
|
}
|
|
176
|
+
const DRAW_MEZZANINE = /* i18n */ { id: 'editor.helpers.helperManager.drawTheMezzanine', message: 'Draw the mezzanine' }
|
|
177
|
+
const DRAW_OPENING = /* i18n */ { id: 'editor.helpers.helperManager.drawTheOpening', message: 'Draw the opening' }
|
|
176
178
|
// "Add mezzanine": the outline gesture for the shape picked in the room panel,
|
|
177
179
|
// then Esc; the snapping chip sits below.
|
|
178
180
|
export function mezzanineHints(shape: MezzanineShape): ContextualShortcutHint[] {
|
|
179
181
|
return shape === 'rectangle'
|
|
180
182
|
? [
|
|
181
|
-
{ keys: ['Left click'], label: 'Set one corner, then the opposite' },
|
|
182
|
-
{ keys: ['Drag'], label:
|
|
183
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
183
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.setOneCornerThenTheOpposite', message: 'Set one corner, then the opposite' } },
|
|
184
|
+
{ keys: ['Drag'], label: DRAW_MEZZANINE },
|
|
185
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
184
186
|
]
|
|
185
187
|
: [
|
|
186
|
-
{ keys: ['Left click'], label: 'Add point · click the first to close' },
|
|
187
|
-
{ keys: ['Backspace'], label: 'Remove last point' },
|
|
188
|
-
{ keys: ['Enter'], label: 'Finish' },
|
|
189
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
188
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPointClickTheFirstTo', message: 'Add point · click the first to close' } },
|
|
189
|
+
{ keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } },
|
|
190
|
+
{ keys: ['Enter'], label: /* i18n */ { id: 'editor.helpers.helperManager.finish', message: 'Finish' } },
|
|
191
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
190
192
|
]
|
|
191
193
|
}
|
|
192
194
|
/** "Cut opening": the outline gesture for the chosen shape, then Esc. */
|
|
193
195
|
export function openingHints(shape: OpeningShape): ContextualShortcutHint[] {
|
|
194
196
|
return mezzanineHints(shape).map((hint) =>
|
|
195
|
-
hint.label
|
|
197
|
+
typeof hint.label !== 'string' && hint.label.id === DRAW_MEZZANINE.id ? { ...hint, label: DRAW_OPENING } : hint,
|
|
196
198
|
)
|
|
197
199
|
}
|
|
198
200
|
const TERRACE_CHIP_HINTS = [TERRACE_SHAPE_HINT]
|
|
@@ -200,22 +202,22 @@ const TERRACE_CHIP_HINTS = [TERRACE_SHAPE_HINT]
|
|
|
200
202
|
export function terraceHints(shape: TerraceShape): ContextualShortcutHint[] {
|
|
201
203
|
return shape === 'rectangle'
|
|
202
204
|
? [
|
|
203
|
-
{ keys: ['Left click'], label: 'Set one corner, then the opposite' },
|
|
204
|
-
{ keys: ['Drag'], label: 'Draw the terrace' },
|
|
205
|
-
{ keys: ['Esc'], label: 'Done' },
|
|
205
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.setOneCornerThenTheOpposite', message: 'Set one corner, then the opposite' } },
|
|
206
|
+
{ keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.drawTheTerrace', message: 'Draw the terrace' } },
|
|
207
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.done', message: 'Done' } },
|
|
206
208
|
]
|
|
207
209
|
: [
|
|
208
|
-
{ keys: ['Left click'], label: 'Add point · click the first to close' },
|
|
209
|
-
{ keys: ['Backspace'], label: 'Remove last point' },
|
|
210
|
-
{ keys: ['Enter'], label: 'Finish' },
|
|
211
|
-
{ keys: ['Esc'], label: 'Done' },
|
|
210
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPointClickTheFirstTo', message: 'Add point · click the first to close' } },
|
|
211
|
+
{ keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } },
|
|
212
|
+
{ keys: ['Enter'], label: /* i18n */ { id: 'editor.helpers.helperManager.finish', message: 'Finish' } },
|
|
213
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.done', message: 'Done' } },
|
|
212
214
|
]
|
|
213
215
|
}
|
|
214
216
|
export const ROOM_DIVIDE_HINTS: ContextualShortcutHint[] = [
|
|
215
|
-
{ keys: ['Left click'], label: 'Add point' },
|
|
216
|
-
{ keys: ['Backspace'], label: 'Remove last point' },
|
|
217
|
-
{ keys: ['Enter'], label: 'Finish at the edge' },
|
|
218
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
217
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPoint', message: 'Add point' } },
|
|
218
|
+
{ keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } },
|
|
219
|
+
{ keys: ['Enter'], label: /* i18n */ { id: 'editor.helpers.helperManager.finishAtTheEdge', message: 'Finish at the edge' } },
|
|
220
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
219
221
|
]
|
|
220
222
|
const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
|
|
221
223
|
|
|
@@ -277,22 +279,42 @@ export function mezzanineGesture(
|
|
|
277
279
|
|
|
278
280
|
export const MEZZANINE_GESTURE_HINTS: Record<MezzanineGesture, ContextualShortcutHint[]> = {
|
|
279
281
|
raise: [
|
|
280
|
-
{ keys: ['Drag'], label: 'Raise or lower' },
|
|
281
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
282
|
+
{ keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.raiseOrLower', message: 'Raise or lower' } },
|
|
283
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
282
284
|
],
|
|
283
285
|
resize: [
|
|
284
|
-
{ keys: ['Drag'], label: 'Resize' },
|
|
285
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
286
|
+
{ keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.resize', message: 'Resize' } },
|
|
287
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
286
288
|
],
|
|
287
289
|
move: [
|
|
288
|
-
{ keys: ['Left click'], label: 'Place inside the room' },
|
|
289
|
-
{ keys: [['R', 'T']], label: 'Rotate ±45°' },
|
|
290
|
-
{ keys: ['Alt'], label: 'Free move' },
|
|
291
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
290
|
+
{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.placeInsideTheRoom', message: 'Place inside the room' } },
|
|
291
|
+
{ keys: [['R', 'T']], label: /* i18n */ { id: 'editor.helpers.helperManager.rotate45', message: 'Rotate ±45°' } },
|
|
292
|
+
{ keys: ['Alt'], label: /* i18n */ { id: 'editor.helpers.helperManager.freeMove', message: 'Free move' } },
|
|
293
|
+
{ keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
|
|
292
294
|
],
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Whether the armed tool's own hints (`toolHints`) win over the node in hand: a registry tool that
|
|
299
|
+
* holds its draft in a placing scope (the roof's footprint, a fitting, a block) says what its clicks
|
|
300
|
+
* do, where the generic rows misnamed it ("Move roof · Left click: Place" while a click draws a
|
|
301
|
+
* footprint).
|
|
302
|
+
*/
|
|
303
|
+
export function toolHintsOwnPlacing(
|
|
304
|
+
scope: InteractionScope,
|
|
305
|
+
tool: string | null,
|
|
306
|
+
toolHintCount: number,
|
|
307
|
+
): boolean {
|
|
308
|
+
return (
|
|
309
|
+
scope.kind === 'placing' &&
|
|
310
|
+
scope.driver === 'registry-tool' &&
|
|
311
|
+
scope.nodeType === tool &&
|
|
312
|
+
toolHintCount > 0
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
export function HelperManager() {
|
|
317
|
+
const i18n = usePascalI18n()
|
|
296
318
|
const mode = useEditor((s) => s.mode)
|
|
297
319
|
const tool = useEditor((s) => s.tool)
|
|
298
320
|
const terrainVerb = useEditor((s) => s.terrainVerb)
|
|
@@ -417,7 +439,7 @@ export function HelperManager() {
|
|
|
417
439
|
if (activeHandleDrag?.label === GROUP_MOVE_DRAG_LABEL) {
|
|
418
440
|
return (
|
|
419
441
|
<ContextualHelperPanel
|
|
420
|
-
hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
|
|
442
|
+
hints={[{ keys: ['R / T'], label: i18n._({ id: 'editor.helpers.helperManager.rotateTheSelection45', message: 'Rotate the selection ±45°' }) }]}
|
|
421
443
|
snapContext={snapContext}
|
|
422
444
|
title={MOVE_SELECTION_HUD_TITLE}
|
|
423
445
|
/>
|
|
@@ -440,10 +462,10 @@ export function HelperManager() {
|
|
|
440
462
|
return (
|
|
441
463
|
<ContextualHelperPanel
|
|
442
464
|
hints={[
|
|
443
|
-
{ keys: ['Left click'], label: 'Place' },
|
|
444
|
-
{ keys: [['R', 'T']], label: 'Rotate ±45°' },
|
|
445
|
-
{ keys: ['Alt'], label: 'Free move · slide doors and windows' },
|
|
446
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
465
|
+
{ keys: ['Left click'], label: i18n._({ id: 'editor.helpers.helperManager.place', message: 'Place' }) },
|
|
466
|
+
{ keys: [['R', 'T']], label: i18n._({ id: 'editor.helpers.helperManager.rotate45', message: 'Rotate ±45°' }) },
|
|
467
|
+
{ keys: ['Alt'], label: i18n._({ id: 'editor.helpers.helperManager.freeMoveSlideDoorsAndWindows', message: 'Free move · slide doors and windows' }) },
|
|
468
|
+
{ keys: ['Esc'], label: i18n._({ id: 'editor.helpers.helperManager.cancel', message: 'Cancel' }) },
|
|
447
469
|
]}
|
|
448
470
|
snapContext={snapContext}
|
|
449
471
|
title={MOVE_ROOM_HUD_TITLE}
|
|
@@ -504,8 +526,8 @@ export function HelperManager() {
|
|
|
504
526
|
return (
|
|
505
527
|
<ContextualHelperPanel
|
|
506
528
|
hints={[
|
|
507
|
-
{ keys: ['Drag'], label: 'Resize' },
|
|
508
|
-
{ keys: ['Esc'], label: 'Cancel' },
|
|
529
|
+
{ keys: ['Drag'], label: i18n._({ id: 'editor.helpers.helperManager.resize', message: 'Resize' }) },
|
|
530
|
+
{ keys: ['Esc'], label: i18n._({ id: 'editor.helpers.helperManager.cancel', message: 'Cancel' }) },
|
|
509
531
|
]}
|
|
510
532
|
snapContext={snapContext}
|
|
511
533
|
title={RESIZE_HUD_TITLE}
|
|
@@ -522,7 +544,7 @@ export function HelperManager() {
|
|
|
522
544
|
? nodeRegistry.get(reshapingNode.type)?.affordanceHints?.[scope.reshape]
|
|
523
545
|
: undefined
|
|
524
546
|
const title = reshapingNode
|
|
525
|
-
? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? '
|
|
547
|
+
? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? 'split' : 'edit')
|
|
526
548
|
: null
|
|
527
549
|
if (hints) {
|
|
528
550
|
return (
|
|
@@ -543,12 +565,15 @@ export function HelperManager() {
|
|
|
543
565
|
)
|
|
544
566
|
}
|
|
545
567
|
|
|
546
|
-
if (
|
|
568
|
+
if (
|
|
569
|
+
movingNode &&
|
|
570
|
+
!toolHintsOwnPlacing(scope, tool, tool ? (nodeRegistry.get(tool)?.toolHints?.length ?? 0) : 0)
|
|
571
|
+
) {
|
|
547
572
|
// A fresh placement (e.g. a positioned preset like a shelf) advertises its
|
|
548
573
|
// once/repeat continuation, exactly like the GLB item tool — but an existing
|
|
549
574
|
// node being *moved* is not a placement, so it gets no continuation chip.
|
|
550
575
|
const isFreshPlacement = isFreshPlacementMetadata(movingNode.metadata)
|
|
551
|
-
const movingTitle = nodeKindHudTitle(movingNode.type, isFreshPlacement ? undefined : '
|
|
576
|
+
const movingTitle = nodeKindHudTitle(movingNode.type, isFreshPlacement ? undefined : 'move')
|
|
552
577
|
if (movingNode.type === 'building') return <BuildingHelper showRotate title={movingTitle} />
|
|
553
578
|
const movingContinuationContext = isFreshPlacement
|
|
554
579
|
? keyCyclableContinuationContext(continuationContextOf(movingNode.type))
|
|
@@ -611,7 +636,7 @@ export function HelperManager() {
|
|
|
611
636
|
<ContextualHelperPanel
|
|
612
637
|
hints={contextualEditHints}
|
|
613
638
|
snapContext={snapContext}
|
|
614
|
-
title={contextualHelpNode ? nodeKindHudTitle(contextualHelpNode.type, '
|
|
639
|
+
title={contextualHelpNode ? nodeKindHudTitle(contextualHelpNode.type, 'edit') : null}
|
|
615
640
|
/>
|
|
616
641
|
)
|
|
617
642
|
}
|
|
@@ -639,9 +664,9 @@ export function HelperManager() {
|
|
|
639
664
|
return (
|
|
640
665
|
<ContextualHelperPanel
|
|
641
666
|
hints={[
|
|
642
|
-
{ keys: ['Hover'], label: 'Inspect surface dimensions' },
|
|
643
|
-
{ keys: ['Click'], label: 'Pin measurement lens' },
|
|
644
|
-
{ keys: ['Esc'], label: 'Exit smart measure' },
|
|
667
|
+
{ keys: ['Hover'], label: i18n._({ id: 'editor.helpers.helperManager.inspectSurfaceDimensions', message: 'Inspect surface dimensions' }) },
|
|
668
|
+
{ keys: ['Click'], label: i18n._({ id: 'editor.helpers.helperManager.pinMeasurementLens', message: 'Pin measurement lens' }) },
|
|
669
|
+
{ keys: ['Esc'], label: i18n._({ id: 'editor.helpers.helperManager.exitSmartMeasure', message: 'Exit smart measure' }) },
|
|
645
670
|
]}
|
|
646
671
|
title={SMART_MEASURE_HUD_TITLE}
|
|
647
672
|
/>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
1
2
|
import type { ContinuationContext } from '../../../lib/continuation'
|
|
2
3
|
import type { HudTitle } from '../../../lib/hud-title'
|
|
3
4
|
import type { SnapContext } from '../../../lib/snapping-mode'
|
|
@@ -33,17 +34,18 @@ export function ItemHelper({
|
|
|
33
34
|
extraHints = [],
|
|
34
35
|
replacesPlace = false,
|
|
35
36
|
}: ItemHelperProps) {
|
|
37
|
+
const i18n = usePascalI18n()
|
|
36
38
|
return (
|
|
37
39
|
<ContextualHelperPanel
|
|
38
40
|
continuationContext={continuationContext}
|
|
39
41
|
notice={notice}
|
|
40
42
|
hints={[
|
|
41
43
|
// Where a click does something else (joining a corner), that row says so instead.
|
|
42
|
-
...(replacesPlace ? [] : [{ keys: ['Left click'], label: 'Place' }]),
|
|
44
|
+
...(replacesPlace ? [] : [{ keys: ['Left click'], label: i18n._({ id: 'editor.helpers.itemHelper.place', message: 'Place' }) }]),
|
|
43
45
|
...extraHints,
|
|
44
|
-
{ keys: ['R', 'T'], label: 'Rotate' },
|
|
45
|
-
...(showForce ? [{ keys: ['Alt'], label: 'Force place' }] : []),
|
|
46
|
-
{ keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
|
|
46
|
+
{ keys: ['R', 'T'], label: i18n._({ id: 'editor.helpers.itemHelper.rotate', message: 'Rotate' }) },
|
|
47
|
+
...(showForce ? [{ keys: ['Alt'], label: i18n._({ id: 'editor.helpers.itemHelper.forcePlace', message: 'Force place' }) }] : []),
|
|
48
|
+
{ keys: [showEsc ? 'Esc' : 'Right click'], label: i18n._({ id: 'editor.helpers.itemHelper.cancel', message: 'Cancel' }) },
|
|
47
49
|
]}
|
|
48
50
|
snapContext={snapContext}
|
|
49
51
|
title={title}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
1
2
|
import type { ToolHint } from '@pascal-app/core'
|
|
2
3
|
import type { ContinuationContext } from '../../../lib/continuation'
|
|
3
4
|
import type { HudTitle } from '../../../lib/hud-title'
|
|
@@ -29,6 +30,7 @@ export function RegisteredToolHelper({
|
|
|
29
30
|
title?: HudTitle | null
|
|
30
31
|
notice?: string | null
|
|
31
32
|
}) {
|
|
33
|
+
const i18n = usePascalI18n()
|
|
32
34
|
const visible = useVisibleToolHints(hints)
|
|
33
35
|
if (visible.length === 0 && !snapContext && !continuationContext && !notice) return null
|
|
34
36
|
// Hints carrying a live-state `chip` render as mode chips next to the
|
|
@@ -44,7 +46,7 @@ export function RegisteredToolHelper({
|
|
|
44
46
|
const isBypassHint = hint.key === 'Shift'
|
|
45
47
|
return {
|
|
46
48
|
keys: [hint.key],
|
|
47
|
-
label: shiftPressed && isBypassHint ? 'Guided constraints bypassed' : hint.label,
|
|
49
|
+
label: shiftPressed && isBypassHint ? i18n._({ id: 'editor.helpers.registeredToolHelper.guidedConstraintsBypassed', message: 'Guided constraints bypassed' }) : hint.label,
|
|
48
50
|
active: shiftPressed && isBypassHint,
|
|
49
51
|
}
|
|
50
52
|
})}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type { LevelNode } from '@pascal-app/core'
|
|
3
|
+
import type { LevelNode, MessageDescriptor } from '@pascal-app/core'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
5
|
import type { LevelDuplicatePreset } from '@pascal-app/core/building'
|
|
6
|
-
import {
|
|
6
|
+
import { useLocale, usePascalI18n } from '@pascal-app/core'
|
|
7
|
+
import { localizedLevelName } from '../../i18n/level-name'
|
|
7
8
|
import { cn } from '../../lib/utils'
|
|
8
9
|
import {
|
|
9
10
|
Dialog,
|
|
@@ -16,36 +17,31 @@ import {
|
|
|
16
17
|
|
|
17
18
|
const DUPLICATE_PRESETS: Array<{
|
|
18
19
|
id: LevelDuplicatePreset
|
|
19
|
-
label:
|
|
20
|
-
description:
|
|
20
|
+
label: MessageDescriptor
|
|
21
|
+
description: MessageDescriptor
|
|
21
22
|
}> = [
|
|
22
23
|
{
|
|
23
24
|
id: 'everything',
|
|
24
|
-
label: 'Everything',
|
|
25
|
-
description: 'Structure, materials, furniture, and references.',
|
|
25
|
+
label: /* i18n */ { id: 'editor.levelDuplicateDialog.everything', message: 'Everything' },
|
|
26
|
+
description: /* i18n */ { id: 'editor.levelDuplicateDialog.structureMaterialsFurnitureAndReferences', message: 'Structure, materials, furniture, and references.' },
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
29
|
id: 'structure',
|
|
29
|
-
label: 'Structure only',
|
|
30
|
-
description: 'Walls, slabs, roofs, stairs, windows, and doors without finishes.',
|
|
30
|
+
label: /* i18n */ { id: 'editor.levelDuplicateDialog.structureOnly', message: 'Structure only' },
|
|
31
|
+
description: /* i18n */ { id: 'editor.levelDuplicateDialog.wallsSlabsRoofsStairsWindowsAnd', message: 'Walls, slabs, roofs, stairs, windows, and doors without finishes.' },
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
id: 'structure-materials',
|
|
34
|
-
label: 'Structure + materials',
|
|
35
|
-
description: 'Structure with the current material and finish assignments.',
|
|
35
|
+
label: /* i18n */ { id: 'editor.levelDuplicateDialog.structureMaterials', message: 'Structure + materials' },
|
|
36
|
+
description: /* i18n */ { id: 'editor.levelDuplicateDialog.structureWithTheCurrentMaterialAnd', message: 'Structure with the current material and finish assignments.' },
|
|
36
37
|
},
|
|
37
38
|
{
|
|
38
39
|
id: 'structure-furniture',
|
|
39
|
-
label: 'Structure + furniture',
|
|
40
|
-
description: 'Structure, finishes, and placed items, without guide references.',
|
|
40
|
+
label: /* i18n */ { id: 'editor.levelDuplicateDialog.structureFurniture', message: 'Structure + furniture' },
|
|
41
|
+
description: /* i18n */ { id: 'editor.levelDuplicateDialog.structureFinishesAndPlacedItemsWithout', message: 'Structure, finishes, and placed items, without guide references.' },
|
|
41
42
|
},
|
|
42
43
|
]
|
|
43
44
|
|
|
44
|
-
function getLevelLabel(level: LevelNode | null) {
|
|
45
|
-
if (!level) return 'this level'
|
|
46
|
-
return getLevelDisplayName(level)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
45
|
export function LevelDuplicateDialog({
|
|
50
46
|
open,
|
|
51
47
|
level,
|
|
@@ -57,6 +53,8 @@ export function LevelDuplicateDialog({
|
|
|
57
53
|
onConfirm: (preset: LevelDuplicatePreset) => void
|
|
58
54
|
onOpenChange: (open: boolean) => void
|
|
59
55
|
}) {
|
|
56
|
+
const i18n = usePascalI18n()
|
|
57
|
+
const locale = useLocale()
|
|
60
58
|
const [preset, setPreset] = useState<LevelDuplicatePreset>('everything')
|
|
61
59
|
|
|
62
60
|
useEffect(() => {
|
|
@@ -69,8 +67,19 @@ export function LevelDuplicateDialog({
|
|
|
69
67
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
|
70
68
|
<DialogContent className="sm:max-w-md" showCloseButton={false}>
|
|
71
69
|
<DialogHeader>
|
|
72
|
-
<DialogTitle>Duplicate Level</DialogTitle>
|
|
73
|
-
<DialogDescription>
|
|
70
|
+
<DialogTitle>{i18n._({ id: 'editor.levelDuplicateDialog.duplicateLevel', message: 'Duplicate Level' })}</DialogTitle>
|
|
71
|
+
<DialogDescription>
|
|
72
|
+
{level
|
|
73
|
+
? i18n._({
|
|
74
|
+
id: 'editor.levelDuplicateDialog.chooseWhatToCopyFromLevel',
|
|
75
|
+
message: 'Choose what to copy from {levelName}.',
|
|
76
|
+
values: { levelName: localizedLevelName(level, i18n, locale) },
|
|
77
|
+
})
|
|
78
|
+
: i18n._({
|
|
79
|
+
id: 'editor.levelDuplicateDialog.chooseWhatToCopyFromThisLevel',
|
|
80
|
+
message: 'Choose what to copy from this level.',
|
|
81
|
+
})}
|
|
82
|
+
</DialogDescription>
|
|
74
83
|
</DialogHeader>
|
|
75
84
|
|
|
76
85
|
<div className="grid gap-2">
|
|
@@ -86,8 +95,8 @@ export function LevelDuplicateDialog({
|
|
|
86
95
|
onClick={() => setPreset(option.id)}
|
|
87
96
|
type="button"
|
|
88
97
|
>
|
|
89
|
-
<div className="font-medium text-sm">{option.label}</div>
|
|
90
|
-
<div className="mt-1 text-muted-foreground text-xs">{option.description}</div>
|
|
98
|
+
<div className="font-medium text-sm">{i18n._(option.label)}</div>
|
|
99
|
+
<div className="mt-1 text-muted-foreground text-xs">{i18n._(option.description)}</div>
|
|
91
100
|
</button>
|
|
92
101
|
))}
|
|
93
102
|
</div>
|
|
@@ -98,14 +107,14 @@ export function LevelDuplicateDialog({
|
|
|
98
107
|
onClick={() => onOpenChange(false)}
|
|
99
108
|
type="button"
|
|
100
109
|
>
|
|
101
|
-
Cancel
|
|
110
|
+
{i18n._({ id: 'editor.levelDuplicateDialog.cancel', message: 'Cancel' })}
|
|
102
111
|
</button>
|
|
103
112
|
<button
|
|
104
113
|
className="cursor-pointer rounded-md bg-primary px-4 py-2 text-primary-foreground text-sm transition-opacity hover:opacity-90"
|
|
105
114
|
onClick={() => onConfirm(preset)}
|
|
106
115
|
type="button"
|
|
107
116
|
>
|
|
108
|
-
Duplicate
|
|
117
|
+
{i18n._({ id: 'editor.levelDuplicateDialog.duplicate', message: 'Duplicate' })}
|
|
109
118
|
</button>
|
|
110
119
|
</DialogFooter>
|
|
111
120
|
</DialogContent>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
getLevelAbove,
|
|
7
|
+
getLevelBelow,
|
|
8
|
+
getLevelDisplayName,
|
|
9
|
+
useScene, usePascalI18n,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { buildExteriorWallCopy, levelExteriorWalls } from '@pascal-app/core/building'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { create } from 'zustand'
|
|
14
|
+
|
|
15
|
+
type Offer = { levelId: string; sourceLevelId: string; direction: 'below' | 'above' }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The one open "Copy exterior walls?" offer, for the level just added. While it is open the plan
|
|
19
|
+
* traces the source level's walls on the new one; that trace is the offer's, never the saved
|
|
20
|
+
* Walls below / above preference, and it goes with the offer.
|
|
21
|
+
*/
|
|
22
|
+
export const useLevelWallCopyOffer = create<{
|
|
23
|
+
offer: Offer | null
|
|
24
|
+
setOffer: (offer: Offer | null) => void
|
|
25
|
+
}>((set) => ({ offer: null, setOffer: (offer) => set({ offer }) }))
|
|
26
|
+
|
|
27
|
+
let stopWatchingLevel: (() => void) | null = null
|
|
28
|
+
|
|
29
|
+
/** Opens `offer` (or closes the open one); picking another level closes it. */
|
|
30
|
+
function setOffer(offer: Offer | null) {
|
|
31
|
+
stopWatchingLevel?.()
|
|
32
|
+
stopWatchingLevel = null
|
|
33
|
+
useLevelWallCopyOffer.getState().setOffer(offer)
|
|
34
|
+
if (!offer) return
|
|
35
|
+
stopWatchingLevel = useViewer.subscribe((state) => {
|
|
36
|
+
if (state.selection.levelId !== offer.levelId) setOffer(null)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* After a level is added next to `sourceLevelId` (by default the level under it, else the one over
|
|
42
|
+
* it, for a basement), and selected: when that level has an outline, offer to copy its exterior
|
|
43
|
+
* walls up or down, tracing them dashed on the plan while the offer is open.
|
|
44
|
+
*/
|
|
45
|
+
export function offerExteriorWallCopy(levelId: string, sourceLevelId?: string) {
|
|
46
|
+
const nodes = useScene.getState().nodes as Record<string, AnyNode>
|
|
47
|
+
const graph = nodes as Record<AnyNodeId, AnyNode>
|
|
48
|
+
sourceLevelId ??= (getLevelBelow(levelId, graph) ?? getLevelAbove(levelId, graph))?.id
|
|
49
|
+
const source = sourceLevelId ? nodes[sourceLevelId] : undefined
|
|
50
|
+
const level = nodes[levelId]
|
|
51
|
+
if (!sourceLevelId || source?.type !== 'level' || level?.type !== 'level') return setOffer(null)
|
|
52
|
+
const direction = source.level < level.level ? 'below' : 'above'
|
|
53
|
+
setOffer(
|
|
54
|
+
levelExteriorWalls(nodes, sourceLevelId).length ? { levelId, sourceLevelId, direction } : null,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Closes the open offer without copying. */
|
|
59
|
+
export function dismissExteriorWallCopy() {
|
|
60
|
+
setOffer(null)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Copies the offered walls in place, as one undo step of its own. */
|
|
64
|
+
export function acceptExteriorWallCopy(offer: Offer): number {
|
|
65
|
+
const walls = buildExteriorWallCopy({
|
|
66
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
67
|
+
fromLevelId: offer.sourceLevelId,
|
|
68
|
+
toLevelId: offer.levelId,
|
|
69
|
+
})
|
|
70
|
+
if (walls.length) useScene.getState().applyNodeChanges({ create: walls })
|
|
71
|
+
setOffer(null)
|
|
72
|
+
return walls.length
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* "Copy exterior walls from the level below?" — a small card under the level list, never in the
|
|
77
|
+
* way: it goes when answered or when another level is picked, and hides once the level has walls.
|
|
78
|
+
*/
|
|
79
|
+
export function LevelWallCopyPrompt() {
|
|
80
|
+
const i18n = usePascalI18n()
|
|
81
|
+
const offer = useLevelWallCopyOffer((state) => state.offer)
|
|
82
|
+
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
83
|
+
const sourceName = useScene((state) => {
|
|
84
|
+
const source = offer ? state.nodes[offer.sourceLevelId as never] : undefined
|
|
85
|
+
return source?.type === 'level' ? getLevelDisplayName(source) : null
|
|
86
|
+
})
|
|
87
|
+
const levelHasWalls = useScene((state) => {
|
|
88
|
+
const level = offer ? state.nodes[offer.levelId as AnyNodeId] : undefined
|
|
89
|
+
return (
|
|
90
|
+
level?.type === 'level' &&
|
|
91
|
+
level.children.some((id) => state.nodes[id as AnyNodeId]?.type === 'wall')
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
if (!offer || !sourceName || activeLevelId !== offer.levelId || levelHasWalls) return null
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className="mt-2 w-56 rounded-xl border border-border bg-background/95 p-3 text-xs shadow-2xl backdrop-blur-md"
|
|
98
|
+
data-level-wall-copy={offer.levelId}
|
|
99
|
+
role="status"
|
|
100
|
+
>
|
|
101
|
+
<p className="font-medium text-foreground text-sm">
|
|
102
|
+
{i18n._({ id: 'editor.levelWallCopyPrompt.copyExteriorWallsFromTheLevel', message: 'Copy exterior walls from the level {direction}?', values: { direction: offer.direction } })}</p>
|
|
103
|
+
<p className="mt-0.5 text-muted-foreground">
|
|
104
|
+
{i18n._({ id: 'editor.levelWallCopyPrompt.sourcenameSOutsideWallsInPlace', message: '{sourceName}’s outside walls, in place, so this level lines up with it.', values: { sourceName } })}</p>
|
|
105
|
+
<div className="mt-2.5 flex gap-2">
|
|
106
|
+
<button
|
|
107
|
+
className="flex-1 rounded-full bg-foreground px-3 py-1.5 font-medium text-background transition-opacity hover:opacity-90"
|
|
108
|
+
data-level-wall-copy-accept
|
|
109
|
+
onClick={() => acceptExteriorWallCopy(offer)}
|
|
110
|
+
type="button"
|
|
111
|
+
>
|
|
112
|
+
{i18n._({ id: 'editor.levelWallCopyPrompt.copy', message: 'Copy' })}
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
className="flex-1 rounded-full border border-border px-3 py-1.5 text-foreground transition-colors hover:bg-accent"
|
|
116
|
+
onClick={dismissExteriorWallCopy}
|
|
117
|
+
type="button"
|
|
118
|
+
>
|
|
119
|
+
{i18n._({ id: 'editor.levelWallCopyPrompt.noThanks', message: 'No thanks' })}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|