@pascal-app/editor 1.0.7 → 1.1.1
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 +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +101 -45
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1983
- 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 +5 -3
- 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 +8 -4
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +199 -30
- 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/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +43 -13
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +2 -2
- 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/room-controls.tsx +10 -6
- package/src/components/editor/room-handles-2d.tsx +3 -3
- package/src/components/editor/room-handles.tsx +4 -5
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +41 -1670
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +11 -7
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
- 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 +30 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +33 -21
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
- 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/use-placement-coordinator.tsx +3 -4
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- 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-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
- package/src/components/ui/helpers/helper-manager.tsx +55 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
- package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +42 -19
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +7 -7
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +7 -9
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +22 -11
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +1036 -306
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2267 -439
- package/src/i18n/locales/fr/messages.po +2267 -439
- package/src/i18n/locales/ja/messages.po +2267 -439
- package/src/i18n/locales/zh-CN/messages.po +2267 -439
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +183 -178
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -3
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +2 -2
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- 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/material-paint.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +7 -5
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/paint-region-mode.ts +1 -1
- 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/point-ask/session.ts +1 -2
- 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/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +8 -37
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +22 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- 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/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/measurements.ts +0 -225
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -183
- package/src/lib/scene-bounds.ts +0 -169
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/window-interaction.ts +0 -142
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
package/src/store/use-editor.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ChimneyMaterialRole,
|
|
11
11
|
DEFAULT_BRUSH_SETTINGS,
|
|
12
12
|
type DormerSurfaceMaterialRole,
|
|
13
|
+
isMoveLocked,
|
|
13
14
|
type LevelNode,
|
|
14
15
|
nodeRegistry,
|
|
15
16
|
type RoofSurfaceMaterialRole,
|
|
@@ -20,6 +21,15 @@ import {
|
|
|
20
21
|
type WallSurfaceSide,
|
|
21
22
|
} from '@pascal-app/core'
|
|
22
23
|
import { useViewer } from '@pascal-app/viewer'
|
|
24
|
+
import {
|
|
25
|
+
cameraPoseStore,
|
|
26
|
+
type NavigationSyncPose,
|
|
27
|
+
type NavigationSyncPoseInput,
|
|
28
|
+
publishNavigationSyncPose,
|
|
29
|
+
type RoomKey,
|
|
30
|
+
sameRoom,
|
|
31
|
+
subscribeNavigationSyncPose,
|
|
32
|
+
} from '@pascal-app/viewer/ui'
|
|
23
33
|
import { create } from 'zustand'
|
|
24
34
|
import { persist } from 'zustand/middleware'
|
|
25
35
|
import {
|
|
@@ -55,7 +65,6 @@ import {
|
|
|
55
65
|
type PaintHoverInfo,
|
|
56
66
|
type PaintScope,
|
|
57
67
|
} from '../lib/paint-scope'
|
|
58
|
-
import { type RoomKey, sameRoom } from '../lib/room-selection'
|
|
59
68
|
import {
|
|
60
69
|
cycleSnappingModeIn,
|
|
61
70
|
defaultSnappingModeFor,
|
|
@@ -65,8 +74,6 @@ import {
|
|
|
65
74
|
snapContextOf,
|
|
66
75
|
snappingModesFor,
|
|
67
76
|
} from '../lib/snapping-mode'
|
|
68
|
-
import { cameraPoseStore } from './camera-pose-store'
|
|
69
|
-
import { publishNavigationSyncPoseToStore } from './navigation-sync-pose-store'
|
|
70
77
|
import useInteractionScope from './use-interaction-scope'
|
|
71
78
|
import {
|
|
72
79
|
clampViewPaneRatio,
|
|
@@ -232,18 +239,6 @@ export type CatalogCategory =
|
|
|
232
239
|
export type FloorplanSelectionTool = 'click' | 'marquee'
|
|
233
240
|
export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
|
|
234
241
|
|
|
235
|
-
export type NavigationSyncSource = '2d' | '3d'
|
|
236
|
-
|
|
237
|
-
export type NavigationSyncPose = {
|
|
238
|
-
source: NavigationSyncSource
|
|
239
|
-
revision: number
|
|
240
|
-
target: [number, number, number]
|
|
241
|
-
azimuth: number
|
|
242
|
-
viewWidth: number
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
|
|
246
|
-
|
|
247
242
|
// Combined tool type. Known literals keep autocomplete; the `(string & {})`
|
|
248
243
|
// arm lets plugin-contributed tool ids (e.g. `'trees:tree'`) typecheck without
|
|
249
244
|
// the host enumerating every plugin kind. The runtime dispatch is already
|
|
@@ -489,12 +484,13 @@ type EditorState = {
|
|
|
489
484
|
setViewPinned: (viewId: string, pinned: boolean) => void
|
|
490
485
|
isFloorplanHovered: boolean
|
|
491
486
|
setFloorplanHovered: (hovered: boolean) => void
|
|
487
|
+
/** Mirror of viewer/ui's navigation sync store, kept for plugins that read it here. */
|
|
488
|
+
navigationSyncPose: NavigationSyncPose | null
|
|
489
|
+
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
492
490
|
// Toggleable DWV riser-diagram (plumbing isometric) overlay.
|
|
493
491
|
isRiserOpen: boolean
|
|
494
492
|
setRiserOpen: (open: boolean) => void
|
|
495
493
|
toggleRiserOpen: () => void
|
|
496
|
-
navigationSyncPose: NavigationSyncPose | null
|
|
497
|
-
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
498
494
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
499
495
|
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
500
496
|
gridSnapStep: GridSnapStep
|
|
@@ -1256,6 +1252,10 @@ const useEditor = create<EditorState>()(
|
|
|
1256
1252
|
set({ placementDragMode: false })
|
|
1257
1253
|
return
|
|
1258
1254
|
}
|
|
1255
|
+
if (isMoveLocked(node)) {
|
|
1256
|
+
set({ placementDragMode: false })
|
|
1257
|
+
return
|
|
1258
|
+
}
|
|
1259
1259
|
const targetNode = node
|
|
1260
1260
|
const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
|
|
1261
1261
|
if (isNew) {
|
|
@@ -1533,18 +1533,11 @@ const useEditor = create<EditorState>()(
|
|
|
1533
1533
|
set((state) => ({ pinnedViews: { ...state.pinnedViews, [viewId]: pinned } })),
|
|
1534
1534
|
isFloorplanHovered: false,
|
|
1535
1535
|
setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
|
|
1536
|
+
navigationSyncPose: null,
|
|
1537
|
+
publishNavigationSyncPose,
|
|
1536
1538
|
isRiserOpen: false,
|
|
1537
1539
|
setRiserOpen: (open) => set({ isRiserOpen: open }),
|
|
1538
1540
|
toggleRiserOpen: () => set((state) => ({ isRiserOpen: !state.isRiserOpen })),
|
|
1539
|
-
navigationSyncPose: null,
|
|
1540
|
-
publishNavigationSyncPose: (pose) => {
|
|
1541
|
-
const navigationSyncPose = {
|
|
1542
|
-
...pose,
|
|
1543
|
-
revision: (get().navigationSyncPose?.revision ?? 0) + 1,
|
|
1544
|
-
}
|
|
1545
|
-
publishNavigationSyncPoseToStore(navigationSyncPose)
|
|
1546
|
-
set({ navigationSyncPose })
|
|
1547
|
-
},
|
|
1548
1541
|
floorplanSelectionTool: 'click' as FloorplanSelectionTool,
|
|
1549
1542
|
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
1550
1543
|
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
@@ -1858,4 +1851,7 @@ usePaintRegionMode.subscribe((state) => {
|
|
|
1858
1851
|
if (useEditor.getState().paintEraser !== paintEraser) useEditor.setState({ paintEraser })
|
|
1859
1852
|
})
|
|
1860
1853
|
|
|
1854
|
+
// Published plugins read the 2D/3D navigation pose from the editor store; the shared store owns it.
|
|
1855
|
+
subscribeNavigationSyncPose((navigationSyncPose) => useEditor.setState({ navigationSyncPose }))
|
|
1856
|
+
|
|
1861
1857
|
export default useEditor
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES,
|
|
5
|
+
type FloorplanWallDimensionFaces,
|
|
6
|
+
normalizeFloorplanWallDimensionFaces,
|
|
7
|
+
} from '@pascal-app/core'
|
|
3
8
|
import { create } from 'zustand'
|
|
4
9
|
import { persist } from 'zustand/middleware'
|
|
5
10
|
import {
|
|
@@ -8,11 +13,6 @@ import {
|
|
|
8
13
|
type FloorplanAnnotationVisibility,
|
|
9
14
|
normalizeFloorplanAnnotationVisibility,
|
|
10
15
|
} from '../lib/floorplan/annotation-visibility'
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES,
|
|
13
|
-
type FloorplanWallDimensionFaces,
|
|
14
|
-
normalizeFloorplanWallDimensionFaces,
|
|
15
|
-
} from '../lib/floorplan/floorplan-extension'
|
|
16
16
|
|
|
17
17
|
type PersistedFloorplanAnnotationState = {
|
|
18
18
|
visibility: FloorplanAnnotationVisibility
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
isPlacementTypingKey,
|
|
4
|
+
PLACEMENT_TYPING_DISTANCE_OFFSET,
|
|
5
|
+
PLACEMENT_TYPING_LENGTH_ANGLE,
|
|
6
|
+
routePlacementTypingKey,
|
|
7
|
+
usePlacementTyping,
|
|
8
|
+
} from './use-placement-typing'
|
|
9
|
+
|
|
10
|
+
const key = (
|
|
11
|
+
k: string,
|
|
12
|
+
mods: Partial<Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey'>> = {},
|
|
13
|
+
) => ({
|
|
14
|
+
key: k,
|
|
15
|
+
metaKey: false,
|
|
16
|
+
ctrlKey: false,
|
|
17
|
+
altKey: false,
|
|
18
|
+
...mods,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const beginTwoFields = (activeField: 0 | 1) => {
|
|
22
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET, activeField })
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('usePlacementTyping', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
usePlacementTyping.getState().clear()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('keeps two independent field buffers and toggles the active field', () => {
|
|
32
|
+
const typing = usePlacementTyping.getState()
|
|
33
|
+
typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET, fieldDefaults: ['1.2', '0'] })
|
|
34
|
+
typing.append('2')
|
|
35
|
+
typing.toggleField()
|
|
36
|
+
typing.append('-')
|
|
37
|
+
typing.append('4')
|
|
38
|
+
|
|
39
|
+
expect(usePlacementTyping.getState().fields).toEqual(['2', '-4'])
|
|
40
|
+
expect(usePlacementTyping.getState().fieldDefaults).toEqual(['1.2', '0'])
|
|
41
|
+
expect(usePlacementTyping.getState().activeField).toBe(1)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('backspace only changes the active field', () => {
|
|
45
|
+
const typing = usePlacementTyping.getState()
|
|
46
|
+
typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET })
|
|
47
|
+
typing.append('25')
|
|
48
|
+
typing.toggleField()
|
|
49
|
+
typing.append('3')
|
|
50
|
+
typing.backspace()
|
|
51
|
+
|
|
52
|
+
expect(usePlacementTyping.getState().fields).toEqual(['25', ''])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('publishes projection and an enter request without clearing input', () => {
|
|
56
|
+
const typing = usePlacementTyping.getState()
|
|
57
|
+
typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET })
|
|
58
|
+
typing.append('2')
|
|
59
|
+
typing.setProjectedPosition([2, 0, 1])
|
|
60
|
+
typing.requestCommit()
|
|
61
|
+
|
|
62
|
+
expect(usePlacementTyping.getState().projectedPosition).toEqual([2, 0, 1])
|
|
63
|
+
expect(usePlacementTyping.getState().submitRevision).toBe(1)
|
|
64
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('clear ends the session and forgets its labels', () => {
|
|
68
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE })
|
|
69
|
+
usePlacementTyping.getState().append('3')
|
|
70
|
+
usePlacementTyping.getState().clear()
|
|
71
|
+
const state = usePlacementTyping.getState()
|
|
72
|
+
expect(state.isActive).toBe(false)
|
|
73
|
+
expect(state.fields).toEqual(['', ''])
|
|
74
|
+
expect(state.labels).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('routePlacementTypingKey', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
usePlacementTyping.getState().clear()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a digit or a leading dot opens a session on the first field', () => {
|
|
84
|
+
expect(routePlacementTypingKey(key('5'), { begin: beginTwoFields })).toBe('edit')
|
|
85
|
+
expect(usePlacementTyping.getState().fields).toEqual(['5', ''])
|
|
86
|
+
usePlacementTyping.getState().clear()
|
|
87
|
+
expect(routePlacementTypingKey(key('.'), { begin: beginTwoFields })).toBe('edit')
|
|
88
|
+
expect(usePlacementTyping.getState().fields).toEqual(['.', ''])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('letters, signs and separators leave tool shortcuts alone until a session opens', () => {
|
|
92
|
+
for (const k of ['c', 'm', 't', 'f', '-', ' ', ',']) {
|
|
93
|
+
expect(routePlacementTypingKey(key(k), { begin: beginTwoFields })).toBeNull()
|
|
94
|
+
}
|
|
95
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('no session opens when the tool refuses or offers no begin', () => {
|
|
99
|
+
expect(routePlacementTypingKey(key('5'), { begin: () => false })).toBeNull()
|
|
100
|
+
expect(routePlacementTypingKey(key('5'))).toBeNull()
|
|
101
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('Tab opens a session on the second field only when the tool asks for it', () => {
|
|
105
|
+
expect(routePlacementTypingKey(key('Tab'), { begin: beginTwoFields })).toBeNull()
|
|
106
|
+
expect(routePlacementTypingKey(key('Tab'), { begin: beginTwoFields, tabBegins: true })).toBe(
|
|
107
|
+
'edit',
|
|
108
|
+
)
|
|
109
|
+
expect(usePlacementTyping.getState().activeField).toBe(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('inside a session: unit letters extend, Tab and comma switch, Enter commits', () => {
|
|
113
|
+
routePlacementTypingKey(key('1'), { begin: beginTwoFields })
|
|
114
|
+
for (const k of ['8', '0', 'c', 'm']) routePlacementTypingKey(key(k))
|
|
115
|
+
expect(routePlacementTypingKey(key('Tab'))).toBe('edit')
|
|
116
|
+
routePlacementTypingKey(key('-'))
|
|
117
|
+
routePlacementTypingKey(key('2'))
|
|
118
|
+
expect(routePlacementTypingKey(key(','))).toBe('edit')
|
|
119
|
+
expect(usePlacementTyping.getState().fields).toEqual(['180cm', '-2'])
|
|
120
|
+
expect(usePlacementTyping.getState().activeField).toBe(0)
|
|
121
|
+
expect(routePlacementTypingKey(key('Enter'))).toBe('commit')
|
|
122
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('Backspace edits, Delete empties the active field', () => {
|
|
126
|
+
routePlacementTypingKey(key('4'), { begin: beginTwoFields })
|
|
127
|
+
routePlacementTypingKey(key('2'))
|
|
128
|
+
routePlacementTypingKey(key('Backspace'))
|
|
129
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('4')
|
|
130
|
+
routePlacementTypingKey(key('Delete'))
|
|
131
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('')
|
|
132
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('the first Escape only ends the session', () => {
|
|
136
|
+
routePlacementTypingKey(key('3'), { begin: beginTwoFields })
|
|
137
|
+
expect(routePlacementTypingKey(key('Escape'))).toBe('clear')
|
|
138
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
139
|
+
expect(routePlacementTypingKey(key('Escape'))).toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('chords are never consumed', () => {
|
|
143
|
+
expect(
|
|
144
|
+
routePlacementTypingKey(key('5', { metaKey: true }), { begin: beginTwoFields }),
|
|
145
|
+
).toBeNull()
|
|
146
|
+
routePlacementTypingKey(key('5'), { begin: beginTwoFields })
|
|
147
|
+
expect(routePlacementTypingKey(key('z', { ctrlKey: true }))).toBeNull()
|
|
148
|
+
expect(routePlacementTypingKey(key('Enter', { altKey: true }))).toBeNull()
|
|
149
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('5')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('isPlacementTypingKey', () => {
|
|
154
|
+
test('accepts the measurement grammar', () => {
|
|
155
|
+
for (const k of ['0', '9', 'm', 'c', '.', "'", '"', '-', '+', ' ', '/']) {
|
|
156
|
+
expect(isPlacementTypingKey(k)).toBe(true)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('rejects control keys and the field separator', () => {
|
|
161
|
+
for (const k of ['Enter', 'Escape', 'Tab', 'Shift', '', 'F1', ',']) {
|
|
162
|
+
expect(isPlacementTypingKey(k)).toBe(false)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed coordinate entry shared by the placement tools: a drafted wall's
|
|
6
|
+
* length/angle and a cabinet's distance/offset along its wall. One session at
|
|
7
|
+
* a time; the active tool owns what the two fields mean and reads them on
|
|
8
|
+
* every change, the HUD (`PlacementCoordinateInput`) only displays them.
|
|
9
|
+
*/
|
|
10
|
+
export type PlacementTypingField = 0 | 1
|
|
11
|
+
export type PlacementTypingValues = [string, string]
|
|
12
|
+
export type PlacementTypingProjection = [number, number, number]
|
|
13
|
+
|
|
14
|
+
export const PLACEMENT_TYPING_DISTANCE_OFFSET: readonly MessageDescriptor[] = [
|
|
15
|
+
/* i18n */ { id: 'editor.placementTyping.distance', message: 'Distance' },
|
|
16
|
+
/* i18n */ { id: 'editor.placementTyping.offset', message: 'Offset' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const PLACEMENT_TYPING_LENGTH_ANGLE: readonly MessageDescriptor[] = [
|
|
20
|
+
/* i18n */ { id: 'editor.placementTyping.length', message: 'Length' },
|
|
21
|
+
/* i18n */ { id: 'editor.placementTyping.angle', message: 'Angle' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export type PlacementTypingSession = {
|
|
25
|
+
/** What each field means, in field order; the HUD shows one box per label. */
|
|
26
|
+
labels: readonly MessageDescriptor[]
|
|
27
|
+
fieldDefaults?: PlacementTypingValues
|
|
28
|
+
activeField?: PlacementTypingField
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type PlacementTypingState = {
|
|
32
|
+
isActive: boolean
|
|
33
|
+
labels: readonly MessageDescriptor[]
|
|
34
|
+
fields: PlacementTypingValues
|
|
35
|
+
/** What an empty field stands for, shown as its placeholder. */
|
|
36
|
+
fieldDefaults: PlacementTypingValues
|
|
37
|
+
activeField: PlacementTypingField
|
|
38
|
+
projectedPosition: PlacementTypingProjection | null
|
|
39
|
+
submitRevision: number
|
|
40
|
+
begin: (session: PlacementTypingSession) => void
|
|
41
|
+
append: (value: string) => void
|
|
42
|
+
backspace: () => void
|
|
43
|
+
setField: (field: PlacementTypingField, value: string) => void
|
|
44
|
+
setActiveField: (field: PlacementTypingField) => void
|
|
45
|
+
toggleField: () => void
|
|
46
|
+
setProjectedPosition: (position: PlacementTypingProjection | null) => void
|
|
47
|
+
requestCommit: () => void
|
|
48
|
+
clear: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const EMPTY_VALUES: PlacementTypingValues = ['', '']
|
|
52
|
+
|
|
53
|
+
export const usePlacementTyping = create<PlacementTypingState>((set) => ({
|
|
54
|
+
isActive: false,
|
|
55
|
+
labels: [],
|
|
56
|
+
fields: [...EMPTY_VALUES],
|
|
57
|
+
fieldDefaults: [...EMPTY_VALUES],
|
|
58
|
+
activeField: 0,
|
|
59
|
+
projectedPosition: null,
|
|
60
|
+
submitRevision: 0,
|
|
61
|
+
begin: ({ labels, fieldDefaults = EMPTY_VALUES, activeField = 0 }) =>
|
|
62
|
+
set({
|
|
63
|
+
isActive: true,
|
|
64
|
+
labels,
|
|
65
|
+
fields: [...EMPTY_VALUES],
|
|
66
|
+
fieldDefaults: [...fieldDefaults],
|
|
67
|
+
activeField,
|
|
68
|
+
projectedPosition: null,
|
|
69
|
+
}),
|
|
70
|
+
append: (value) =>
|
|
71
|
+
set((state) => {
|
|
72
|
+
const fields: PlacementTypingValues = [...state.fields]
|
|
73
|
+
fields[state.activeField] += value
|
|
74
|
+
return { isActive: true, fields }
|
|
75
|
+
}),
|
|
76
|
+
backspace: () =>
|
|
77
|
+
set((state) => {
|
|
78
|
+
const fields: PlacementTypingValues = [...state.fields]
|
|
79
|
+
fields[state.activeField] = fields[state.activeField].slice(0, -1)
|
|
80
|
+
return { fields }
|
|
81
|
+
}),
|
|
82
|
+
setField: (field, value) =>
|
|
83
|
+
set((state) => {
|
|
84
|
+
const fields: PlacementTypingValues = [...state.fields]
|
|
85
|
+
fields[field] = value
|
|
86
|
+
return { isActive: true, fields }
|
|
87
|
+
}),
|
|
88
|
+
setActiveField: (activeField) => set({ activeField }),
|
|
89
|
+
toggleField: () => set((state) => ({ activeField: state.activeField === 0 ? 1 : 0 })),
|
|
90
|
+
setProjectedPosition: (projectedPosition) => set({ projectedPosition }),
|
|
91
|
+
requestCommit: () =>
|
|
92
|
+
set((state: PlacementTypingState) => ({ submitRevision: state.submitRevision + 1 })),
|
|
93
|
+
clear: () =>
|
|
94
|
+
set({
|
|
95
|
+
isActive: false,
|
|
96
|
+
labels: [],
|
|
97
|
+
fields: [...EMPTY_VALUES],
|
|
98
|
+
fieldDefaults: [...EMPTY_VALUES],
|
|
99
|
+
activeField: 0,
|
|
100
|
+
projectedPosition: null,
|
|
101
|
+
}),
|
|
102
|
+
}))
|
|
103
|
+
|
|
104
|
+
/** Keys that extend a field: digits, unit letters, separators, signs, fractions. */
|
|
105
|
+
export function isPlacementTypingKey(key: string): boolean {
|
|
106
|
+
return key.length === 1 && /^[0-9a-zA-Z.'"+\-/ ]$/.test(key)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keys that may open a session. Letters only ever continue a field, so a
|
|
111
|
+
* tool's single-letter shortcuts keep working until a number is typed.
|
|
112
|
+
*/
|
|
113
|
+
export function isPlacementTypingStartKey(key: string): boolean {
|
|
114
|
+
return key.length === 1 && /^[0-9.]$/.test(key)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type PlacementTypingKeyOutcome = 'edit' | 'commit' | 'clear'
|
|
118
|
+
|
|
119
|
+
export type PlacementTypingKeyOptions = {
|
|
120
|
+
/**
|
|
121
|
+
* Opens a session for a start key (or Tab, with `tabBegins`) when none is
|
|
122
|
+
* active. Returns false when the tool cannot take typed entry right now.
|
|
123
|
+
*/
|
|
124
|
+
begin?: (activeField: PlacementTypingField) => boolean
|
|
125
|
+
/** Tab with no session opens one on the second field. */
|
|
126
|
+
tabBegins?: boolean
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The one keyboard convention for typed entry. Tab or comma switches field,
|
|
131
|
+
* Enter asks the tool to commit, Escape clears the session (the tool's next
|
|
132
|
+
* Escape cancels its draft), Backspace edits and Delete empties the active
|
|
133
|
+
* field. Chords with Cmd/Ctrl/Alt are never consumed. Returns null when the
|
|
134
|
+
* key is not part of typed entry; the caller prevents default and stops
|
|
135
|
+
* propagation for every other outcome.
|
|
136
|
+
*/
|
|
137
|
+
export function routePlacementTypingKey(
|
|
138
|
+
event: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>,
|
|
139
|
+
options: PlacementTypingKeyOptions = {},
|
|
140
|
+
): PlacementTypingKeyOutcome | null {
|
|
141
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return null
|
|
142
|
+
const typing = usePlacementTyping.getState()
|
|
143
|
+
const { key } = event
|
|
144
|
+
if (!typing.isActive) {
|
|
145
|
+
if (isPlacementTypingStartKey(key)) {
|
|
146
|
+
if (!options.begin?.(0)) return null
|
|
147
|
+
usePlacementTyping.getState().append(key)
|
|
148
|
+
return 'edit'
|
|
149
|
+
}
|
|
150
|
+
if (key === 'Tab' && options.tabBegins && options.begin?.(1)) return 'edit'
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
if (key === 'Tab' || key === ',') {
|
|
154
|
+
typing.toggleField()
|
|
155
|
+
return 'edit'
|
|
156
|
+
}
|
|
157
|
+
if (key === 'Enter') return 'commit'
|
|
158
|
+
if (key === 'Escape') {
|
|
159
|
+
typing.clear()
|
|
160
|
+
return 'clear'
|
|
161
|
+
}
|
|
162
|
+
if (key === 'Backspace') {
|
|
163
|
+
typing.backspace()
|
|
164
|
+
return 'edit'
|
|
165
|
+
}
|
|
166
|
+
if (key === 'Delete') {
|
|
167
|
+
typing.setField(typing.activeField, '')
|
|
168
|
+
return 'edit'
|
|
169
|
+
}
|
|
170
|
+
if (isPlacementTypingKey(key)) {
|
|
171
|
+
typing.append(key)
|
|
172
|
+
return 'edit'
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Parse options for a typed length: a bare number is read in the display unit. */
|
|
178
|
+
export function placementTypingLengthOptions(
|
|
179
|
+
unit: 'metric' | 'imperial',
|
|
180
|
+
metricNotation: 'meters' | 'millimeters',
|
|
181
|
+
) {
|
|
182
|
+
return {
|
|
183
|
+
bareUnit: unit === 'imperial' ? 'in' : metricNotation === 'millimeters' ? 'mm' : 'm',
|
|
184
|
+
system: unit === 'imperial' ? ('imperial' as const) : ('metric' as const),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
2
|
+
import { isTypingTarget, useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { create } from 'zustand'
|
|
4
4
|
import { hasLiveGestures } from '../lib/gesture-lifecycle'
|
|
5
5
|
import { removeWallRegion } from '../lib/paint-regions'
|
|
6
6
|
import { sfxEmitter } from '../lib/sfx-bus'
|
|
7
|
-
import { isTypingTarget } from '../lib/typing-target'
|
|
8
7
|
import useDeleteConfirmation from './use-delete-confirmation'
|
|
9
8
|
import useInteractionScope from './use-interaction-scope'
|
|
10
9
|
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { Polygon } from '@pascal-app/core'
|
|
2
|
-
import {
|
|
3
|
-
type PillCandidate,
|
|
4
|
-
placePills,
|
|
5
|
-
type RoomHit,
|
|
6
|
-
type RoomLabelEntry,
|
|
7
|
-
roomAt,
|
|
8
|
-
} from '../room-labels'
|
|
9
|
-
|
|
10
|
-
// One room-label placement pass and one pointer pick, for a 900 × 600 px house of 31, 60 and 120
|
|
11
|
-
// rooms (every third an L), printed as JSON medians in ms. Run with `bun run <this file>`.
|
|
12
|
-
let seed = 11
|
|
13
|
-
const random = () => {
|
|
14
|
-
seed = (seed * 1_103_515_245 + 12_345) % 2 ** 31
|
|
15
|
-
return seed / 2 ** 31
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function house(count: number) {
|
|
19
|
-
const columns = Math.ceil(Math.sqrt(count * 1.5))
|
|
20
|
-
const rows = Math.ceil(count / columns)
|
|
21
|
-
const width = 900 / columns
|
|
22
|
-
const depth = 600 / rows
|
|
23
|
-
return Array.from({ length: count }, (_, i) => {
|
|
24
|
-
const left = 270 + (i % columns) * width
|
|
25
|
-
const top = 150 + Math.floor(i / columns) * depth
|
|
26
|
-
const right = left + width
|
|
27
|
-
const bottom = top + depth
|
|
28
|
-
const outer: [number, number][] =
|
|
29
|
-
i % 3 === 0
|
|
30
|
-
? [
|
|
31
|
-
[left, top],
|
|
32
|
-
[right, top],
|
|
33
|
-
[right, top + depth / 2],
|
|
34
|
-
[left + width / 2, top + depth / 2],
|
|
35
|
-
[left + width / 2, bottom],
|
|
36
|
-
[left, bottom],
|
|
37
|
-
]
|
|
38
|
-
: [
|
|
39
|
-
[left, top],
|
|
40
|
-
[right, top],
|
|
41
|
-
[right, bottom],
|
|
42
|
-
[left, bottom],
|
|
43
|
-
]
|
|
44
|
-
const outline: Polygon = { outer, holes: [] }
|
|
45
|
-
const pill = 60 + random() * 110
|
|
46
|
-
const candidate: PillCandidate = {
|
|
47
|
-
id: `room_${i}`,
|
|
48
|
-
x: left + width / 4,
|
|
49
|
-
y: top + depth / 4,
|
|
50
|
-
size: [pill, 24, pill * 0.6],
|
|
51
|
-
area: width * depth * (i % 3 === 0 ? 0.75 : 1),
|
|
52
|
-
outline,
|
|
53
|
-
}
|
|
54
|
-
const entry: RoomLabelEntry = {
|
|
55
|
-
zoneId: candidate.id,
|
|
56
|
-
polygon: outer,
|
|
57
|
-
holes: [],
|
|
58
|
-
area: candidate.area,
|
|
59
|
-
}
|
|
60
|
-
return { candidate, entry }
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const median = (samples: number[]) => samples.sort((a, b) => a - b)[samples.length >> 1]!
|
|
65
|
-
|
|
66
|
-
const pane = { width: 1440, height: 900 }
|
|
67
|
-
const results: Record<string, unknown> = {}
|
|
68
|
-
for (const count of [31, 60, 120]) {
|
|
69
|
-
const rooms = house(count)
|
|
70
|
-
const candidates = rooms.map((room) => room.candidate)
|
|
71
|
-
const entries = rooms.map((room) => room.entry)
|
|
72
|
-
for (let i = 0; i < 20; i++) placePills(candidates, pane)
|
|
73
|
-
const cold: number[] = []
|
|
74
|
-
const warm: number[] = []
|
|
75
|
-
let previous = placePills(candidates, pane)
|
|
76
|
-
for (let i = 0; i < 100; i++) {
|
|
77
|
-
let started = performance.now()
|
|
78
|
-
placePills(candidates, pane)
|
|
79
|
-
cold.push(performance.now() - started)
|
|
80
|
-
started = performance.now()
|
|
81
|
-
previous = placePills(candidates, pane, previous)
|
|
82
|
-
warm.push(performance.now() - started)
|
|
83
|
-
}
|
|
84
|
-
const points = Array.from(
|
|
85
|
-
{ length: 1000 },
|
|
86
|
-
(): RoomHit => [270 + random() * 900, 150 + random() * 600, 0],
|
|
87
|
-
)
|
|
88
|
-
for (const point of points) roomAt(entries, () => point)
|
|
89
|
-
const picks: number[] = []
|
|
90
|
-
for (let i = 0; i < 20; i++) {
|
|
91
|
-
const started = performance.now()
|
|
92
|
-
for (const point of points) roomAt(entries, () => point)
|
|
93
|
-
picks.push((performance.now() - started) / points.length)
|
|
94
|
-
}
|
|
95
|
-
const tiers = Object.fromEntries(
|
|
96
|
-
['full', 'name', 'dot', 'hidden'].map((tier) => [
|
|
97
|
-
tier,
|
|
98
|
-
[...previous.values()].filter((placement) => placement.tier === tier).length,
|
|
99
|
-
]),
|
|
100
|
-
)
|
|
101
|
-
results[count] = {
|
|
102
|
-
placeMs: median(cold),
|
|
103
|
-
placeWithPreviousMs: median(warm),
|
|
104
|
-
pickMs: median(picks),
|
|
105
|
-
tiers,
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
console.log(JSON.stringify(results, null, 2))
|