@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
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
useLiveTransforms,
|
|
39
39
|
useScene,
|
|
40
40
|
} from '@pascal-app/core'
|
|
41
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
41
|
+
import { isTypingTarget, useViewer } from '@pascal-app/viewer'
|
|
42
42
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
43
43
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
44
44
|
import { type Group, Vector3 } from 'three'
|
|
@@ -61,7 +61,6 @@ import {
|
|
|
61
61
|
surfaceAttachmentUpdates,
|
|
62
62
|
updateSurfaceNode,
|
|
63
63
|
} from '../../../lib/surface-attachment'
|
|
64
|
-
import { isTypingTarget } from '../../../lib/typing-target'
|
|
65
64
|
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
66
65
|
import useEditor, {
|
|
67
66
|
getActiveSnappingMode,
|
|
@@ -484,6 +483,18 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
484
483
|
return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
|
|
485
484
|
}
|
|
486
485
|
|
|
486
|
+
const resolvePosition = (position: [number, number, number]) => {
|
|
487
|
+
const nodes = useScene.getState().nodes
|
|
488
|
+
const current = nodes[node.id] ?? node
|
|
489
|
+
return (
|
|
490
|
+
nodeRegistry.get(current.type)?.capabilities?.movable?.resolvePosition?.({
|
|
491
|
+
node: current,
|
|
492
|
+
position,
|
|
493
|
+
nodes,
|
|
494
|
+
}) ?? position
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
487
498
|
const keepStartingElevation = (position: [number, number, number], yaw: number) => {
|
|
488
499
|
if (!preserveLevelAndElevation) return position
|
|
489
500
|
const rotation = toCommitRotation(yaw)
|
|
@@ -534,14 +545,19 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
534
545
|
// them through `useLiveNodeOverrides` (transient — no history churn;
|
|
535
546
|
// GeometrySystem merges overrides via getEffectiveNode). Each connected
|
|
536
547
|
// node is re-dirtied so its geometry rebuilds against the new override.
|
|
537
|
-
|
|
548
|
+
// False, previewing nothing, when the connected joints cannot follow.
|
|
549
|
+
const previewConnectivity = (
|
|
550
|
+
position: [number, number, number],
|
|
551
|
+
rotationY: number,
|
|
552
|
+
): boolean => {
|
|
538
553
|
const connectivity = connectivityRef.current
|
|
539
|
-
if (!connectivity) return
|
|
554
|
+
if (!connectivity) return true
|
|
540
555
|
const updates = resolveConnectivityUpdates(
|
|
541
556
|
connectivity,
|
|
542
557
|
buildPreviewNode(position, rotationY),
|
|
543
558
|
)
|
|
544
|
-
if (updates
|
|
559
|
+
if (!updates) return false
|
|
560
|
+
if (updates.length === 0) return true
|
|
545
561
|
useLiveNodeOverrides
|
|
546
562
|
.getState()
|
|
547
563
|
.setMany(updates.map((u) => [u.id, u.data as Record<string, unknown>] as const))
|
|
@@ -549,6 +565,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
549
565
|
for (const u of updates) {
|
|
550
566
|
if (useScene.getState().nodes[u.id]) useScene.getState().markDirty(u.id)
|
|
551
567
|
}
|
|
568
|
+
return true
|
|
552
569
|
}
|
|
553
570
|
|
|
554
571
|
const clearConnectivityOverrides = () => {
|
|
@@ -1026,7 +1043,11 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1026
1043
|
},
|
|
1027
1044
|
).position
|
|
1028
1045
|
}
|
|
1029
|
-
position = keepStartingElevation(position, rotationRef.current)
|
|
1046
|
+
position = resolvePosition(keepStartingElevation(position, rotationRef.current))
|
|
1047
|
+
// Carry connected ductwork along (preview only — committed on drop). A
|
|
1048
|
+
// position the connected joints cannot follow holds the last frame,
|
|
1049
|
+
// which is what the drop commits.
|
|
1050
|
+
if (!previewConnectivity(position, rotationRef.current)) return
|
|
1030
1051
|
const visualPosition = getVisualPosition(position)
|
|
1031
1052
|
hasMovedRef.current = true
|
|
1032
1053
|
setCursorPosition(visualPosition)
|
|
@@ -1054,8 +1075,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1054
1075
|
})
|
|
1055
1076
|
syncParentFramePreview(position)
|
|
1056
1077
|
markMovedNodeDirty()
|
|
1057
|
-
// Carry connected ductwork along (preview only — committed on drop).
|
|
1058
|
-
previewConnectivity(position, rotationRef.current)
|
|
1059
1078
|
|
|
1060
1079
|
const soundStep = {
|
|
1061
1080
|
coords: [x, z],
|
|
@@ -1116,7 +1135,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1116
1135
|
// tool — unless Alt (free place) is held to force placement. Other kinds
|
|
1117
1136
|
// carry no validity box (`validRef` stays true), so they're never blocked.
|
|
1118
1137
|
if (!validRef.current && !altRef.current) return
|
|
1119
|
-
const position
|
|
1138
|
+
const position = resolvePosition([...lastCursorRef.current])
|
|
1120
1139
|
|
|
1121
1140
|
const rotation = toCommitRotation(rotationRef.current)
|
|
1122
1141
|
let committedId = node.id as AnyNodeId
|
|
@@ -1171,8 +1190,9 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1171
1190
|
? resolveConnectivityUpdates(
|
|
1172
1191
|
connectivityRef.current,
|
|
1173
1192
|
buildPreviewNode(position, rotationRef.current),
|
|
1174
|
-
)
|
|
1193
|
+
)?.filter((u) => useScene.getState().nodes[u.id])
|
|
1175
1194
|
: []
|
|
1195
|
+
if (!connectivityUpdates) return
|
|
1176
1196
|
const surfaceId = surfaceAttachmentId(effectiveNode)
|
|
1177
1197
|
itemSurfaceMove?.restore()
|
|
1178
1198
|
useScene.temporal.getState().resume()
|
|
@@ -1298,7 +1318,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1298
1318
|
attachmentRotationRef.current,
|
|
1299
1319
|
)
|
|
1300
1320
|
if (nextFreeRotation === null) return
|
|
1301
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
1302
1321
|
const hostedPose = itemSurfaceMove?.rotate(nextFreeRotation)
|
|
1303
1322
|
let position = hostedPose?.position ?? lastCursorRef.current
|
|
1304
1323
|
if (
|
|
@@ -1319,10 +1338,16 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1319
1338
|
)
|
|
1320
1339
|
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1321
1340
|
}
|
|
1322
|
-
|
|
1341
|
+
const rotationY = hostedPose?.rotationY ?? nextFreeRotation
|
|
1342
|
+
position = keepStartingElevation(position, rotationY)
|
|
1343
|
+
// Rotating the fitting swings its collars — connected ducts follow. A
|
|
1344
|
+
// swing they cannot follow holds the last frame; a surface-hosted node
|
|
1345
|
+
// has already turned with its host, so its drop refuses instead.
|
|
1346
|
+
if (!previewConnectivity(position, rotationY) && !hostedPose) return
|
|
1347
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
1323
1348
|
lastCursorRef.current = position
|
|
1324
|
-
freeRotationRef.current =
|
|
1325
|
-
rotationRef.current =
|
|
1349
|
+
freeRotationRef.current = rotationY
|
|
1350
|
+
rotationRef.current = rotationY
|
|
1326
1351
|
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
1327
1352
|
const visualPosition = getVisualPosition(position)
|
|
1328
1353
|
setCursorPosition(visualPosition)
|
|
@@ -1335,8 +1360,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1335
1360
|
})
|
|
1336
1361
|
syncParentFramePreview(position)
|
|
1337
1362
|
markMovedNodeDirty()
|
|
1338
|
-
// Rotating the fitting swings its collars — connected ducts follow.
|
|
1339
|
-
previewConnectivity(position, rotationRef.current)
|
|
1340
1363
|
// Rotation changes the footprint's collision span — re-check validity.
|
|
1341
1364
|
if (!itemSurfaceMove?.hosted && itemSurfaceMove?.rejection && lastSurfaceEvent)
|
|
1342
1365
|
onItemMove(lastSurfaceEvent)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
|
|
2
|
+
import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
|
|
2
3
|
import type { ThreeElements } from '@react-three/fiber'
|
|
3
4
|
import { forwardRef } from 'react'
|
|
4
5
|
import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
|
|
@@ -8,7 +9,6 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
8
9
|
import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
|
|
9
10
|
import useEditor from '../../../store/use-editor'
|
|
10
11
|
import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
|
|
11
|
-
import { Html } from '../../editor/localized-html'
|
|
12
12
|
|
|
13
13
|
interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
14
14
|
color?: string
|
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import '../../../three-types'
|
|
4
4
|
|
|
5
|
+
import { formatLinearMeasurement, type LinearUnit, type MetricNotation } from '@pascal-app/core'
|
|
6
|
+
import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
|
|
5
7
|
import { useEffect, useMemo } from 'react'
|
|
6
8
|
import { PlaneGeometry } from 'three'
|
|
7
9
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
8
10
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
9
11
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
10
|
-
import {
|
|
11
|
-
formatLinearMeasurement,
|
|
12
|
-
type LinearUnit,
|
|
13
|
-
type MetricNotation,
|
|
14
|
-
} from '../../../lib/measurements'
|
|
15
|
-
import { Html } from '../../editor/localized-html'
|
|
16
12
|
import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
|
|
17
13
|
|
|
18
14
|
const VALID_COLOR = 0x22_c5_5e // green-500
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
4
|
+
import { type PlacementTypingField, usePlacementTyping } from '../../../store/use-placement-typing'
|
|
5
|
+
|
|
6
|
+
export type PlacementCoordinateInputProps = {
|
|
7
|
+
/** What each empty field stands for; defaults to the session's `fieldDefaults`. */
|
|
8
|
+
placeholders?: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The typed-entry HUD, one box per field of the session. It only displays the
|
|
13
|
+
* session: keys reach the active tool's own handler (`routePlacementTypingKey`),
|
|
14
|
+
* so the same HUD can sit in the 3D view and the plan at once without
|
|
15
|
+
* competing for focus. Clicking a field makes it the active one.
|
|
16
|
+
*/
|
|
17
|
+
export function PlacementCoordinateInput({ placeholders }: PlacementCoordinateInputProps) {
|
|
18
|
+
const i18n = usePascalI18n()
|
|
19
|
+
const isActive = usePlacementTyping((state) => state.isActive)
|
|
20
|
+
const labels = usePlacementTyping((state) => state.labels)
|
|
21
|
+
const fields = usePlacementTyping((state) => state.fields)
|
|
22
|
+
const fieldDefaults = usePlacementTyping((state) => state.fieldDefaults)
|
|
23
|
+
const activeField = usePlacementTyping((state) => state.activeField)
|
|
24
|
+
|
|
25
|
+
if (!isActive) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-placement-coordinate-entry
|
|
30
|
+
onClick={(event) => event.stopPropagation()}
|
|
31
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
32
|
+
style={{
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
background: 'rgba(15, 23, 42, 0.94)',
|
|
35
|
+
border: '1px solid rgba(148, 163, 184, 0.55)',
|
|
36
|
+
borderRadius: 6,
|
|
37
|
+
boxShadow: '0 4px 14px rgba(15, 23, 42, 0.28)',
|
|
38
|
+
color: '#f8fafc',
|
|
39
|
+
display: 'flex',
|
|
40
|
+
gap: 6,
|
|
41
|
+
padding: '5px 7px',
|
|
42
|
+
pointerEvents: 'auto',
|
|
43
|
+
userSelect: 'none',
|
|
44
|
+
whiteSpace: 'nowrap',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{labels.map((descriptor, index) => {
|
|
48
|
+
const field = index as PlacementTypingField
|
|
49
|
+
const label = i18n._(descriptor)
|
|
50
|
+
const active = field === activeField
|
|
51
|
+
const value = fields[field]
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-placement-coordinate-field={descriptor.id}
|
|
55
|
+
key={descriptor.id}
|
|
56
|
+
onPointerDown={(event) => {
|
|
57
|
+
event.stopPropagation()
|
|
58
|
+
usePlacementTyping.getState().setActiveField(field)
|
|
59
|
+
}}
|
|
60
|
+
style={{
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
display: 'flex',
|
|
63
|
+
flexDirection: 'column',
|
|
64
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
65
|
+
fontSize: 9,
|
|
66
|
+
gap: 2,
|
|
67
|
+
textTransform: 'uppercase',
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<span style={{ color: active ? '#a5b4fc' : '#94a3b8' }}>{label}</span>
|
|
71
|
+
<span
|
|
72
|
+
aria-label={label}
|
|
73
|
+
role="status"
|
|
74
|
+
style={{
|
|
75
|
+
background: active ? '#eef2ff' : '#e2e8f0',
|
|
76
|
+
borderRadius: 3,
|
|
77
|
+
color: value ? '#0f172a' : '#64748b',
|
|
78
|
+
fontSize: 12,
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
minWidth: 76,
|
|
81
|
+
outline: active ? '2px solid #818cf8' : 'none',
|
|
82
|
+
padding: '3px 5px',
|
|
83
|
+
textAlign: 'right',
|
|
84
|
+
textTransform: 'none',
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{value || placeholders?.[field] || fieldDefaults[field] || '\u00a0'}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
})}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
|
|
4
5
|
import { useLayoutEffect, useMemo } from 'react'
|
|
5
6
|
import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
|
|
6
7
|
import { LineBasicNodeMaterial } from 'three/webgpu'
|
|
7
8
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
8
9
|
import type { PlacementPreviewDimension } from '../../../store/use-placement-preview'
|
|
9
10
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
10
|
-
import { Html } from '../../editor/localized-html'
|
|
11
11
|
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
12
12
|
|
|
13
13
|
const DIMENSION_COLOR = 0x63_66_f1
|
|
@@ -83,10 +83,6 @@ function getCurveTangentAtPoint(point: PlanPoint, segment: SegmentAngleLike): Pl
|
|
|
83
83
|
return best.tangent
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function formatAngleRadians(angle: number) {
|
|
87
|
-
return `${Math.round((angle * 180) / Math.PI)}°`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
86
|
export function getAngleBetweenVectors(first: PlanPoint, second: PlanPoint): number | null {
|
|
91
87
|
const firstLength = Math.hypot(first[0], first[1])
|
|
92
88
|
const secondLength = Math.hypot(second[0], second[1])
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useLiveTerrain,
|
|
11
11
|
} from '@pascal-app/core'
|
|
12
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
|
|
13
14
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
14
15
|
import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
|
15
16
|
import {
|
|
@@ -24,7 +25,6 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
24
25
|
import { sculptFieldForSite, terrainPointInsideSite } from '../../../lib/terrain-sculpt'
|
|
25
26
|
import { brushRingColor } from '../../../lib/terrain-verb-color'
|
|
26
27
|
import useEditor from '../../../store/use-editor'
|
|
27
|
-
import { Html } from '../../editor/localized-html'
|
|
28
28
|
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
29
29
|
|
|
30
30
|
/** Segments in the cursor ring. 64 reads as a circle at any usable brush size. */
|
|
@@ -41,7 +41,6 @@ import { FloorRegionControls3D } from '../editor/floor-region-controls'
|
|
|
41
41
|
import { OpenWallEnds3DLayer } from '../editor/open-wall-ends-3d-layer'
|
|
42
42
|
import { OpeningGuides3DLayer } from '../editor/opening-guides-3d-layer'
|
|
43
43
|
import { WallSnapBeaconLayer } from '../editor/wall-snap-beacon-layer'
|
|
44
|
-
import { ElevatorTool } from './elevator/elevator-tool'
|
|
45
44
|
import { MoveTool } from './item/move-tool'
|
|
46
45
|
import { WallPaintRegionTool } from './paint-region/wall-paint-region-tool'
|
|
47
46
|
import { RegistryToolProvider } from './registry-tool-context'
|
|
@@ -305,21 +304,6 @@ export const ToolManager: React.FC = () => {
|
|
|
305
304
|
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
|
306
305
|
setSelection({ selectedIds: [nodeId] })
|
|
307
306
|
}
|
|
308
|
-
const handlePlacedElevatorSelected = (
|
|
309
|
-
nodeId: AnyNodeId,
|
|
310
|
-
elevatorBuildingId: BuildingNode['id'],
|
|
311
|
-
) => {
|
|
312
|
-
// Preserve the active level. `setSelection`'s hierarchy guard nulls
|
|
313
|
-
// `levelId` whenever `buildingId` is passed without an explicit
|
|
314
|
-
// `levelId` — which deselected the current floor plan the moment an
|
|
315
|
-
// elevator was placed. Pass the current level through so the floor
|
|
316
|
-
// plan stays selected.
|
|
317
|
-
setSelection({
|
|
318
|
-
buildingId: elevatorBuildingId,
|
|
319
|
-
levelId: activeLevelId ?? null,
|
|
320
|
-
selectedIds: [nodeId],
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
307
|
|
|
324
308
|
return (
|
|
325
309
|
<RegistryToolProvider value={registryToolContext}>
|
|
@@ -463,14 +447,7 @@ export const ToolManager: React.FC = () => {
|
|
|
463
447
|
<RegistryToolComponent />
|
|
464
448
|
</Suspense>
|
|
465
449
|
)}
|
|
466
|
-
{!movingNode &&
|
|
467
|
-
<ElevatorTool
|
|
468
|
-
buildingId={buildingId as BuildingNode['id'] | null}
|
|
469
|
-
levelId={activeLevelId ?? null}
|
|
470
|
-
onPlaced={handlePlacedElevatorSelected}
|
|
471
|
-
/>
|
|
472
|
-
)}
|
|
473
|
-
{!movingNode && BuildToolComponent && tool !== 'elevator' ? <BuildToolComponent /> : null}
|
|
450
|
+
{!movingNode && BuildToolComponent ? <BuildToolComponent /> : null}
|
|
474
451
|
{/* Figma-style alignment guides published by the move / placement
|
|
475
452
|
tools above. Lives inside the building-local group so the
|
|
476
453
|
building-local guide coords render at the right world position. */}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
formatWallDraftHeading,
|
|
4
|
+
resolveTypedWallEnd,
|
|
5
|
+
wallDraftHeadingDegrees,
|
|
6
|
+
} from './wall-typed-entry'
|
|
7
|
+
|
|
8
|
+
const metric = { bareUnit: 'm', system: 'metric' } as const
|
|
9
|
+
|
|
10
|
+
function expectPoint(actual: [number, number] | null, expected: [number, number]) {
|
|
11
|
+
expect(actual).not.toBeNull()
|
|
12
|
+
expect(actual![0]).toBeCloseTo(expected[0], 10)
|
|
13
|
+
expect(actual![1]).toBeCloseTo(expected[1], 10)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('resolveTypedWallEnd — length only', () => {
|
|
17
|
+
test('keeps the pointer direction and replaces the distance', () => {
|
|
18
|
+
expectPoint(resolveTypedWallEnd([0, 0], [3, 4], ['1', ''], metric), [0.6, 0.8])
|
|
19
|
+
expectPoint(resolveTypedWallEnd([0, 0], [-30, -40], ['5', ''], metric), [-3, -4])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('reads the whole field, not the length the pointer last showed', () => {
|
|
23
|
+
expectPoint(resolveTypedWallEnd([0, 0], [1, 0], ['12', ''], metric), [12, 0])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('honours unit suffixes and the bare unit', () => {
|
|
27
|
+
expectPoint(resolveTypedWallEnd([1, 1], [11, 1], ['250cm', ''], metric), [3.5, 1])
|
|
28
|
+
expectPoint(
|
|
29
|
+
resolveTypedWallEnd([0, 0], [0, 9], ['2500', ''], { bareUnit: 'mm', system: 'metric' }),
|
|
30
|
+
[0, 2.5],
|
|
31
|
+
)
|
|
32
|
+
expectPoint(
|
|
33
|
+
resolveTypedWallEnd([0, 0], [9, 0], ["10'", ''], { bareUnit: 'in', system: 'imperial' }),
|
|
34
|
+
[3.048, 0],
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('has no endpoint without a usable length or a direction', () => {
|
|
39
|
+
expect(resolveTypedWallEnd([0, 0], [1, 0], ['', ''], metric)).toBeNull()
|
|
40
|
+
expect(resolveTypedWallEnd([0, 0], [1, 0], ['abc', ''], metric)).toBeNull()
|
|
41
|
+
expect(resolveTypedWallEnd([0, 0], [1, 0], ['0', ''], metric)).toBeNull()
|
|
42
|
+
expect(resolveTypedWallEnd([2, 2], [2, 2], ['5', ''], metric)).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('resolveTypedWallEnd — angle', () => {
|
|
47
|
+
test('angle only locks the heading; the pointer projection sets the length', () => {
|
|
48
|
+
// 90° runs along +Z; a pointer at (1, 3) projects to 3 m along it.
|
|
49
|
+
expectPoint(resolveTypedWallEnd([0, 0], [1, 3], ['', '90'], metric), [0, 3])
|
|
50
|
+
// (3, 4) projects to 7/√2 along 45°, landing 3.5 out on each axis.
|
|
51
|
+
expectPoint(resolveTypedWallEnd([1, 1], [4, 5], ['', '45'], metric), [4.5, 4.5])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('angle only quantises the projected length to the step', () => {
|
|
55
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0.2, 2.37], ['', '90'], metric, 0.5), [0, 2.5])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('angle only never flips the wall behind its start', () => {
|
|
59
|
+
expectPoint(resolveTypedWallEnd([0, 0], [-4, 0.1], ['', '0'], metric), [0, 0])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('length and angle fix the endpoint whatever the pointer does', () => {
|
|
63
|
+
expectPoint(resolveTypedWallEnd([2, 1], [-50, 80], ['4', '180'], metric), [-2, 1])
|
|
64
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['2', '270'], metric), [0, -2])
|
|
65
|
+
expectPoint(resolveTypedWallEnd([0, 0], [5, 5], ['1', '-90'], metric), [0, -1])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('accepts degree and radian suffixes', () => {
|
|
69
|
+
expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', '90°'], metric), [0, 2])
|
|
70
|
+
expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', `${Math.PI}rad`], metric), [-2, 0])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('an unreadable angle leaves the direction to the pointer', () => {
|
|
74
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0, 4], ['2', 'x'], metric), [0, 2])
|
|
75
|
+
expect(resolveTypedWallEnd([0, 0], [0, 4], ['', 'x'], metric)).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('wallDraftHeadingDegrees', () => {
|
|
80
|
+
test('measures from +X towards +Z in [0, 360)', () => {
|
|
81
|
+
expect(wallDraftHeadingDegrees([0, 0], [1, 0])).toBeCloseTo(0, 10)
|
|
82
|
+
expect(wallDraftHeadingDegrees([0, 0], [0, 1])).toBeCloseTo(90, 10)
|
|
83
|
+
expect(wallDraftHeadingDegrees([0, 0], [-1, 0])).toBeCloseTo(180, 10)
|
|
84
|
+
expect(wallDraftHeadingDegrees([0, 0], [0, -1])).toBeCloseTo(270, 10)
|
|
85
|
+
expect(wallDraftHeadingDegrees([1, 1], [1, 1])).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('round-trips through a typed angle', () => {
|
|
89
|
+
const heading = wallDraftHeadingDegrees([0, 0], [-3, -4])!
|
|
90
|
+
expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['5', String(heading)], metric), [-3, -4])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('labels the live heading in the same convention, wrapping 360° to 0°', () => {
|
|
94
|
+
expect(formatWallDraftHeading([0, 0], [0, 1])).toBe('90°')
|
|
95
|
+
expect(formatWallDraftHeading([0, 0], [1, -1])).toBe('315°')
|
|
96
|
+
expect(formatWallDraftHeading([0, 0], [1, -0.001])).toBe('0°')
|
|
97
|
+
expect(formatWallDraftHeading([0, 0], [0, 0])).toBe('')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { formatAngleRadians } from '@pascal-app/core'
|
|
2
|
+
import { type ParseMeasurementOptions, parseMeasurement } from '../../../lib/measurement-parser'
|
|
3
|
+
import type { PlacementTypingValues } from '../../../store/use-placement-typing'
|
|
4
|
+
import type { WallPlanPoint } from './wall-drafting'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Heading of a wall draft in degrees, the way the 15° angle snap measures it:
|
|
8
|
+
* from the building's +X axis towards +Z (`atan2(dz, dx)`), in [0, 360).
|
|
9
|
+
* On an unrotated plan +Z points down the screen, so 90 runs straight down.
|
|
10
|
+
*/
|
|
11
|
+
export function wallDraftHeadingDegrees(start: WallPlanPoint, end: WallPlanPoint): number | null {
|
|
12
|
+
const dx = end[0] - start[0]
|
|
13
|
+
const dz = end[1] - start[1]
|
|
14
|
+
if (Math.hypot(dx, dz) <= 1e-6) return null
|
|
15
|
+
const degrees = (Math.atan2(dz, dx) * 180) / Math.PI
|
|
16
|
+
return ((degrees % 360) + 360) % 360
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The draft's live heading as the angle field's placeholder, e.g. `90°`. */
|
|
20
|
+
export function formatWallDraftHeading(start: WallPlanPoint, end: WallPlanPoint): string {
|
|
21
|
+
const degrees = wallDraftHeadingDegrees(start, end)
|
|
22
|
+
if (degrees === null) return ''
|
|
23
|
+
return formatAngleRadians(((Math.round(degrees) % 360) * Math.PI) / 180)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The endpoint a typed wall draft commits to. Field 0 is the length, field 1
|
|
28
|
+
* the heading (see `wallDraftHeadingDegrees`); an empty field is left to the
|
|
29
|
+
* pointer:
|
|
30
|
+
* - length only: the pointer steers the direction, the length is exact;
|
|
31
|
+
* - angle only: the direction is locked, the pointer's projection onto it sets
|
|
32
|
+
* the length (quantised to `lengthStep` when one is given);
|
|
33
|
+
* - both: the endpoint is fully determined and the pointer is ignored.
|
|
34
|
+
*
|
|
35
|
+
* Returns null when no field holds a usable value or the pointer gives no
|
|
36
|
+
* direction to follow, so the caller keeps the pointer's own endpoint.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveTypedWallEnd(
|
|
39
|
+
start: WallPlanPoint,
|
|
40
|
+
pointer: WallPlanPoint,
|
|
41
|
+
fields: Readonly<PlacementTypingValues>,
|
|
42
|
+
lengthOptions: ParseMeasurementOptions,
|
|
43
|
+
lengthStep = 0,
|
|
44
|
+
): WallPlanPoint | null {
|
|
45
|
+
const typedLength = fields[0].trim()
|
|
46
|
+
? parseMeasurement(fields[0], { kind: 'length', unitId: 'm' }, lengthOptions)
|
|
47
|
+
: null
|
|
48
|
+
const length = typedLength !== null && typedLength > 0 ? typedLength : null
|
|
49
|
+
const typedAngle = fields[1].trim()
|
|
50
|
+
? parseMeasurement(fields[1], { kind: 'angle', unitId: 'rad' }, { bareUnit: 'deg' })
|
|
51
|
+
: null
|
|
52
|
+
|
|
53
|
+
const dx = pointer[0] - start[0]
|
|
54
|
+
const dz = pointer[1] - start[1]
|
|
55
|
+
if (typedAngle === null) {
|
|
56
|
+
if (length === null) return null
|
|
57
|
+
const pointerLength = Math.hypot(dx, dz)
|
|
58
|
+
if (pointerLength <= 1e-6) return null
|
|
59
|
+
return [start[0] + (dx / pointerLength) * length, start[1] + (dz / pointerLength) * length]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dirX = Math.cos(typedAngle)
|
|
63
|
+
const dirZ = Math.sin(typedAngle)
|
|
64
|
+
let distance = length
|
|
65
|
+
if (distance === null) {
|
|
66
|
+
const projected = Math.max(0, dx * dirX + dz * dirZ)
|
|
67
|
+
distance = lengthStep > 0 ? Math.round(projected / lengthStep) * lengthStep : projected
|
|
68
|
+
}
|
|
69
|
+
return [start[0] + dirX * distance, start[1] + dirZ * distance]
|
|
70
|
+
}
|
|
@@ -4,8 +4,8 @@ import { emitter, usePascalI18n } from '@pascal-app/core'
|
|
|
4
4
|
import Image from 'next/image'
|
|
5
5
|
import useEditor from '../../../store/use-editor'
|
|
6
6
|
import { visibleScene } from '../../../store/view-layout'
|
|
7
|
-
import { ActionButton } from '
|
|
8
|
-
import { FollowPascalToggle } from '
|
|
7
|
+
import { ActionButton } from '@pascal-app/viewer/ui'
|
|
8
|
+
import { FollowPascalToggle } from '@pascal-app/viewer/ui'
|
|
9
9
|
|
|
10
10
|
export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
11
11
|
const i18n = usePascalI18n()
|
|
@@ -9,7 +9,7 @@ import Image from 'next/image'
|
|
|
9
9
|
import { Fragment } from 'react'
|
|
10
10
|
import { cn } from './../../../lib/utils'
|
|
11
11
|
import useEditor from './../../../store/use-editor'
|
|
12
|
-
import { ActionButton } from '
|
|
12
|
+
import { ActionButton } from '@pascal-app/viewer/ui'
|
|
13
13
|
import { MeasurementControl } from './measurement-control'
|
|
14
14
|
import { PointAskButton } from './point-ask-button'
|
|
15
15
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { usePascalI18n } from '@pascal-app/core'
|
|
4
4
|
import { AnimatePresence, motion } from 'motion/react'
|
|
5
5
|
import { useReducedMotion } from '../../../hooks/use-reduced-motion'
|
|
6
|
-
import useFollowPascal from '
|
|
6
|
+
import { useFollowPascal } from '@pascal-app/viewer/ui'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Offered while a build plays and the person has the camera: one press and the camera goes back to
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { motion } from 'motion/react'
|
|
6
|
-
import { TooltipProvider } from '
|
|
7
|
-
import { useIsMobile } from '
|
|
6
|
+
import { TooltipProvider } from '@pascal-app/viewer/ui'
|
|
7
|
+
import { useIsMobile } from '@pascal-app/viewer/ui'
|
|
8
8
|
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
|
9
9
|
import { shouldShowEditingControls } from './../../../lib/interaction/overlay-policy'
|
|
10
10
|
import { cn } from './../../../lib/utils'
|
|
@@ -12,7 +12,7 @@ import useEditor from './../../../store/use-editor'
|
|
|
12
12
|
import { CameraActions } from './camera-actions'
|
|
13
13
|
import { ControlModes } from './control-modes'
|
|
14
14
|
import { FollowPascalPill } from './follow-pascal-pill'
|
|
15
|
-
import { type ActionMenuPlacement, ActionMenuPlacementProvider } from '
|
|
15
|
+
import { type ActionMenuPlacement, ActionMenuPlacementProvider } from '@pascal-app/viewer/ui'
|
|
16
16
|
import { SecondaryToggles } from './view-toggles'
|
|
17
17
|
|
|
18
18
|
// Mobile bottom offset matches the viewer's overlap behind the sheet's
|
|
@@ -27,9 +27,9 @@ import { cn } from '../../../lib/utils'
|
|
|
27
27
|
import useEditor from '../../../store/use-editor'
|
|
28
28
|
import { VIEW_2D } from '../../../store/view-layout'
|
|
29
29
|
import useFloorplanMode from '../../../store/use-floorplan-mode'
|
|
30
|
-
import { Popover, PopoverContent, PopoverTrigger } from '
|
|
31
|
-
import { ActionButton } from '
|
|
32
|
-
import { useActionMenuPopupSide } from '
|
|
30
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@pascal-app/viewer/ui'
|
|
31
|
+
import { ActionButton } from '@pascal-app/viewer/ui'
|
|
32
|
+
import { useActionMenuPopupSide } from '@pascal-app/viewer/ui'
|
|
33
33
|
|
|
34
34
|
const measurementOptions = [
|
|
35
35
|
{ kind: 'distance', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.distance', message: 'Distance' }, icon: Ruler },
|
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react'
|
|
2
|
-
|
|
3
|
-
/** Viewport edge the action menu docks to. */
|
|
4
|
-
export type ActionMenuPlacement = 'bottom' | 'top'
|
|
5
|
-
|
|
6
1
|
/** Where chrome under a top-docked menu starts: below the host toolbar row and the menu itself. */
|
|
7
2
|
export const TOP_MENU_CLEARANCE = '7.25rem'
|
|
8
|
-
|
|
9
|
-
const ActionMenuPlacementContext = createContext<ActionMenuPlacement>('bottom')
|
|
10
|
-
|
|
11
|
-
export const ActionMenuPlacementProvider = ActionMenuPlacementContext.Provider
|
|
12
|
-
|
|
13
|
-
export function useActionMenuPlacement(): ActionMenuPlacement {
|
|
14
|
-
return useContext(ActionMenuPlacementContext)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Side tooltips and popovers open toward, away from the docked edge. */
|
|
18
|
-
export function useActionMenuPopupSide(): 'top' | 'bottom' {
|
|
19
|
-
return useActionMenuPlacement() === 'top' ? 'bottom' : 'top'
|
|
20
|
-
}
|
|
@@ -6,7 +6,7 @@ import { cn } from '../../../lib/utils'
|
|
|
6
6
|
import useEditor from '../../../store/use-editor'
|
|
7
7
|
import { usePointAsk } from '../../../store/use-point-ask'
|
|
8
8
|
import { isViewVisible, VIEW_3D } from '../../../store/view-layout'
|
|
9
|
-
import { ActionButton } from '
|
|
9
|
+
import { ActionButton } from '@pascal-app/viewer/ui'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Point and ask (key C): the toolbar's way into the mode where hovering names what Pascal would
|