@pascal-app/editor 1.0.6 → 1.1.0
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 +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- 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 +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.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 +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- 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 +7 -3
- 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 +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- 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 +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- 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-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- 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/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- 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 +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- 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 +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- 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/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -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 +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- 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 +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -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 +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- 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 +34 -28
- 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 +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- 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 +465 -66
- 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 +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- 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/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- 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/settings-panel/load-build-dialog.tsx +1 -5
- 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 +5 -3
- 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 +118 -72
- 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 +9 -4
- 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 +19 -9
- 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 +62 -5
- 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 +15849 -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 +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -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 +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- 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/open-wall-ends.ts +2 -1
- 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/gesture-lifecycle.ts +3 -1
- 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/interaction/scope.ts +1 -0
- 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/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- 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-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- 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/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -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 +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- 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 +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- 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/snapping-mode.test.ts +23 -1
- 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 +21 -6
- 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 +17 -6
- 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 +44 -0
- 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 +53 -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-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
getWallCurveFrameAt,
|
|
8
8
|
getWallCurveLength,
|
|
9
|
+
getWallFaceOffsets,
|
|
9
10
|
isCurvedWall,
|
|
10
11
|
type WallNode,
|
|
11
12
|
} from '@pascal-app/core'
|
|
@@ -124,6 +125,142 @@ export function findWallSnapTarget(
|
|
|
124
125
|
return bestTarget
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// A drafted segment closer than this to a wall's direction (sine of the angle
|
|
129
|
+
// between them) runs alongside it: the wall topology stacks such walls rather
|
|
130
|
+
// than T-joining them, so the body snap leaves them alone. Two walls meeting
|
|
131
|
+
// closer than this aren't a corner either.
|
|
132
|
+
const WALL_BODY_SNAP_MIN_SINE = 0.5
|
|
133
|
+
|
|
134
|
+
/** A straight wall's reference line as a frame, with the faces its body spans. */
|
|
135
|
+
type WallBodyFrame = {
|
|
136
|
+
wall: WallNode
|
|
137
|
+
length: number
|
|
138
|
+
ux: number
|
|
139
|
+
uz: number
|
|
140
|
+
faces: { a: number; b: number }
|
|
141
|
+
/** Offset along the wall's left normal, the side its face offsets are measured on. */
|
|
142
|
+
across: (q: WallPlanPoint) => number
|
|
143
|
+
/** Station along the reference line from `start`. */
|
|
144
|
+
along: (q: WallPlanPoint) => number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function wallBodyFrame(wall: WallNode): WallBodyFrame | null {
|
|
148
|
+
if (isCurvedWall(wall)) return null
|
|
149
|
+
const [x1, z1] = wall.start
|
|
150
|
+
const length = Math.hypot(wall.end[0] - x1, wall.end[1] - z1)
|
|
151
|
+
if (length < 1e-9) return null
|
|
152
|
+
const ux = (wall.end[0] - x1) / length
|
|
153
|
+
const uz = (wall.end[1] - z1) / length
|
|
154
|
+
return {
|
|
155
|
+
wall,
|
|
156
|
+
length,
|
|
157
|
+
ux,
|
|
158
|
+
uz,
|
|
159
|
+
faces: getWallFaceOffsets(wall),
|
|
160
|
+
across: (q) => (q[1] - z1) * ux - (q[0] - x1) * uz,
|
|
161
|
+
along: (q) => (q[0] - x1) * ux + (q[1] - z1) * uz,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The corner a join at `target` on `host`'s reference line falls into: where
|
|
167
|
+
* `host` meets another straight wall, at an L or a T, with `target` inside that
|
|
168
|
+
* wall's drawn body. A T-join there would split `host` into a stub buried in
|
|
169
|
+
* the corner block, so the end joins the junction instead. A shared end comes
|
|
170
|
+
* back exactly; walls meeting at a shallow angle aren't a corner.
|
|
171
|
+
*/
|
|
172
|
+
function cornerJunctionAt(
|
|
173
|
+
target: WallPlanPoint,
|
|
174
|
+
host: WallBodyFrame,
|
|
175
|
+
frames: WallBodyFrame[],
|
|
176
|
+
): WallPlanPoint | null {
|
|
177
|
+
for (const other of frames) {
|
|
178
|
+
if (other === host) continue
|
|
179
|
+
if (Math.abs(host.ux * other.uz - host.uz * other.ux) < WALL_BODY_SNAP_MIN_SINE) continue
|
|
180
|
+
const offset = other.across(target)
|
|
181
|
+
if (offset < other.faces.b || offset > other.faces.a) continue
|
|
182
|
+
const ends = [host.wall.start, host.wall.end, other.wall.start, other.wall.end]
|
|
183
|
+
const crossing = segmentIntersection(ends[0]!, ends[1]!, ends[2]!, ends[3]!)
|
|
184
|
+
if (!crossing) continue
|
|
185
|
+
const sharedEnd = ends.find((end) => distanceSquared(end, crossing) <= 1e-12)
|
|
186
|
+
return sharedEnd ? [sharedEnd[0], sharedEnd[1]] : crossing
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Where a drawn wall end joins a wall it is aimed into: the point on a straight
|
|
193
|
+
* wall's reference line (`start` → `end`, the line the wall topology T-joins a
|
|
194
|
+
* partition end on, whatever the wall's justification) for an end inside that
|
|
195
|
+
* wall's drawn body or within `margin` of either face.
|
|
196
|
+
*
|
|
197
|
+
* `point` is the raw cursor and `placed` its grid / angle placement; either one
|
|
198
|
+
* inside a body captures, so the grid can't leave an end a fraction short of a
|
|
199
|
+
* face. Given the drafted segment's `start`, the end slides along start →
|
|
200
|
+
* `placed` onto the line, so a square or angle-locked segment keeps its
|
|
201
|
+
* direction, and a wall the segment runs alongside (or starts on) is skipped.
|
|
202
|
+
* Without a start the end drops square onto the line, keeping its position
|
|
203
|
+
* along the wall. A join that lands inside a corner block, where the wall
|
|
204
|
+
* meets another at an L or a T, takes the junction itself (`cornerJunctionAt`).
|
|
205
|
+
* Curved walls are left to the edge snap (`findWallSnapTarget`).
|
|
206
|
+
*/
|
|
207
|
+
export function findWallBodySnapTarget(
|
|
208
|
+
point: WallPlanPoint,
|
|
209
|
+
walls: WallNode[],
|
|
210
|
+
options?: {
|
|
211
|
+
ignoreWallIds?: string[]
|
|
212
|
+
placed?: WallPlanPoint
|
|
213
|
+
start?: WallPlanPoint
|
|
214
|
+
margin?: number
|
|
215
|
+
},
|
|
216
|
+
): WallPlanPoint | null {
|
|
217
|
+
const ignored = new Set(options?.ignoreWallIds ?? [])
|
|
218
|
+
const placed = options?.placed ?? point
|
|
219
|
+
const start = options?.start
|
|
220
|
+
const margin = options?.margin ?? WALL_CONNECT_SNAP_RADIUS
|
|
221
|
+
const frames = walls.flatMap((wall) => {
|
|
222
|
+
const frame = ignored.has(wall.id) ? null : wallBodyFrame(wall)
|
|
223
|
+
return frame ? [frame] : []
|
|
224
|
+
})
|
|
225
|
+
let best: WallPlanPoint | null = null
|
|
226
|
+
let bestOutside = Number.POSITIVE_INFINITY
|
|
227
|
+
|
|
228
|
+
for (const frame of frames) {
|
|
229
|
+
const { length, ux, uz, faces, across, along } = frame
|
|
230
|
+
const [x1, z1] = frame.wall.start
|
|
231
|
+
const onWall = (q: WallPlanPoint) => along(q) > 0 && along(q) < length
|
|
232
|
+
const outsideBody = (q: WallPlanPoint) =>
|
|
233
|
+
onWall(q) ? Math.max(0, faces.b - across(q), across(q) - faces.a) : Number.POSITIVE_INFINITY
|
|
234
|
+
const outside = Math.min(outsideBody(point), outsideBody(placed))
|
|
235
|
+
if (outside > margin || outside >= bestOutside) continue
|
|
236
|
+
|
|
237
|
+
let target: WallPlanPoint | null
|
|
238
|
+
if (Math.abs(across(point)) <= 1e-9 && onWall(point)) {
|
|
239
|
+
// Already on the line: a joined end snapped again (the plan view hands
|
|
240
|
+
// the 3D tool its resolved point) stays put.
|
|
241
|
+
target = [point[0], point[1]]
|
|
242
|
+
} else if (start) {
|
|
243
|
+
const dx = placed[0] - start[0]
|
|
244
|
+
const dz = placed[1] - start[1]
|
|
245
|
+
const run = Math.hypot(dx, dz)
|
|
246
|
+
if (run < 1e-9 || Math.abs(dx * uz - dz * ux) / run < WALL_BODY_SNAP_MIN_SINE) continue
|
|
247
|
+
const startAcross = across(start)
|
|
248
|
+
const t = startAcross / (startAcross - across(placed))
|
|
249
|
+
if (!(t > 1e-6)) continue
|
|
250
|
+
target = [start[0] + dx * t, start[1] + dz * t]
|
|
251
|
+
if (!onWall(target)) continue
|
|
252
|
+
} else {
|
|
253
|
+
const station = along(placed)
|
|
254
|
+
if (!(station > 0 && station < length)) continue
|
|
255
|
+
target = [x1 + ux * station, z1 + uz * station]
|
|
256
|
+
}
|
|
257
|
+
best = cornerJunctionAt(target, frame, frames) ?? target
|
|
258
|
+
bestOutside = outside
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return best
|
|
262
|
+
}
|
|
263
|
+
|
|
127
264
|
/**
|
|
128
265
|
* Wall ids that contain an already-resolved snap point.
|
|
129
266
|
*
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
formatWallDraftHeading,
|
|
4
|
+
resolveTypedWallEnd,
|
|
5
|
+
wallDraftHeadingDegrees,
|
|
6
|
+
} from './wall-typed-entry'
|
|
7
|
+
|
|
8
|
+
const metric = { bareUnit: 'm', system: 'metric' } as const
|
|
9
|
+
|
|
10
|
+
function expectPoint(actual: [number, number] | null, expected: [number, number]) {
|
|
11
|
+
expect(actual).not.toBeNull()
|
|
12
|
+
expect(actual![0]).toBeCloseTo(expected[0], 10)
|
|
13
|
+
expect(actual![1]).toBeCloseTo(expected[1], 10)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('resolveTypedWallEnd — length only', () => {
|
|
17
|
+
test('keeps the pointer direction and replaces the distance', () => {
|
|
18
|
+
expectPoint(resolveTypedWallEnd([0, 0], [3, 4], ['1', ''], metric), [0.6, 0.8])
|
|
19
|
+
expectPoint(resolveTypedWallEnd([0, 0], [-30, -40], ['5', ''], metric), [-3, -4])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('reads the whole field, not the length the pointer last showed', () => {
|
|
23
|
+
expectPoint(resolveTypedWallEnd([0, 0], [1, 0], ['12', ''], metric), [12, 0])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('honours unit suffixes and the bare unit', () => {
|
|
27
|
+
expectPoint(resolveTypedWallEnd([1, 1], [11, 1], ['250cm', ''], metric), [3.5, 1])
|
|
28
|
+
expectPoint(
|
|
29
|
+
resolveTypedWallEnd([0, 0], [0, 9], ['2500', ''], { bareUnit: 'mm', system: 'metric' }),
|
|
30
|
+
[0, 2.5],
|
|
31
|
+
)
|
|
32
|
+
expectPoint(
|
|
33
|
+
resolveTypedWallEnd([0, 0], [9, 0], ["10'", ''], { bareUnit: 'in', system: 'imperial' }),
|
|
34
|
+
[3.048, 0],
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('has no endpoint without a usable length or a direction', () => {
|
|
39
|
+
expect(resolveTypedWallEnd([0, 0], [1, 0], ['', ''], metric)).toBeNull()
|
|
40
|
+
expect(resolveTypedWallEnd([0, 0], [1, 0], ['abc', ''], metric)).toBeNull()
|
|
41
|
+
expect(resolveTypedWallEnd([0, 0], [1, 0], ['0', ''], metric)).toBeNull()
|
|
42
|
+
expect(resolveTypedWallEnd([2, 2], [2, 2], ['5', ''], metric)).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('resolveTypedWallEnd — angle', () => {
|
|
47
|
+
test('angle only locks the heading; the pointer projection sets the length', () => {
|
|
48
|
+
// 90° runs along +Z; a pointer at (1, 3) projects to 3 m along it.
|
|
49
|
+
expectPoint(resolveTypedWallEnd([0, 0], [1, 3], ['', '90'], metric), [0, 3])
|
|
50
|
+
// (3, 4) projects to 7/√2 along 45°, landing 3.5 out on each axis.
|
|
51
|
+
expectPoint(resolveTypedWallEnd([1, 1], [4, 5], ['', '45'], metric), [4.5, 4.5])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('angle only quantises the projected length to the step', () => {
|
|
55
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0.2, 2.37], ['', '90'], metric, 0.5), [0, 2.5])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('angle only never flips the wall behind its start', () => {
|
|
59
|
+
expectPoint(resolveTypedWallEnd([0, 0], [-4, 0.1], ['', '0'], metric), [0, 0])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('length and angle fix the endpoint whatever the pointer does', () => {
|
|
63
|
+
expectPoint(resolveTypedWallEnd([2, 1], [-50, 80], ['4', '180'], metric), [-2, 1])
|
|
64
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['2', '270'], metric), [0, -2])
|
|
65
|
+
expectPoint(resolveTypedWallEnd([0, 0], [5, 5], ['1', '-90'], metric), [0, -1])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('accepts degree and radian suffixes', () => {
|
|
69
|
+
expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', '90°'], metric), [0, 2])
|
|
70
|
+
expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', `${Math.PI}rad`], metric), [-2, 0])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('an unreadable angle leaves the direction to the pointer', () => {
|
|
74
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0, 4], ['2', 'x'], metric), [0, 2])
|
|
75
|
+
expect(resolveTypedWallEnd([0, 0], [0, 4], ['', 'x'], metric)).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('wallDraftHeadingDegrees', () => {
|
|
80
|
+
test('measures from +X towards +Z in [0, 360)', () => {
|
|
81
|
+
expect(wallDraftHeadingDegrees([0, 0], [1, 0])).toBeCloseTo(0, 10)
|
|
82
|
+
expect(wallDraftHeadingDegrees([0, 0], [0, 1])).toBeCloseTo(90, 10)
|
|
83
|
+
expect(wallDraftHeadingDegrees([0, 0], [-1, 0])).toBeCloseTo(180, 10)
|
|
84
|
+
expect(wallDraftHeadingDegrees([0, 0], [0, -1])).toBeCloseTo(270, 10)
|
|
85
|
+
expect(wallDraftHeadingDegrees([1, 1], [1, 1])).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('round-trips through a typed angle', () => {
|
|
89
|
+
const heading = wallDraftHeadingDegrees([0, 0], [-3, -4])!
|
|
90
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['5', String(heading)], metric), [-3, -4])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('labels the live heading in the same convention, wrapping 360° to 0°', () => {
|
|
94
|
+
expect(formatWallDraftHeading([0, 0], [0, 1])).toBe('90°')
|
|
95
|
+
expect(formatWallDraftHeading([0, 0], [1, -1])).toBe('315°')
|
|
96
|
+
expect(formatWallDraftHeading([0, 0], [1, -0.001])).toBe('0°')
|
|
97
|
+
expect(formatWallDraftHeading([0, 0], [0, 0])).toBe('')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { formatAngleRadians } from '@pascal-app/core'
|
|
2
|
+
import { type ParseMeasurementOptions, parseMeasurement } from '../../../lib/measurement-parser'
|
|
3
|
+
import type { PlacementTypingValues } from '../../../store/use-placement-typing'
|
|
4
|
+
import type { WallPlanPoint } from './wall-drafting'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Heading of a wall draft in degrees, the way the 15° angle snap measures it:
|
|
8
|
+
* from the building's +X axis towards +Z (`atan2(dz, dx)`), in [0, 360).
|
|
9
|
+
* On an unrotated plan +Z points down the screen, so 90 runs straight down.
|
|
10
|
+
*/
|
|
11
|
+
export function wallDraftHeadingDegrees(start: WallPlanPoint, end: WallPlanPoint): number | null {
|
|
12
|
+
const dx = end[0] - start[0]
|
|
13
|
+
const dz = end[1] - start[1]
|
|
14
|
+
if (Math.hypot(dx, dz) <= 1e-6) return null
|
|
15
|
+
const degrees = (Math.atan2(dz, dx) * 180) / Math.PI
|
|
16
|
+
return ((degrees % 360) + 360) % 360
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The draft's live heading as the angle field's placeholder, e.g. `90°`. */
|
|
20
|
+
export function formatWallDraftHeading(start: WallPlanPoint, end: WallPlanPoint): string {
|
|
21
|
+
const degrees = wallDraftHeadingDegrees(start, end)
|
|
22
|
+
if (degrees === null) return ''
|
|
23
|
+
return formatAngleRadians(((Math.round(degrees) % 360) * Math.PI) / 180)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The endpoint a typed wall draft commits to. Field 0 is the length, field 1
|
|
28
|
+
* the heading (see `wallDraftHeadingDegrees`); an empty field is left to the
|
|
29
|
+
* pointer:
|
|
30
|
+
* - length only: the pointer steers the direction, the length is exact;
|
|
31
|
+
* - angle only: the direction is locked, the pointer's projection onto it sets
|
|
32
|
+
* the length (quantised to `lengthStep` when one is given);
|
|
33
|
+
* - both: the endpoint is fully determined and the pointer is ignored.
|
|
34
|
+
*
|
|
35
|
+
* Returns null when no field holds a usable value or the pointer gives no
|
|
36
|
+
* direction to follow, so the caller keeps the pointer's own endpoint.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveTypedWallEnd(
|
|
39
|
+
start: WallPlanPoint,
|
|
40
|
+
pointer: WallPlanPoint,
|
|
41
|
+
fields: Readonly<PlacementTypingValues>,
|
|
42
|
+
lengthOptions: ParseMeasurementOptions,
|
|
43
|
+
lengthStep = 0,
|
|
44
|
+
): WallPlanPoint | null {
|
|
45
|
+
const typedLength = fields[0].trim()
|
|
46
|
+
? parseMeasurement(fields[0], { kind: 'length', unitId: 'm' }, lengthOptions)
|
|
47
|
+
: null
|
|
48
|
+
const length = typedLength !== null && typedLength > 0 ? typedLength : null
|
|
49
|
+
const typedAngle = fields[1].trim()
|
|
50
|
+
? parseMeasurement(fields[1], { kind: 'angle', unitId: 'rad' }, { bareUnit: 'deg' })
|
|
51
|
+
: null
|
|
52
|
+
|
|
53
|
+
const dx = pointer[0] - start[0]
|
|
54
|
+
const dz = pointer[1] - start[1]
|
|
55
|
+
if (typedAngle === null) {
|
|
56
|
+
if (length === null) return null
|
|
57
|
+
const pointerLength = Math.hypot(dx, dz)
|
|
58
|
+
if (pointerLength <= 1e-6) return null
|
|
59
|
+
return [start[0] + (dx / pointerLength) * length, start[1] + (dz / pointerLength) * length]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dirX = Math.cos(typedAngle)
|
|
63
|
+
const dirZ = Math.sin(typedAngle)
|
|
64
|
+
let distance = length
|
|
65
|
+
if (distance === null) {
|
|
66
|
+
const projected = Math.max(0, dx * dirX + dz * dirZ)
|
|
67
|
+
distance = lengthStep > 0 ? Math.round(projected / lengthStep) * lengthStep : projected
|
|
68
|
+
}
|
|
69
|
+
return [start[0] + dirX * distance, start[1] + dirZ * distance]
|
|
70
|
+
}
|
|
@@ -184,9 +184,9 @@ export const ZoneTool: React.FC = () => {
|
|
|
184
184
|
updateLines()
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
187
|
+
// Snap on the building's own grid lattice, the one its walls are drafted
|
|
188
|
+
// on (square to a turned house). Grid quantize only in grid mode; off /
|
|
189
|
+
// lines / angles leave the raw cursor.
|
|
190
190
|
const gridPointOf = (event: GridEvent): [number, number] =>
|
|
191
191
|
isGridSnapActive()
|
|
192
192
|
? snapWorldXZForActiveBuilding(
|
|
@@ -44,6 +44,8 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
|
|
|
44
44
|
// These are toggles, not commands: the armed/unarmed state has to be
|
|
45
45
|
// readable without decoding the active-state styling.
|
|
46
46
|
aria-pressed={isActive}
|
|
47
|
+
// Icon-only: the tooltip text is the button's name.
|
|
48
|
+
aria-label={label}
|
|
47
49
|
className={cn('relative h-11 w-11 transition-all', className)}
|
|
48
50
|
onClick={(event) => {
|
|
49
51
|
triggerSFX('sfx:menu-click')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { emitter } from '@pascal-app/core'
|
|
3
|
+
import { emitter, usePascalI18n } from '@pascal-app/core'
|
|
4
4
|
import Image from 'next/image'
|
|
5
5
|
import useEditor from '../../../store/use-editor'
|
|
6
6
|
import { visibleScene } from '../../../store/view-layout'
|
|
@@ -8,6 +8,7 @@ import { ActionButton } from './action-button'
|
|
|
8
8
|
import { FollowPascalToggle } from './follow-pascal-toggle'
|
|
9
9
|
|
|
10
10
|
export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
11
|
+
const i18n = usePascalI18n()
|
|
11
12
|
// Orbit stays useful in 2D-only (it spins the synced floorplan view), but
|
|
12
13
|
// top view only tilts the hidden 3D camera — pointless without the canvas.
|
|
13
14
|
const is2dOnly = useEditor((s) => visibleScene(s) === '2d')
|
|
@@ -31,13 +32,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
31
32
|
{/* Orbit CCW */}
|
|
32
33
|
<ActionButton
|
|
33
34
|
className="group hover:bg-black/4"
|
|
34
|
-
label=
|
|
35
|
+
label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
|
|
35
36
|
onClick={orbitCCW}
|
|
36
37
|
size="icon"
|
|
37
38
|
variant="ghost"
|
|
38
39
|
>
|
|
39
40
|
<Image
|
|
40
|
-
alt=
|
|
41
|
+
alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
|
|
41
42
|
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
42
43
|
height={28}
|
|
43
44
|
src="/icons/rotate.webp"
|
|
@@ -48,13 +49,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
48
49
|
{/* Orbit CW */}
|
|
49
50
|
<ActionButton
|
|
50
51
|
className="group hover:bg-black/4"
|
|
51
|
-
label=
|
|
52
|
+
label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
|
|
52
53
|
onClick={orbitCW}
|
|
53
54
|
size="icon"
|
|
54
55
|
variant="ghost"
|
|
55
56
|
>
|
|
56
57
|
<Image
|
|
57
|
-
alt=
|
|
58
|
+
alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
|
|
58
59
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
59
60
|
height={28}
|
|
60
61
|
src="/icons/rotate.webp"
|
|
@@ -71,13 +72,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
71
72
|
{!is2dOnly && (
|
|
72
73
|
<ActionButton
|
|
73
74
|
className="group hover:bg-black/4"
|
|
74
|
-
label=
|
|
75
|
+
label={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
|
|
75
76
|
onClick={goToTopView}
|
|
76
77
|
size="icon"
|
|
77
78
|
variant="ghost"
|
|
78
79
|
>
|
|
79
80
|
<Image
|
|
80
|
-
alt=
|
|
81
|
+
alt={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
|
|
81
82
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
82
83
|
height={28}
|
|
83
84
|
src="/icons/topview.webp"
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
4
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
5
|
+
|
|
3
6
|
import { Icon } from '@iconify/react'
|
|
4
7
|
import { type LucideIcon, Trash2 } from 'lucide-react'
|
|
5
8
|
import Image from 'next/image'
|
|
@@ -17,7 +20,7 @@ type ControlConfig = {
|
|
|
17
20
|
icon?: LucideIcon
|
|
18
21
|
iconifyIcon?: string
|
|
19
22
|
imageSrc?: string
|
|
20
|
-
label:
|
|
23
|
+
label: MessageDescriptor
|
|
21
24
|
shortcut?: string
|
|
22
25
|
color: string
|
|
23
26
|
activeColor: string
|
|
@@ -28,7 +31,7 @@ const controls: ControlConfig[] = [
|
|
|
28
31
|
{
|
|
29
32
|
id: 'select',
|
|
30
33
|
imageSrc: '/icons/select.webp',
|
|
31
|
-
label: 'Select',
|
|
34
|
+
label: /* i18n */ { id: 'editor.actionMenu.controlModes.select', message: 'Select' },
|
|
32
35
|
shortcut: 'V',
|
|
33
36
|
color: 'hover:bg-blue-500/20 hover:text-blue-600 dark:hover:text-blue-400',
|
|
34
37
|
activeColor: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 dark:hover:text-accent-foreground',
|
|
@@ -36,7 +39,7 @@ const controls: ControlConfig[] = [
|
|
|
36
39
|
{
|
|
37
40
|
id: 'delete',
|
|
38
41
|
icon: Trash2,
|
|
39
|
-
label: 'Delete',
|
|
42
|
+
label: /* i18n */ { id: 'editor.actionMenu.controlModes.delete', message: 'Delete' },
|
|
40
43
|
shortcut: 'X',
|
|
41
44
|
color: 'hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400',
|
|
42
45
|
activeColor: 'bg-red-500/20 text-red-600 dark:text-red-400 dark:hover:text-accent-foreground',
|
|
@@ -44,6 +47,7 @@ const controls: ControlConfig[] = [
|
|
|
44
47
|
]
|
|
45
48
|
|
|
46
49
|
export function ControlModes() {
|
|
50
|
+
const i18n = usePascalI18n()
|
|
47
51
|
const mode = useEditor((state) => state.mode)
|
|
48
52
|
const selectionTool = useEditor((state) => state.floorplanSelectionTool)
|
|
49
53
|
const armToolMode = useEditor((state) => state.armToolMode)
|
|
@@ -94,7 +98,7 @@ export function ControlModes() {
|
|
|
94
98
|
// tour at this button. Nothing here reads it.
|
|
95
99
|
data-guide-target={c.id === 'select' ? 'mode-select' : undefined}
|
|
96
100
|
isActive={isActive}
|
|
97
|
-
label={c.label}
|
|
101
|
+
label={i18n._(c.label)}
|
|
98
102
|
onClick={() => handleClick(c.id)}
|
|
99
103
|
shortcut={c.shortcut}
|
|
100
104
|
size="icon"
|
|
@@ -102,7 +106,7 @@ export function ControlModes() {
|
|
|
102
106
|
>
|
|
103
107
|
{c.imageSrc ? (
|
|
104
108
|
<Image
|
|
105
|
-
alt={c.label}
|
|
109
|
+
alt={i18n._(c.label)}
|
|
106
110
|
className={cn(
|
|
107
111
|
'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
|
|
108
112
|
isActive
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import { AnimatePresence, motion } from 'motion/react'
|
|
4
5
|
import { useReducedMotion } from '../../../hooks/use-reduced-motion'
|
|
5
6
|
import useFollowPascal from '../../../store/use-follow-pascal'
|
|
@@ -9,6 +10,7 @@ import useFollowPascal from '../../../store/use-follow-pascal'
|
|
|
9
10
|
* following. It comes up from where the toolbar is (origin-aware) and goes the same way.
|
|
10
11
|
*/
|
|
11
12
|
export function FollowPascalPill() {
|
|
13
|
+
const i18n = usePascalI18n()
|
|
12
14
|
const shows = useFollowPascal((state) => state.on && state.paused && state.building)
|
|
13
15
|
const resume = useFollowPascal((state) => state.resume)
|
|
14
16
|
const reduced = useReducedMotion()
|
|
@@ -24,7 +26,7 @@ export function FollowPascalPill() {
|
|
|
24
26
|
transition={reduced ? { duration: 0.12 } : { type: 'spring', bounce: 0, duration: 0.3 }}
|
|
25
27
|
type="button"
|
|
26
28
|
>
|
|
27
|
-
Resume following
|
|
29
|
+
{i18n._({ id: 'editor.actionMenu.followPascalPill.resumeFollowing', message: 'Resume following' })}
|
|
28
30
|
</motion.button>
|
|
29
31
|
) : null}
|
|
30
32
|
</AnimatePresence>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import { Video } from 'lucide-react'
|
|
4
5
|
import { cn } from '../../../lib/utils'
|
|
5
6
|
import useFollowPascal from '../../../store/use-follow-pascal'
|
|
@@ -13,6 +14,7 @@ export function FollowPascalToggle({
|
|
|
13
14
|
className?: string
|
|
14
15
|
tooltipClassName?: string
|
|
15
16
|
}) {
|
|
17
|
+
const i18n = usePascalI18n()
|
|
16
18
|
const on = useFollowPascal((state) => state.on)
|
|
17
19
|
const setOn = useFollowPascal((state) => state.setOn)
|
|
18
20
|
return (
|
|
@@ -24,7 +26,11 @@ export function FollowPascalToggle({
|
|
|
24
26
|
: 'hover:bg-black/4 hover:text-violet-600 dark:hover:text-violet-300',
|
|
25
27
|
className,
|
|
26
28
|
)}
|
|
27
|
-
label={
|
|
29
|
+
label={
|
|
30
|
+
on
|
|
31
|
+
? i18n._({ id: 'editor.actionMenu.followPascalToggle.on', message: 'Follow Pascal: On' })
|
|
32
|
+
: i18n._({ id: 'editor.actionMenu.followPascalToggle.off', message: 'Follow Pascal: Off' })
|
|
33
|
+
}
|
|
28
34
|
onClick={() => setOn(!on)}
|
|
29
35
|
size="icon"
|
|
30
36
|
tooltipClassName={tooltipClassName}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
1
2
|
import type { CatalogCategory } from './../../../store/use-editor'
|
|
2
3
|
|
|
3
4
|
export type FurnishToolConfig = {
|
|
4
5
|
id: 'item'
|
|
5
6
|
iconSrc: string
|
|
6
|
-
label:
|
|
7
|
+
label: MessageDescriptor
|
|
7
8
|
catalogCategory: CatalogCategory
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const furnishTools: FurnishToolConfig[] = [
|
|
11
|
-
{ id: 'item', iconSrc: '/icons/couch.webp', label: 'Furniture', catalogCategory: 'furniture' },
|
|
12
|
-
{ id: 'item', iconSrc: '/icons/appliance.webp', label: 'Appliance', catalogCategory: 'appliance' },
|
|
13
|
-
{ id: 'item', iconSrc: '/icons/kitchen.webp', label: 'Kitchen', catalogCategory: 'kitchen' },
|
|
14
|
-
{ id: 'item', iconSrc: '/icons/bathroom.webp', label: 'Bathroom', catalogCategory: 'bathroom' },
|
|
15
|
-
{ id: 'item', iconSrc: '/icons/tree.webp', label: 'Outdoor', catalogCategory: 'outdoor' },
|
|
12
|
+
{ id: 'item', iconSrc: '/icons/couch.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.furniture', message: 'Furniture' }, catalogCategory: 'furniture' },
|
|
13
|
+
{ id: 'item', iconSrc: '/icons/appliance.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.appliance', message: 'Appliance' }, catalogCategory: 'appliance' },
|
|
14
|
+
{ id: 'item', iconSrc: '/icons/kitchen.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.kitchen', message: 'Kitchen' }, catalogCategory: 'kitchen' },
|
|
15
|
+
{ id: 'item', iconSrc: '/icons/bathroom.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.bathroom', message: 'Bathroom' }, catalogCategory: 'bathroom' },
|
|
16
|
+
{ id: 'item', iconSrc: '/icons/tree.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.outdoor', message: 'Outdoor' }, catalogCategory: 'outdoor' },
|
|
16
17
|
]
|