@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { type MessageDescriptor, type PascalI18n, usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import { type GroundMode, useViewer } from '@pascal-app/viewer'
|
|
4
5
|
import { ArrowLeftRight, Check, Columns2, MoreHorizontal, Pin, PinOff, X } from 'lucide-react'
|
|
5
6
|
import { type ReactNode, useEffect, useRef } from 'react'
|
|
@@ -7,7 +8,7 @@ import { useIsMobile } from '../../../hooks/use-mobile'
|
|
|
7
8
|
import { type EditorViewDescriptor, isSceneView, isViewPinned } from '../../../lib/editor-views'
|
|
8
9
|
import { cn } from '../../../lib/utils'
|
|
9
10
|
import useEditor from '../../../store/use-editor'
|
|
10
|
-
import type
|
|
11
|
+
import { VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../../store/view-layout'
|
|
11
12
|
import { IconRefGlyph } from '../../ui/icon-ref'
|
|
12
13
|
import {
|
|
13
14
|
DropdownMenu,
|
|
@@ -30,7 +31,16 @@ const IDLE =
|
|
|
30
31
|
'text-muted-foreground/70 hover:bg-black/5 dark:hover:bg-white/8 hover:text-muted-foreground'
|
|
31
32
|
|
|
32
33
|
/** The views of one pane: its pinned views, the one it shows, and a menu with the rest. */
|
|
34
|
+
/** Built-in views are named by the editor; host and plugin views bring their own label. */
|
|
35
|
+
function viewLabel(view: EditorViewDescriptor, i18n: PascalI18n): string {
|
|
36
|
+
if (view.id === VIEW_3D) return i18n._({ id: 'editor.views.view3d', message: '3D' })
|
|
37
|
+
if (view.id === VIEW_2D) return i18n._({ id: 'editor.views.view2d', message: '2D' })
|
|
38
|
+
if (view.id === PLUGINS_VIEW_ID) return i18n._({ id: 'editor.views.plugins', message: 'Plugins' })
|
|
39
|
+
return view.label
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?: string }) {
|
|
43
|
+
const i18n = usePascalI18n()
|
|
34
44
|
const layout = useActiveViewLayout()
|
|
35
45
|
const views = useEditorViews()
|
|
36
46
|
const pinnedViews = useEditor((s) => s.pinnedViews)
|
|
@@ -58,7 +68,11 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
|
|
|
58
68
|
return (
|
|
59
69
|
<div className={cn(GROUP, 'min-w-0 shrink', className)}>
|
|
60
70
|
<div
|
|
61
|
-
aria-label={
|
|
71
|
+
aria-label={i18n._({
|
|
72
|
+
id: 'editor.views.viewBar.paneViews',
|
|
73
|
+
message: 'Pane {pane} views',
|
|
74
|
+
values: { pane: pane + 1 },
|
|
75
|
+
})}
|
|
62
76
|
className="flex min-w-0 overflow-x-auto [scrollbar-width:none]"
|
|
63
77
|
role="tablist"
|
|
64
78
|
ref={tabList}
|
|
@@ -102,16 +116,20 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
|
|
|
102
116
|
}
|
|
103
117
|
}}
|
|
104
118
|
tabIndex={view.id === current ? 0 : -1}
|
|
105
|
-
title={view
|
|
119
|
+
title={viewLabel(view, i18n)}
|
|
106
120
|
role="tab"
|
|
107
121
|
type="button"
|
|
108
122
|
>
|
|
109
123
|
<IconRefGlyph icon={view.icon} size={14} />
|
|
110
|
-
<span className="max-w-36 truncate">{view
|
|
124
|
+
<span className="max-w-36 truncate">{viewLabel(view, i18n)}</span>
|
|
111
125
|
</button>
|
|
112
126
|
{view.onClose && (
|
|
113
127
|
<button
|
|
114
|
-
aria-label={
|
|
128
|
+
aria-label={i18n._({
|
|
129
|
+
id: 'editor.views.viewBar.closeLabel',
|
|
130
|
+
message: 'Close {label}',
|
|
131
|
+
values: { label: viewLabel(view, i18n) },
|
|
132
|
+
})}
|
|
115
133
|
className="mr-1 flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
116
134
|
onClick={view.onClose}
|
|
117
135
|
type="button"
|
|
@@ -128,9 +146,27 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
|
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
const GROUND_MODES = [
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
{
|
|
150
|
+
mode: 'auto',
|
|
151
|
+
label: /* i18n */ { id: 'editor.views.viewBar.auto', message: 'Auto' },
|
|
152
|
+
title: /* i18n */ {
|
|
153
|
+
id: 'editor.views.viewBar.veilTheGroundOverALevel',
|
|
154
|
+
message: 'Veil the ground over a level below grade',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
mode: 'solid',
|
|
159
|
+
label: /* i18n */ { id: 'editor.views.viewBar.solid', message: 'Solid' },
|
|
160
|
+
title: /* i18n */ {
|
|
161
|
+
id: 'editor.views.viewBar.keepTheGroundSolid',
|
|
162
|
+
message: 'Keep the ground solid',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
] as const satisfies ReadonlyArray<{
|
|
166
|
+
mode: GroundMode
|
|
167
|
+
label: MessageDescriptor
|
|
168
|
+
title: MessageDescriptor
|
|
169
|
+
}>
|
|
134
170
|
|
|
135
171
|
/**
|
|
136
172
|
* Over a level below grade the ground is veiled so its rooms show, and the bar says so: the
|
|
@@ -139,6 +175,7 @@ const GROUND_MODES = [
|
|
|
139
175
|
* a level at grade or only partly below it.
|
|
140
176
|
*/
|
|
141
177
|
export function GroundModeToggle() {
|
|
178
|
+
const i18n = usePascalI18n()
|
|
142
179
|
const belowGrade = useViewer((s) => s.belowGrade)
|
|
143
180
|
const gradeStanding = useViewer((s) => s.gradeStanding)
|
|
144
181
|
const groundWindowVeiled = useViewer((s) => s.groundWindowVeiled)
|
|
@@ -147,15 +184,31 @@ export function GroundModeToggle() {
|
|
|
147
184
|
const veiled =
|
|
148
185
|
groundMode === 'auto' &&
|
|
149
186
|
((belowGrade && gradeStanding !== 'partly-below-grade') || groundWindowVeiled)
|
|
150
|
-
const
|
|
151
|
-
const label =
|
|
187
|
+
const standing =
|
|
152
188
|
gradeStanding === 'partly-below-grade'
|
|
153
|
-
?
|
|
189
|
+
? 'partly'
|
|
154
190
|
: gradeStanding === 'below-grade'
|
|
155
|
-
?
|
|
156
|
-
:
|
|
191
|
+
? 'below'
|
|
192
|
+
: 'other'
|
|
193
|
+
const label = veiled
|
|
194
|
+
? i18n._({
|
|
195
|
+
id: 'editor.views.viewBar.groundVeiled',
|
|
196
|
+
message:
|
|
197
|
+
'{standing, select, partly {Partly below grade · ground veiled} below {Below grade · ground veiled} other {Ground veiled}}',
|
|
198
|
+
values: { standing },
|
|
199
|
+
})
|
|
200
|
+
: i18n._({
|
|
201
|
+
id: 'editor.views.viewBar.groundSolid',
|
|
202
|
+
message:
|
|
203
|
+
'{standing, select, partly {Partly below grade · ground solid} below {Below grade · ground solid} other {Ground solid}}',
|
|
204
|
+
values: { standing },
|
|
205
|
+
})
|
|
157
206
|
return (
|
|
158
|
-
<div
|
|
207
|
+
<div
|
|
208
|
+
aria-label={i18n._({ id: 'editor.views.viewBar.ground', message: 'Ground' })}
|
|
209
|
+
className={GROUP}
|
|
210
|
+
role="group"
|
|
211
|
+
>
|
|
159
212
|
<span className="flex items-center px-2.5 text-muted-foreground text-xs">{label}</span>
|
|
160
213
|
{GROUND_MODES.map(({ mode, label, title }) => (
|
|
161
214
|
<button
|
|
@@ -163,10 +216,10 @@ export function GroundModeToggle() {
|
|
|
163
216
|
className={cn(SEGMENT, groundMode === mode ? ACTIVE : IDLE)}
|
|
164
217
|
key={mode}
|
|
165
218
|
onClick={() => useViewer.getState().setGroundMode(mode)}
|
|
166
|
-
title={title}
|
|
219
|
+
title={i18n._(title)}
|
|
167
220
|
type="button"
|
|
168
221
|
>
|
|
169
|
-
{label}
|
|
222
|
+
{i18n._(label)}
|
|
170
223
|
</button>
|
|
171
224
|
))}
|
|
172
225
|
</div>
|
|
@@ -175,15 +228,16 @@ export function GroundModeToggle() {
|
|
|
175
228
|
|
|
176
229
|
/** Split toggle and, split, the swap — at the right end of the last pane's bar. */
|
|
177
230
|
function SplitControls() {
|
|
231
|
+
const i18n = usePascalI18n()
|
|
178
232
|
const split = useActiveViewLayout().split
|
|
179
233
|
return (
|
|
180
234
|
<div className={GROUP}>
|
|
181
235
|
{split && (
|
|
182
236
|
<button
|
|
183
|
-
aria-label=
|
|
237
|
+
aria-label={i18n._({ id: 'editor.views.viewBar.swapPanes', message: 'Swap panes' })}
|
|
184
238
|
className={cn(SEGMENT, IDLE, 'px-2')}
|
|
185
239
|
onClick={() => useEditor.getState().swapPanes()}
|
|
186
|
-
title=
|
|
240
|
+
title={i18n._({ id: 'editor.views.viewBar.swapPanes', message: 'Swap panes' })}
|
|
187
241
|
type="button"
|
|
188
242
|
>
|
|
189
243
|
<ArrowLeftRight className="h-3 w-3" />
|
|
@@ -193,11 +247,15 @@ function SplitControls() {
|
|
|
193
247
|
aria-pressed={split}
|
|
194
248
|
className={cn(SEGMENT, split ? ACTIVE : IDLE)}
|
|
195
249
|
onClick={() => useEditor.getState().toggleSplit()}
|
|
196
|
-
title={
|
|
250
|
+
title={
|
|
251
|
+
split
|
|
252
|
+
? i18n._({ id: 'editor.views.viewBar.closeSplit', message: 'Close split (\\)' })
|
|
253
|
+
: i18n._({ id: 'editor.views.viewBar.split', message: 'Split (\\)' })
|
|
254
|
+
}
|
|
197
255
|
type="button"
|
|
198
256
|
>
|
|
199
257
|
<Columns2 className="h-3 w-3" />
|
|
200
|
-
<span>Split</span>
|
|
258
|
+
<span>{i18n._({ id: 'editor.views.viewBar.split2', message: 'Split' })}</span>
|
|
201
259
|
</button>
|
|
202
260
|
</div>
|
|
203
261
|
)
|
|
@@ -272,6 +330,7 @@ function MoreViewsMenu({
|
|
|
272
330
|
current: string
|
|
273
331
|
views: EditorViewDescriptor[]
|
|
274
332
|
}) {
|
|
333
|
+
const i18n = usePascalI18n()
|
|
275
334
|
const pinnedViews = useEditor((s) => s.pinnedViews)
|
|
276
335
|
const setPaneView = useEditor((s) => s.setPaneView)
|
|
277
336
|
const setViewPinned = useEditor((s) => s.setViewPinned)
|
|
@@ -282,7 +341,7 @@ function MoreViewsMenu({
|
|
|
282
341
|
<DropdownMenu>
|
|
283
342
|
<DropdownMenuTrigger asChild>
|
|
284
343
|
<button
|
|
285
|
-
aria-label=
|
|
344
|
+
aria-label={i18n._({ id: 'editor.views.viewBar.moreViews', message: 'More views' })}
|
|
286
345
|
className={cn(SEGMENT, IDLE, 'shrink-0 px-2')}
|
|
287
346
|
type="button"
|
|
288
347
|
>
|
|
@@ -290,7 +349,9 @@ function MoreViewsMenu({
|
|
|
290
349
|
</button>
|
|
291
350
|
</DropdownMenuTrigger>
|
|
292
351
|
<DropdownMenuContent align="start" className="w-64">
|
|
293
|
-
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
352
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
353
|
+
{i18n._({ id: 'editor.views.viewBar.views', message: 'Views' })}
|
|
354
|
+
</DropdownMenuLabel>
|
|
294
355
|
{listed.map((view) => {
|
|
295
356
|
const pinned = isViewPinned(view, pinnedViews)
|
|
296
357
|
return (
|
|
@@ -300,11 +361,23 @@ function MoreViewsMenu({
|
|
|
300
361
|
onSelect={() => setPaneView(pane, view.id)}
|
|
301
362
|
>
|
|
302
363
|
<IconRefGlyph icon={view.icon} size={16} />
|
|
303
|
-
<span className="min-w-0 flex-1 truncate">{view
|
|
364
|
+
<span className="min-w-0 flex-1 truncate">{viewLabel(view, i18n)}</span>
|
|
304
365
|
{view.id === current && <Check className="h-3.5 w-3.5 text-muted-foreground" />}
|
|
305
366
|
{!isSceneView(view.id) && !view.onClose && !view.alwaysPinned && (
|
|
306
367
|
<button
|
|
307
|
-
aria-label={
|
|
368
|
+
aria-label={
|
|
369
|
+
pinned
|
|
370
|
+
? i18n._({
|
|
371
|
+
id: 'editor.views.viewBar.unpinLabel',
|
|
372
|
+
message: 'Unpin {label}',
|
|
373
|
+
values: { label: viewLabel(view, i18n) },
|
|
374
|
+
})
|
|
375
|
+
: i18n._({
|
|
376
|
+
id: 'editor.views.viewBar.pinLabel',
|
|
377
|
+
message: 'Pin {label}',
|
|
378
|
+
values: { label: viewLabel(view, i18n) },
|
|
379
|
+
})
|
|
380
|
+
}
|
|
308
381
|
aria-pressed={pinned}
|
|
309
382
|
className={cn(
|
|
310
383
|
'flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-black/6 dark:hover:bg-white/10',
|
|
@@ -316,7 +389,17 @@ function MoreViewsMenu({
|
|
|
316
389
|
setViewPinned(view.id, !pinned)
|
|
317
390
|
}}
|
|
318
391
|
onPointerDown={(event) => event.stopPropagation()}
|
|
319
|
-
title={
|
|
392
|
+
title={
|
|
393
|
+
pinned
|
|
394
|
+
? i18n._({
|
|
395
|
+
id: 'editor.views.viewBar.unpinFromTheViewBar',
|
|
396
|
+
message: 'Unpin from the view bar',
|
|
397
|
+
})
|
|
398
|
+
: i18n._({
|
|
399
|
+
id: 'editor.views.viewBar.pinToTheViewBar',
|
|
400
|
+
message: 'Pin to the view bar',
|
|
401
|
+
})
|
|
402
|
+
}
|
|
320
403
|
type="button"
|
|
321
404
|
>
|
|
322
405
|
{pinned ? <PinOff className="h-3.5 w-3.5" /> : <Pin className="h-3.5 w-3.5" />}
|
|
@@ -333,7 +416,9 @@ function MoreViewsMenu({
|
|
|
333
416
|
onSelect={() => setPaneView(pane, PLUGINS_VIEW_ID)}
|
|
334
417
|
>
|
|
335
418
|
<img alt="" className="h-4 w-4 object-contain" src="/icons/plugins.webp" />
|
|
336
|
-
<span>
|
|
419
|
+
<span>
|
|
420
|
+
{i18n._({ id: 'editor.views.viewBar.browsePlugins', message: 'Browse plugins' })}
|
|
421
|
+
</span>
|
|
337
422
|
</DropdownMenuItem>
|
|
338
423
|
</>
|
|
339
424
|
)}
|
|
@@ -20,11 +20,11 @@ import {
|
|
|
20
20
|
type WallNode,
|
|
21
21
|
} from '@pascal-app/core'
|
|
22
22
|
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
|
23
|
-
import { Html } from '@react-three/drei'
|
|
24
23
|
import { createPortal, useFrame } from '@react-three/fiber'
|
|
25
24
|
import { useMemo, useState } from 'react'
|
|
26
25
|
import * as THREE from 'three'
|
|
27
26
|
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
27
|
+
import { Html } from './localized-html'
|
|
28
28
|
|
|
29
29
|
const GUIDE_Y_OFFSET = 0.08
|
|
30
30
|
const LABEL_LIFT = 0.08
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type WallNode,
|
|
20
20
|
} from '@pascal-app/core'
|
|
21
21
|
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
-
import { Html } from '@react-three/drei'
|
|
23
22
|
import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
24
23
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
25
24
|
import {
|
|
@@ -61,6 +60,7 @@ import useInteractionScope, {
|
|
|
61
60
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
62
61
|
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
63
62
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
63
|
+
import { Html } from './localized-html'
|
|
64
64
|
import { MeasurementPill } from './measurement-pill'
|
|
65
65
|
import {
|
|
66
66
|
createArrowHitAreaGeometry,
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
NO_RAYCAST,
|
|
71
71
|
useInvisibleHitAreaMaterial,
|
|
72
72
|
} from './node-arrow-handles'
|
|
73
|
+
import { WallPullGrip } from './wall-pull-grip'
|
|
73
74
|
import { WallPushArrow } from './wall-push-arrow'
|
|
74
75
|
|
|
75
76
|
const HANDLE_OFFSET = 0.27
|
|
@@ -251,6 +252,9 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
251
252
|
/>
|
|
252
253
|
<WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
|
|
253
254
|
<WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
|
|
255
|
+
{/* Past each end, the grip that pulls the next wall out of it. */}
|
|
256
|
+
<WallPullGrip endpoint="start" wall={effectiveWall} />
|
|
257
|
+
<WallPullGrip endpoint="end" wall={effectiveWall} />
|
|
254
258
|
</group>
|
|
255
259
|
<WallBaseElevationHandle
|
|
256
260
|
baseElevation={baseElevation}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { nodeRegistry, type WallNode } from '@pascal-app/core'
|
|
4
|
+
import { type ThreeEvent, useThree } from '@react-three/fiber'
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
+
import { Color, DoubleSide, ExtrudeGeometry, OrthographicCamera, Shape } from 'three'
|
|
7
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
8
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
9
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
10
|
+
import { startWallPull, WALL_PULL_GRIP_TITLE, wallPullSeed } from '../../lib/wall-pull'
|
|
11
|
+
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
12
|
+
import {
|
|
13
|
+
createEndpointHitAreaGeometry,
|
|
14
|
+
InvisibleHandleHitArea,
|
|
15
|
+
NO_RAYCAST,
|
|
16
|
+
useInvisibleHitAreaMaterial,
|
|
17
|
+
} from './handles/handle-arrow'
|
|
18
|
+
|
|
19
|
+
const PULL_FILL = '#10b981' // emerald-500 — the 2D grip's colour
|
|
20
|
+
const PULL_HOVER_FILL = '#34d399' // emerald-400
|
|
21
|
+
// Out from the wall end (m, before zoom): clear of the corner picker's hex.
|
|
22
|
+
const PULL_ARROW_OFFSET = 0.2
|
|
23
|
+
const PULL_ARROW_LENGTH = 0.34
|
|
24
|
+
const PULL_ARROW_HALF_WIDTH = 0.11
|
|
25
|
+
const PULL_ARROW_SHAFT_HALF_WIDTH = 0.04
|
|
26
|
+
const PULL_HIT_RADIUS = 0.17
|
|
27
|
+
const PULL_FLOOR_LIFT = 0.02
|
|
28
|
+
|
|
29
|
+
// The grip's tooltip: one plain DOM element beside the pointer, so it never
|
|
30
|
+
// lives in (or unmounts with) the R3F tree.
|
|
31
|
+
let tooltip: HTMLDivElement | null = null
|
|
32
|
+
|
|
33
|
+
function showGripTooltip(clientX: number, clientY: number) {
|
|
34
|
+
if (typeof document === 'undefined') return
|
|
35
|
+
if (!tooltip) {
|
|
36
|
+
tooltip = document.createElement('div')
|
|
37
|
+
tooltip.textContent = WALL_PULL_GRIP_TITLE
|
|
38
|
+
tooltip.setAttribute('role', 'tooltip')
|
|
39
|
+
Object.assign(tooltip.style, {
|
|
40
|
+
position: 'fixed',
|
|
41
|
+
zIndex: '60',
|
|
42
|
+
pointerEvents: 'none',
|
|
43
|
+
whiteSpace: 'nowrap',
|
|
44
|
+
borderRadius: '6px',
|
|
45
|
+
background: 'rgba(24, 24, 27, 0.92)',
|
|
46
|
+
color: '#fff',
|
|
47
|
+
font: '500 11px/1.4 system-ui, -apple-system, sans-serif',
|
|
48
|
+
padding: '4px 8px',
|
|
49
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
tooltip.style.left = `${clientX + 14}px`
|
|
53
|
+
tooltip.style.top = `${clientY + 18}px`
|
|
54
|
+
if (!tooltip.isConnected) document.body.appendChild(tooltip)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hideGripTooltip() {
|
|
58
|
+
tooltip?.remove()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A flat outward arrow (tip along +X), lying in the XZ plane from x = 0. */
|
|
62
|
+
function createPullArrowGeometry() {
|
|
63
|
+
const head = PULL_ARROW_LENGTH * 0.45
|
|
64
|
+
const shape = new Shape()
|
|
65
|
+
shape.moveTo(0, -PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
66
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, -PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
67
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, -PULL_ARROW_HALF_WIDTH)
|
|
68
|
+
shape.lineTo(PULL_ARROW_LENGTH, 0)
|
|
69
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, PULL_ARROW_HALF_WIDTH)
|
|
70
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
71
|
+
shape.lineTo(0, PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
72
|
+
shape.lineTo(0, -PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
73
|
+
const geometry = new ExtrudeGeometry(shape, {
|
|
74
|
+
depth: 0.03,
|
|
75
|
+
bevelEnabled: true,
|
|
76
|
+
bevelThickness: 0.01,
|
|
77
|
+
bevelSize: 0.012,
|
|
78
|
+
bevelSegments: 4,
|
|
79
|
+
})
|
|
80
|
+
geometry.translate(0, 0, -0.015)
|
|
81
|
+
geometry.rotateX(-Math.PI / 2)
|
|
82
|
+
geometry.computeVertexNormals()
|
|
83
|
+
geometry.computeBoundingSphere()
|
|
84
|
+
return geometry
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A selected wall end's "pull to continue" grip in 3D — the twin of the floor
|
|
89
|
+
* plan's: an emerald arrow lying on the floor just past the end, pointing out
|
|
90
|
+
* along the wall (along its tangent when curved), titled "Drag to draw the next
|
|
91
|
+
* wall" while the pointer rests on it. Pressing it starts
|
|
92
|
+
* `startWallPull`: the wall tool's Walls run opens at that end, the drag shows
|
|
93
|
+
* the next wall, and the release (the canvas click) places it. Screen-sized
|
|
94
|
+
* like the corner picker beside it; rendered in the wall's level frame at its
|
|
95
|
+
* base, by `WallMoveSideHandles`.
|
|
96
|
+
*/
|
|
97
|
+
export function WallPullGrip({ wall, endpoint }: { wall: WallNode; endpoint: 'start' | 'end' }) {
|
|
98
|
+
const [hover, setHover] = useState(false)
|
|
99
|
+
const { camera } = useThree()
|
|
100
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
101
|
+
const seed = wallPullSeed(wall, endpoint)
|
|
102
|
+
const geometry = useMemo(() => createPullArrowGeometry(), [])
|
|
103
|
+
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(PULL_HIT_RADIUS), [])
|
|
104
|
+
const hitMaterial = useInvisibleHitAreaMaterial()
|
|
105
|
+
const material = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
new MeshBasicNodeMaterial({
|
|
108
|
+
color: new Color(PULL_FILL),
|
|
109
|
+
side: DoubleSide,
|
|
110
|
+
transparent: true,
|
|
111
|
+
opacity: 0.95,
|
|
112
|
+
depthTest: false,
|
|
113
|
+
depthWrite: false,
|
|
114
|
+
toneMapped: false,
|
|
115
|
+
}),
|
|
116
|
+
[],
|
|
117
|
+
)
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
material.color.set(hover ? PULL_HOVER_FILL : PULL_FILL)
|
|
120
|
+
}, [hover, material])
|
|
121
|
+
useEffect(() => () => geometry.dispose(), [geometry])
|
|
122
|
+
useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
|
|
123
|
+
useEffect(() => () => material.dispose(), [material])
|
|
124
|
+
// Warm the wall tool's module so the press starts drawing at once.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
void nodeRegistry
|
|
127
|
+
.get('wall')
|
|
128
|
+
?.tool?.()
|
|
129
|
+
.catch(() => {})
|
|
130
|
+
}, [])
|
|
131
|
+
useEffect(
|
|
132
|
+
() => () => {
|
|
133
|
+
hideGripTooltip()
|
|
134
|
+
if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
|
|
135
|
+
},
|
|
136
|
+
[],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (!seed) return null
|
|
140
|
+
const [dx, dz] = seed.reference
|
|
141
|
+
const offset = PULL_ARROW_OFFSET * zoom
|
|
142
|
+
const position: [number, number, number] = [
|
|
143
|
+
seed.start[0] + dx * offset,
|
|
144
|
+
PULL_FLOOR_LIFT,
|
|
145
|
+
seed.start[1] + dz * offset,
|
|
146
|
+
]
|
|
147
|
+
const rotationY = Math.atan2(-dz, dx)
|
|
148
|
+
|
|
149
|
+
const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
|
|
150
|
+
if (event.button !== 0) return
|
|
151
|
+
event.stopPropagation()
|
|
152
|
+
suppressBoxSelectForPointer(event)
|
|
153
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
154
|
+
setHover(false)
|
|
155
|
+
hideGripTooltip()
|
|
156
|
+
if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
|
|
157
|
+
startWallPull({ wallId: wall.id, endpoint, origin: '3d' })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<group position={position} rotation={[0, rotationY, 0]} scale={zoom}>
|
|
162
|
+
<group position={[PULL_ARROW_LENGTH / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
163
|
+
<InvisibleHandleHitArea
|
|
164
|
+
geometry={hitGeometry}
|
|
165
|
+
material={hitMaterial}
|
|
166
|
+
onPointerDown={onPointerDown}
|
|
167
|
+
onPointerEnter={(event) => {
|
|
168
|
+
event.stopPropagation()
|
|
169
|
+
setHover(true)
|
|
170
|
+
showGripTooltip(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
171
|
+
document.body.style.cursor = 'grab'
|
|
172
|
+
}}
|
|
173
|
+
onPointerLeave={(event) => {
|
|
174
|
+
event.stopPropagation()
|
|
175
|
+
setHover(false)
|
|
176
|
+
hideGripTooltip()
|
|
177
|
+
if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
|
|
178
|
+
}}
|
|
179
|
+
scale={1}
|
|
180
|
+
/>
|
|
181
|
+
</group>
|
|
182
|
+
<mesh
|
|
183
|
+
frustumCulled={false}
|
|
184
|
+
geometry={geometry}
|
|
185
|
+
layers={EDITOR_LAYER}
|
|
186
|
+
material={material}
|
|
187
|
+
raycast={NO_RAYCAST}
|
|
188
|
+
renderOrder={1003}
|
|
189
|
+
scale={hover ? 1.15 : 1}
|
|
190
|
+
/>
|
|
191
|
+
</group>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
17
|
import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
|
|
18
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
19
|
+
import { Html } from '@react-three/drei'
|
|
19
20
|
import { useFrame } from '@react-three/fiber'
|
|
20
21
|
import { memo, useMemo, useRef } from 'react'
|
|
21
22
|
import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
|
|
@@ -31,10 +32,14 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
31
32
|
*
|
|
32
33
|
* endpoint (corner) → square midpoint → triangle
|
|
33
34
|
* intersection → ✕ cross wall body (edge) → circle
|
|
35
|
+
* perpendicular → ⊥ corner projection guide → diamond
|
|
36
|
+
* grid length → small +
|
|
34
37
|
*
|
|
35
38
|
* Subscribes to the shared `useWallSnapIndicator` store (published by the wall
|
|
36
39
|
* draft + endpoint-move tools). The vertical mouse pillar and the corner
|
|
37
|
-
* (endpoint) square are green; the other floor glyphs are indigo.
|
|
40
|
+
* (endpoint) square are green; the other floor glyphs are indigo. A point that
|
|
41
|
+
* carries a `label` (a wall pulled from a wall end) also names what it caught
|
|
42
|
+
* in a pill beside the foot of the pillar.
|
|
38
43
|
*
|
|
39
44
|
* The point carries only XZ (building-local plan coords); like the alignment
|
|
40
45
|
* guides it's lifted to the active level's building-local Y each frame so it
|
|
@@ -135,6 +140,23 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
|
135
140
|
renderOrder={1002}
|
|
136
141
|
/>
|
|
137
142
|
<SnapMarker kind={point.kind} x={point.x} z={point.z} />
|
|
143
|
+
{point.label ? (
|
|
144
|
+
<Html
|
|
145
|
+
position={[point.x, 0.05, point.z]}
|
|
146
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
147
|
+
zIndexRange={[24, 0]}
|
|
148
|
+
>
|
|
149
|
+
<div
|
|
150
|
+
className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-sans font-semibold text-[11px] text-white"
|
|
151
|
+
style={{
|
|
152
|
+
backgroundColor: point.kind === 'endpoint' ? '#16a34a' : '#6366f1',
|
|
153
|
+
transform: 'translate(12px, 8px)',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{point.label}
|
|
157
|
+
</div>
|
|
158
|
+
</Html>
|
|
159
|
+
) : null}
|
|
138
160
|
</group>
|
|
139
161
|
)
|
|
140
162
|
})
|
|
@@ -295,6 +317,64 @@ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }
|
|
|
295
317
|
</>
|
|
296
318
|
)
|
|
297
319
|
}
|
|
320
|
+
if (kind === 'perpendicular') {
|
|
321
|
+
// ⊥ — a base bar with an upright from its middle.
|
|
322
|
+
return (
|
|
323
|
+
<>
|
|
324
|
+
<mesh
|
|
325
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
326
|
+
layers={EDITOR_LAYER}
|
|
327
|
+
material={beaconMaterial}
|
|
328
|
+
position={[x, y, z]}
|
|
329
|
+
renderOrder={1001}
|
|
330
|
+
scale={[MARKER * 2.6, 1, MARKER * 0.45]}
|
|
331
|
+
/>
|
|
332
|
+
<mesh
|
|
333
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
334
|
+
layers={EDITOR_LAYER}
|
|
335
|
+
material={beaconMaterial}
|
|
336
|
+
position={[x, y, z - MARKER * 0.8]}
|
|
337
|
+
renderOrder={1001}
|
|
338
|
+
scale={[MARKER * 0.45, 1, MARKER * 1.6]}
|
|
339
|
+
/>
|
|
340
|
+
</>
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
if (kind === 'alignment') {
|
|
344
|
+
return (
|
|
345
|
+
<mesh
|
|
346
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
347
|
+
layers={EDITOR_LAYER}
|
|
348
|
+
material={beaconMaterial}
|
|
349
|
+
position={[x, y, z]}
|
|
350
|
+
renderOrder={1001}
|
|
351
|
+
rotation={[0, Math.PI / 4, 0]}
|
|
352
|
+
scale={[MARKER * 1.4, 1, MARKER * 1.4]}
|
|
353
|
+
/>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
if (kind === 'grid') {
|
|
357
|
+
return (
|
|
358
|
+
<>
|
|
359
|
+
<mesh
|
|
360
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
361
|
+
layers={EDITOR_LAYER}
|
|
362
|
+
material={beaconMaterial}
|
|
363
|
+
position={[x, y, z]}
|
|
364
|
+
renderOrder={1001}
|
|
365
|
+
scale={[MARKER * 1.8, 1, MARKER * 0.35]}
|
|
366
|
+
/>
|
|
367
|
+
<mesh
|
|
368
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
369
|
+
layers={EDITOR_LAYER}
|
|
370
|
+
material={beaconMaterial}
|
|
371
|
+
position={[x, y, z]}
|
|
372
|
+
renderOrder={1001}
|
|
373
|
+
scale={[MARKER * 0.35, 1, MARKER * 1.8]}
|
|
374
|
+
/>
|
|
375
|
+
</>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
298
378
|
// 'wall' (edge / along-wall) → circle
|
|
299
379
|
return (
|
|
300
380
|
<mesh
|