@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +663 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +790 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +284 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +230 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +187 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +392 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +122 -0
- package/src/lib/paint-pass-through.ts +78 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +265 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +1017 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +580 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +201 -0
- package/src/store/view-layout.ts +199 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
type BaseNode,
|
|
6
|
+
type BuildingNode,
|
|
7
|
+
type CameraPose,
|
|
8
|
+
emitter,
|
|
9
|
+
type LevelNode,
|
|
10
|
+
resolveBuildingForLevel,
|
|
11
|
+
useScene,
|
|
12
|
+
type ZoneNode,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import {
|
|
15
|
+
groundVeiled,
|
|
16
|
+
type RevealEvent,
|
|
17
|
+
revealChangedNodeIds,
|
|
18
|
+
subscribeRevealEvents,
|
|
19
|
+
useViewer,
|
|
20
|
+
} from '@pascal-app/viewer'
|
|
21
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
22
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
23
|
+
import { Vector3 } from 'three'
|
|
24
|
+
import {
|
|
25
|
+
clearFrame,
|
|
26
|
+
Follower,
|
|
27
|
+
type FollowWant,
|
|
28
|
+
frameOf,
|
|
29
|
+
OrbitSpring,
|
|
30
|
+
type OrbitValue,
|
|
31
|
+
} from '../../lib/follow-pascal'
|
|
32
|
+
import { anyExterior } from '../../lib/follow-pascal-exterior'
|
|
33
|
+
import { FollowPresentation, type PresentationPort } from '../../lib/follow-pascal-presentation'
|
|
34
|
+
import { FollowSight } from '../../lib/follow-pascal-sight'
|
|
35
|
+
import { stayOnLevel, wantFromEvent } from '../../lib/follow-pascal-targets'
|
|
36
|
+
import useEditor from '../../store/use-editor'
|
|
37
|
+
import useFollowPascal from '../../store/use-follow-pascal'
|
|
38
|
+
import { usePointAsk } from '../../store/use-point-ask'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Follow Pascal: while a build plays, the camera eases to what the current step is working on, framed
|
|
42
|
+
* at the distance its scale needs. It listens to the reveal's own words (`subscribeRevealEvents`), so
|
|
43
|
+
* a step and the camera share one rhythm, and moves like a critically damped spring on an orbit about
|
|
44
|
+
* what it looks at, so the horizon stays level. Touching the camera, or choosing a view, pauses it; the
|
|
45
|
+
* person resumes it from the pill, or the next build takes it again. Under reduced motion it cuts.
|
|
46
|
+
*
|
|
47
|
+
* To see a build it changes the presentation: it cuts the walls away when no eye sees the target,
|
|
48
|
+
* selects the floor being built and hides the floors above it. An edit or a move seen from outside
|
|
49
|
+
* (an exterior wall bent, a window moved on one, a roof, the ground) is looked at from outdoors with
|
|
50
|
+
* the walls up instead: a cutaway would hide the very wall that moves. A commit with one such change
|
|
51
|
+
* in it takes the outside view for all of it. A build is followed as it always was. When Follow ends (switched off, the person takes the
|
|
52
|
+
* camera, the build is over and no turn is running) it puts back what it changed, and leaves what
|
|
53
|
+
* the person changed since (`FollowPresentation`).
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/** A build is over this long after its last word; the pill goes with it. */
|
|
57
|
+
const BUILD_OVER_MS = 4000
|
|
58
|
+
/** Close enough to its goal that the flight is over. */
|
|
59
|
+
const ARRIVED = { position: 0.01, angle: 0.002 }
|
|
60
|
+
|
|
61
|
+
/** Every way a person takes the camera. */
|
|
62
|
+
const TAKEN = [
|
|
63
|
+
'camera-controls:interaction-start',
|
|
64
|
+
'camera-controls:view',
|
|
65
|
+
'camera-controls:focus',
|
|
66
|
+
'camera-controls:top-view',
|
|
67
|
+
'camera-controls:orbit-cw',
|
|
68
|
+
'camera-controls:orbit-ccw',
|
|
69
|
+
'camera-controls:fit-scene',
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
/** The viewer and the editor as `FollowPresentation` reads and writes them. */
|
|
73
|
+
const viewPort: PresentationPort = {
|
|
74
|
+
read: () => {
|
|
75
|
+
const viewer = useViewer.getState()
|
|
76
|
+
const { selection } = viewer
|
|
77
|
+
return {
|
|
78
|
+
wall: viewer.wallMode,
|
|
79
|
+
levels: viewer.levelMode,
|
|
80
|
+
hideAbove: viewer.hideLevelsAboveSelection,
|
|
81
|
+
floor: {
|
|
82
|
+
buildingId: selection.buildingId,
|
|
83
|
+
levelId: selection.levelId,
|
|
84
|
+
zoneId: selection.zoneId,
|
|
85
|
+
selectedIds: selection.selectedIds,
|
|
86
|
+
room: useEditor.getState().room,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
setWall: (mode) => useViewer.getState().setWallMode(mode),
|
|
91
|
+
setLevels: (mode) => useViewer.getState().setLevelMode(mode),
|
|
92
|
+
setHideAbove: (hide) => useViewer.setState({ hideLevelsAboveSelection: hide }),
|
|
93
|
+
setFloor: ({ buildingId, levelId }) => {
|
|
94
|
+
useViewer.getState().setSelection({
|
|
95
|
+
...(buildingId ? { buildingId: buildingId as BuildingNode['id'] } : {}),
|
|
96
|
+
levelId: levelId as LevelNode['id'],
|
|
97
|
+
})
|
|
98
|
+
useEditor.getState().clearRoom()
|
|
99
|
+
},
|
|
100
|
+
putFloor: (floor) => {
|
|
101
|
+
const nodes = useScene.getState().nodes
|
|
102
|
+
const exists = (id: string | null) => id !== null && !!nodes[id as AnyNodeId]
|
|
103
|
+
useViewer.getState().setSelection({
|
|
104
|
+
buildingId: exists(floor.buildingId) ? (floor.buildingId as BuildingNode['id']) : null,
|
|
105
|
+
levelId: exists(floor.levelId) ? (floor.levelId as LevelNode['id']) : null,
|
|
106
|
+
zoneId: exists(floor.zoneId) ? (floor.zoneId as ZoneNode['id']) : null,
|
|
107
|
+
selectedIds: floor.selectedIds.filter(exists) as BaseNode['id'][],
|
|
108
|
+
})
|
|
109
|
+
const { room } = floor
|
|
110
|
+
useEditor.setState({
|
|
111
|
+
room: room && exists(room.zoneId) && exists(room.levelId) ? room : null,
|
|
112
|
+
hoveredRoom: null,
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const prefersReducedMotion = () =>
|
|
118
|
+
typeof window !== 'undefined' &&
|
|
119
|
+
typeof window.matchMedia === 'function' &&
|
|
120
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
121
|
+
|
|
122
|
+
/** The canvas dips and comes back, so a cut reads as a cross-fade rather than a jump. */
|
|
123
|
+
function dipCanvas() {
|
|
124
|
+
const canvas = document.querySelector<HTMLElement>('[data-pascal-viewer-3d]')
|
|
125
|
+
canvas?.animate([{ opacity: 0.35 }, { opacity: 1 }], { duration: 240, easing: 'ease-out' })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const arrived = (value: OrbitValue, goal: OrbitValue) =>
|
|
129
|
+
Math.abs(value.x - goal.x) < ARRIVED.position &&
|
|
130
|
+
Math.abs(value.y - goal.y) < ARRIVED.position &&
|
|
131
|
+
Math.abs(value.z - goal.z) < ARRIVED.position &&
|
|
132
|
+
Math.abs(value.r - goal.r) < ARRIVED.position &&
|
|
133
|
+
Math.abs(value.az - goal.az) < ARRIVED.angle &&
|
|
134
|
+
Math.abs(value.el - goal.el) < ARRIVED.angle
|
|
135
|
+
|
|
136
|
+
export function FollowPascal() {
|
|
137
|
+
const camera = useThree((state) => state.camera)
|
|
138
|
+
const scene = useThree((state) => state.scene)
|
|
139
|
+
const sight = useMemo(() => new FollowSight(scene), [scene])
|
|
140
|
+
const presentation = useMemo(() => new FollowPresentation(viewPort), [])
|
|
141
|
+
const controls = useThree((state) => state.controls) as {
|
|
142
|
+
getTarget?: (out: Vector3) => Vector3
|
|
143
|
+
} | null
|
|
144
|
+
const size = useThree((state) => state.size)
|
|
145
|
+
const follower = useRef(new Follower())
|
|
146
|
+
const spring = useRef<OrbitSpring | null>(null)
|
|
147
|
+
const clock = useRef(0)
|
|
148
|
+
const flying = useRef(false)
|
|
149
|
+
const synced = useRef(false)
|
|
150
|
+
const last = useRef<FollowWant | null>(null)
|
|
151
|
+
const resuming = useRef(false)
|
|
152
|
+
const over = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
153
|
+
const scratch = useRef(new Vector3())
|
|
154
|
+
const nextSightCheck = useRef(0)
|
|
155
|
+
|
|
156
|
+
/** The camera as it is now, so a flight starts from the view the person has. */
|
|
157
|
+
const currentPose = (): CameraPose => {
|
|
158
|
+
const target = controls?.getTarget?.(scratch.current) ?? scratch.current.set(0, 0, 0)
|
|
159
|
+
return {
|
|
160
|
+
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
161
|
+
target: [target.x, target.y, target.z],
|
|
162
|
+
projection: 'perspective',
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The build in hand has a change seen from outside in it: all of it is looked at from outdoors. */
|
|
167
|
+
const outsideBuild = useRef(false)
|
|
168
|
+
/** The last word was that the play is over: the next edit is another commit's. */
|
|
169
|
+
const played = useRef(false)
|
|
170
|
+
|
|
171
|
+
const unobstructed = (want: FollowWant, goal: OrbitValue): OrbitValue => {
|
|
172
|
+
sight.refresh(want)
|
|
173
|
+
const clear = clearFrame(goal, (pose) => sight.clear(pose))
|
|
174
|
+
if (clear) return clear
|
|
175
|
+
// Seen from outside the walls stay up: a cutaway would take the wall that changes away.
|
|
176
|
+
if (want.exterior || outsideBuild.current) {
|
|
177
|
+
presentation.wall('up')
|
|
178
|
+
return goal
|
|
179
|
+
}
|
|
180
|
+
presentation.wall('cutaway')
|
|
181
|
+
return goal
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const moveTo = (goal: OrbitValue) => {
|
|
185
|
+
const orbit = spring.current!
|
|
186
|
+
if (prefersReducedMotion()) {
|
|
187
|
+
orbit.snap(goal)
|
|
188
|
+
emitter.emit('camera-controls:apply-pose', orbit.pose())
|
|
189
|
+
dipCanvas()
|
|
190
|
+
flying.current = false
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
orbit.to(goal)
|
|
194
|
+
flying.current = true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fly = (asked: FollowWant) => {
|
|
198
|
+
const nodes = useScene.getState().nodes
|
|
199
|
+
// A level below grade is shown, its ground open over it: the look at the whole house that follows
|
|
200
|
+
// furniture landing, and every finished commit, stays on that level. Showing the floors above would
|
|
201
|
+
// close the ground, and the next piece on the basement open it again, once for every commit.
|
|
202
|
+
const { selection, hideLevelsAboveSelection } = useViewer.getState()
|
|
203
|
+
const staying = hideLevelsAboveSelection && groundVeiled(useViewer.getState())
|
|
204
|
+
const want =
|
|
205
|
+
asked.kind === 'group' && !asked.levelId && staying && selection.levelId
|
|
206
|
+
? stayOnLevel(asked, nodes, selection.levelId)
|
|
207
|
+
: asked
|
|
208
|
+
last.current = want
|
|
209
|
+
const level = want.levelId ? nodes[want.levelId as AnyNodeId] : undefined
|
|
210
|
+
if (level?.type === 'level') {
|
|
211
|
+
const buildingId = resolveBuildingForLevel(level.id as AnyNodeId, nodes)
|
|
212
|
+
const building = buildingId ? nodes[buildingId] : undefined
|
|
213
|
+
presentation.floor({
|
|
214
|
+
buildingId: building?.type === 'building' ? building.id : null,
|
|
215
|
+
levelId: level.id,
|
|
216
|
+
})
|
|
217
|
+
presentation.levels('stacked')
|
|
218
|
+
presentation.hideAbove(true)
|
|
219
|
+
} else if (want.kind === 'group' && !want.levelId) {
|
|
220
|
+
// The finished house is looked at whole: its upper floors are not hidden.
|
|
221
|
+
presentation.levels('stacked')
|
|
222
|
+
presentation.hideAbove(false)
|
|
223
|
+
}
|
|
224
|
+
// A change seen from outside: the walls up for it, even if an earlier step cut them away.
|
|
225
|
+
if (want.exterior || outsideBuild.current) presentation.wall('up')
|
|
226
|
+
if (!spring.current) {
|
|
227
|
+
spring.current = new OrbitSpring({ x: 0, y: 1, z: 0, r: 20, az: 0.6, el: 0.5 })
|
|
228
|
+
synced.current = false
|
|
229
|
+
}
|
|
230
|
+
if (!synced.current) {
|
|
231
|
+
spring.current.fromPose(currentPose())
|
|
232
|
+
synced.current = true
|
|
233
|
+
}
|
|
234
|
+
const perspective = camera as { fov?: number }
|
|
235
|
+
const goal = frameOf(want, {
|
|
236
|
+
fov: perspective.fov ?? 45,
|
|
237
|
+
aspect: size.width / Math.max(1, size.height),
|
|
238
|
+
azimuth: spring.current.value().az,
|
|
239
|
+
})
|
|
240
|
+
moveTo(unobstructed(want, goal))
|
|
241
|
+
nextSightCheck.current = clock.current + 0.5
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// What the build says it is doing: where to look, and when a build begins and ends.
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const hear = (event: RevealEvent) => {
|
|
247
|
+
const store = useFollowPascal.getState()
|
|
248
|
+
if (over.current) clearTimeout(over.current)
|
|
249
|
+
if (!store.building) {
|
|
250
|
+
store.beginBuild()
|
|
251
|
+
follower.current.reset()
|
|
252
|
+
synced.current = false
|
|
253
|
+
outsideBuild.current = false
|
|
254
|
+
}
|
|
255
|
+
// An edit or a move is read before its first piece plays: one change seen from outside takes the
|
|
256
|
+
// whole commit outdoors, until the play is over. What is waiting in the queue belongs to a later
|
|
257
|
+
// commit, so a build, a deletion, or the edit that follows a finished play never inherits it.
|
|
258
|
+
const edit = 'changed' in event && event.changed === true
|
|
259
|
+
if (!edit) outsideBuild.current = false
|
|
260
|
+
else {
|
|
261
|
+
if (played.current) outsideBuild.current = false
|
|
262
|
+
const changed = outsideBuild.current ? [] : revealChangedNodeIds()
|
|
263
|
+
if (changed.length && anyExterior(useScene.getState().nodes, changed))
|
|
264
|
+
outsideBuild.current = true
|
|
265
|
+
}
|
|
266
|
+
played.current = event.type === 'complete'
|
|
267
|
+
over.current = setTimeout(() => useFollowPascal.getState().endBuild(), BUILD_OVER_MS)
|
|
268
|
+
if (event.type === 'complete') return
|
|
269
|
+
const want = wantFromEvent(event, useScene.getState().nodes, clock.current)
|
|
270
|
+
if (want) follower.current.want(want)
|
|
271
|
+
}
|
|
272
|
+
const stop = subscribeRevealEvents(hear)
|
|
273
|
+
return () => {
|
|
274
|
+
stop()
|
|
275
|
+
if (over.current) clearTimeout(over.current)
|
|
276
|
+
}
|
|
277
|
+
}, [])
|
|
278
|
+
|
|
279
|
+
// The person takes the camera: it stays theirs, and the next flight starts from where they leave it.
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
const take = () => {
|
|
282
|
+
flying.current = false
|
|
283
|
+
synced.current = false
|
|
284
|
+
useFollowPascal.getState().pause()
|
|
285
|
+
}
|
|
286
|
+
for (const name of TAKEN) emitter.on(name, take)
|
|
287
|
+
return () => {
|
|
288
|
+
for (const name of TAKEN) emitter.off(name, take)
|
|
289
|
+
}
|
|
290
|
+
}, [])
|
|
291
|
+
|
|
292
|
+
// Resuming flies back to what it was last looking at; switching it off ends a flight.
|
|
293
|
+
// Follow ends when it is switched off and when the person takes the camera: what it changed to see
|
|
294
|
+
// the build goes back. A build being over ends it only when no turn is running: a turn runs several
|
|
295
|
+
// programs, and the view holds steady between them (the host's busy flag, below).
|
|
296
|
+
useEffect(
|
|
297
|
+
() =>
|
|
298
|
+
useFollowPascal.subscribe((state, before) => {
|
|
299
|
+
if (before.paused && !state.paused && state.on) resuming.current = true
|
|
300
|
+
if (before.on && !state.on) flying.current = false
|
|
301
|
+
if ((before.on && !state.on) || (!before.paused && state.paused)) presentation.restore()
|
|
302
|
+
// The build is over: that ends it when no turn is running to build more.
|
|
303
|
+
if (before.building && !state.building && !usePointAsk.getState().busy)
|
|
304
|
+
presentation.restore()
|
|
305
|
+
}),
|
|
306
|
+
[presentation],
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
// The turn ends, or the person stops it: the host's busy flag is the editor's word on it. What the
|
|
310
|
+
// reveal still plays after that is still followed, so it ends with the build, not with the turn.
|
|
311
|
+
useEffect(
|
|
312
|
+
() =>
|
|
313
|
+
usePointAsk.subscribe((state, before) => {
|
|
314
|
+
if (before.busy && !state.busy && !useFollowPascal.getState().building)
|
|
315
|
+
presentation.restore()
|
|
316
|
+
}),
|
|
317
|
+
[presentation],
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
// Leaving the editor ends it too.
|
|
321
|
+
useEffect(() => () => presentation.restore(), [presentation])
|
|
322
|
+
|
|
323
|
+
useFrame((_, delta) => {
|
|
324
|
+
clock.current += Math.min(delta, 0.1)
|
|
325
|
+
const { on, paused, building } = useFollowPascal.getState()
|
|
326
|
+
if (!on || paused) return
|
|
327
|
+
if (useViewer.getState().cameraMode !== 'perspective' || useEditor.getState().isFirstPersonMode)
|
|
328
|
+
return
|
|
329
|
+
if (resuming.current) {
|
|
330
|
+
resuming.current = false
|
|
331
|
+
if (last.current) fly(last.current)
|
|
332
|
+
}
|
|
333
|
+
const due = follower.current.due(clock.current)
|
|
334
|
+
if (due) fly(due)
|
|
335
|
+
const orbit = spring.current
|
|
336
|
+
// Recheck as geometry lands, even after arrival; retain a clear orbit instead of hunting each frame.
|
|
337
|
+
if (
|
|
338
|
+
orbit &&
|
|
339
|
+
last.current &&
|
|
340
|
+
(building || flying.current) &&
|
|
341
|
+
clock.current >= nextSightCheck.current
|
|
342
|
+
) {
|
|
343
|
+
nextSightCheck.current = clock.current + 0.5
|
|
344
|
+
const goal = unobstructed(last.current, orbit.destination())
|
|
345
|
+
if (!arrived(goal, orbit.destination())) moveTo(goal)
|
|
346
|
+
}
|
|
347
|
+
if (!(flying.current && orbit)) return
|
|
348
|
+
orbit.step(Math.min(delta, 0.05))
|
|
349
|
+
emitter.emit('camera-controls:apply-pose', orbit.pose())
|
|
350
|
+
if (arrived(orbit.value(), orbit.destination())) flying.current = false
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
getStoredLevelHeight,
|
|
6
|
+
type Point,
|
|
7
|
+
type SlabNode,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
upperFloorHeightControl,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { Html } from '@react-three/drei'
|
|
14
|
+
import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
15
|
+
import { useRef, useState } from 'react'
|
|
16
|
+
import { type Group, Plane, Vector2, Vector3 } from 'three'
|
|
17
|
+
import {
|
|
18
|
+
beginFootprintHeightPreview,
|
|
19
|
+
footprintHeightMinimum,
|
|
20
|
+
footprintHeightValue,
|
|
21
|
+
THICK_FLOOR_HINT,
|
|
22
|
+
thickFloorAdvice,
|
|
23
|
+
} from '../../lib/floor-footprints'
|
|
24
|
+
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
25
|
+
import { runRoomHandleDrag } from '../../lib/room-handle-drag'
|
|
26
|
+
import useEditor, { isGridSnapActive } from '../../store/use-editor'
|
|
27
|
+
import useInteractionScope from '../../store/use-interaction-scope'
|
|
28
|
+
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
29
|
+
import { ElevationArrows, outlineCentroid, useHandleScale } from './room-handles'
|
|
30
|
+
|
|
31
|
+
export const FOOTPRINT_HEIGHT_DRAG_LABEL = 'footprint-height'
|
|
32
|
+
|
|
33
|
+
const PILL =
|
|
34
|
+
'pointer-events-none flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur'
|
|
35
|
+
const NOTICE =
|
|
36
|
+
'pointer-events-none max-w-72 rounded-full border border-border/60 bg-background/95 px-3 py-1 text-center text-muted-foreground text-xs shadow-md backdrop-blur-md'
|
|
37
|
+
|
|
38
|
+
/** The selected footprint floor (base plate), when it is the one selection in select mode. */
|
|
39
|
+
function useSelectedFootprint(): SlabNode | null {
|
|
40
|
+
const selectedId = useViewer((s) =>
|
|
41
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
|
|
42
|
+
)
|
|
43
|
+
const enabled = useEditor((s) => s.mode === 'select')
|
|
44
|
+
const idle = useInteractionScope(
|
|
45
|
+
(s) =>
|
|
46
|
+
s.scope.kind === 'idle' ||
|
|
47
|
+
(s.scope.kind === 'handle-drag' && s.scope.handle === FOOTPRINT_HEIGHT_DRAG_LABEL),
|
|
48
|
+
)
|
|
49
|
+
const plate = useScene((s) => {
|
|
50
|
+
const node = selectedId ? s.nodes[selectedId as AnyNodeId] : undefined
|
|
51
|
+
return node?.type === 'slab' && node.plateRole === 'base' ? node : null
|
|
52
|
+
})
|
|
53
|
+
return enabled && idle ? plate : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A selected footprint floor's one control in 3D: the rooms' arrow–cube–arrow
|
|
58
|
+
* at the middle of the footprint, dragging its height above the ground. The
|
|
59
|
+
* building follows live; the release lands as one undo step. Refused heights
|
|
60
|
+
* (core's conflict, e.g. an upper floor over another footprint) show beside it.
|
|
61
|
+
*/
|
|
62
|
+
export function FootprintHeightHandle() {
|
|
63
|
+
const plate = useSelectedFootprint()
|
|
64
|
+
const root = useRef<Group>(null)
|
|
65
|
+
useFrame(() => {
|
|
66
|
+
const level = plate?.parentId ? sceneRegistry.nodes.get(plate.parentId) : null
|
|
67
|
+
if (!root.current) return
|
|
68
|
+
root.current.visible = !!level
|
|
69
|
+
if (level) {
|
|
70
|
+
level.updateWorldMatrix(true, false)
|
|
71
|
+
root.current.matrix.copy(level.matrixWorld)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
if (!plate) return null
|
|
75
|
+
return (
|
|
76
|
+
<group matrixAutoUpdate={false} ref={root}>
|
|
77
|
+
<FootprintHeightArrows plate={plate} />
|
|
78
|
+
</group>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function FootprintHeightArrows({ plate }: { plate: SlabNode }) {
|
|
83
|
+
const { camera, raycaster, gl } = useThree()
|
|
84
|
+
const zoom = useHandleScale()
|
|
85
|
+
const unit = useViewer((s) => s.unit)
|
|
86
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
87
|
+
const [hover, setHover] = useState(false)
|
|
88
|
+
const [dragValue, setDragValue] = useState<number | null>(null)
|
|
89
|
+
const [notice, setNotice] = useState<string | null>(null)
|
|
90
|
+
const [hint, setHint] = useState<string | null>(null)
|
|
91
|
+
const upper = useScene((s) => upperFloorHeightControl(s.nodes, plate) !== null)
|
|
92
|
+
const anchor: Point = outlineCentroid([plate.polygon as Point[], ...(plate.holes as Point[][])])
|
|
93
|
+
const elevation = plate.elevation
|
|
94
|
+
|
|
95
|
+
const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
|
|
96
|
+
if (event.button !== 0) return
|
|
97
|
+
event.stopPropagation()
|
|
98
|
+
suppressBoxSelectForPointer(event)
|
|
99
|
+
const levelId = plate.parentId
|
|
100
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
101
|
+
if (!(levelId && level)) return
|
|
102
|
+
level.updateWorldMatrix(true, false)
|
|
103
|
+
const toLocal = level.matrixWorld.clone().invert()
|
|
104
|
+
const world = new Vector3(anchor[0], elevation, anchor[1]).applyMatrix4(level.matrixWorld)
|
|
105
|
+
const normal = camera.getWorldPosition(new Vector3()).sub(world).setY(0)
|
|
106
|
+
if (normal.lengthSq() === 0) return
|
|
107
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(normal.normalize(), world)
|
|
108
|
+
const localY = (clientX: number, clientY: number) => {
|
|
109
|
+
const rect = gl.domElement.getBoundingClientRect()
|
|
110
|
+
raycaster.setFromCamera(
|
|
111
|
+
new Vector2(
|
|
112
|
+
((clientX - rect.left) / rect.width) * 2 - 1,
|
|
113
|
+
-((clientY - rect.top) / rect.height) * 2 + 1,
|
|
114
|
+
),
|
|
115
|
+
camera,
|
|
116
|
+
)
|
|
117
|
+
const hit = raycaster.ray.intersectPlane(plane, new Vector3())
|
|
118
|
+
return hit ? hit.applyMatrix4(toLocal).y : null
|
|
119
|
+
}
|
|
120
|
+
const startY = localY(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
121
|
+
if (startY === null) return
|
|
122
|
+
const nodes = () => useScene.getState().nodes
|
|
123
|
+
// Heights are read against the plate as the drag found it (the preview moves it).
|
|
124
|
+
const startNodes = nodes()
|
|
125
|
+
const start = footprintHeightValue(startNodes, plate)
|
|
126
|
+
const min = footprintHeightMinimum(startNodes, plate)
|
|
127
|
+
const levelNode = nodes()[levelId as AnyNodeId]
|
|
128
|
+
const max = levelNode?.type === 'level' ? getStoredLevelHeight(levelNode) - 0.5 : 3
|
|
129
|
+
const step = isGridSnapActive() ? 0.05 : 0.01
|
|
130
|
+
let value = start
|
|
131
|
+
const preview = beginFootprintHeightPreview(plate.id)
|
|
132
|
+
setNotice(null)
|
|
133
|
+
setDragValue(start)
|
|
134
|
+
runRoomHandleDrag({
|
|
135
|
+
label: FOOTPRINT_HEIGHT_DRAG_LABEL,
|
|
136
|
+
nodeId: plate.id,
|
|
137
|
+
levelId,
|
|
138
|
+
requires: [plate.id],
|
|
139
|
+
sample: (x, y) => {
|
|
140
|
+
const pointerY = localY(x, y)
|
|
141
|
+
if (pointerY === null) return null
|
|
142
|
+
const raw = start + pointerY - startY
|
|
143
|
+
return Math.max(min, Math.min(max, Math.round(raw / step) * step))
|
|
144
|
+
},
|
|
145
|
+
onValue: (next) => {
|
|
146
|
+
if (Math.abs(next - value) < 1e-6) return
|
|
147
|
+
// Live: the building follows, outside history until the release.
|
|
148
|
+
const refused = preview.preview(next)
|
|
149
|
+
setNotice(refused)
|
|
150
|
+
setHint(thickFloorAdvice(startNodes, plate, next) ? THICK_FLOOR_HINT : null)
|
|
151
|
+
if (!refused) value = next
|
|
152
|
+
setDragValue(next)
|
|
153
|
+
},
|
|
154
|
+
onCommit: () => {
|
|
155
|
+
setNotice(preview.commit(value))
|
|
156
|
+
setDragValue(null)
|
|
157
|
+
setHint(null)
|
|
158
|
+
},
|
|
159
|
+
onCancel: () => {
|
|
160
|
+
preview.cancel()
|
|
161
|
+
setDragValue(null)
|
|
162
|
+
setHint(null)
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const shown = dragValue ?? null
|
|
168
|
+
return (
|
|
169
|
+
<group>
|
|
170
|
+
<ElevationArrows
|
|
171
|
+
anchor={[anchor[0], elevation, anchor[1]]}
|
|
172
|
+
hover={hover || shown !== null}
|
|
173
|
+
onHoverChange={setHover}
|
|
174
|
+
onPointerDown={onPointerDown}
|
|
175
|
+
zoom={zoom}
|
|
176
|
+
/>
|
|
177
|
+
{(shown !== null || notice || hint) && (
|
|
178
|
+
<Html
|
|
179
|
+
center
|
|
180
|
+
position={[anchor[0], elevation + 0.75 * zoom, anchor[1]]}
|
|
181
|
+
zIndexRange={[25, 0]}
|
|
182
|
+
>
|
|
183
|
+
<div className="flex flex-col items-center gap-1" data-footprint-height-pill>
|
|
184
|
+
{shown !== null && (
|
|
185
|
+
<div className={PILL}>
|
|
186
|
+
<span className="font-medium text-foreground">
|
|
187
|
+
{upper ? 'Floor top' : 'Foundation'}
|
|
188
|
+
</span>
|
|
189
|
+
<span className="text-muted-foreground">
|
|
190
|
+
{formatLinearMeasurement(shown, unit, metricNotation)}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
{(notice ?? hint) && (
|
|
195
|
+
<div className={NOTICE} data-footprint-height-notice role="status">
|
|
196
|
+
{notice ?? hint}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</Html>
|
|
201
|
+
)}
|
|
202
|
+
</group>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -18,13 +18,17 @@ import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
|
18
18
|
import { Plane, Vector2, Vector3 } from 'three'
|
|
19
19
|
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
20
20
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
21
|
+
import { deleteSelectedSeparator, requestRoomDeletion } from '../../lib/room-structure-commands'
|
|
22
|
+
import { captureElementActionOrigin, completeElementAction } from '../../lib/room-zone-routing'
|
|
21
23
|
import {
|
|
22
24
|
copySelectedNodesToEditorClipboard,
|
|
23
25
|
duplicateNodesToLevel,
|
|
24
26
|
getEditorClipboardSnapshot,
|
|
27
|
+
type PasteRefusal,
|
|
25
28
|
pasteSystemEditorClipboardToLevel,
|
|
26
29
|
} from '../../lib/scene-clipboard'
|
|
27
30
|
import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
|
|
31
|
+
import { isTypingTarget } from '../../lib/typing-target'
|
|
28
32
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
29
33
|
import useDeleteConfirmation from '../../store/use-delete-confirmation'
|
|
30
34
|
import useEditor, {
|
|
@@ -33,6 +37,7 @@ import useEditor, {
|
|
|
33
37
|
isGridSnapActive,
|
|
34
38
|
isMagneticSnapActive,
|
|
35
39
|
} from '../../store/use-editor'
|
|
40
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
36
41
|
import useInteractionScope from '../../store/use-interaction-scope'
|
|
37
42
|
import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
|
|
38
43
|
import {
|
|
@@ -366,6 +371,7 @@ export function startGroupPickUp(
|
|
|
366
371
|
}
|
|
367
372
|
|
|
368
373
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
374
|
+
if (isTypingTarget(e.target)) return
|
|
369
375
|
const key = e.key.toLowerCase()
|
|
370
376
|
if ((e.metaKey || e.ctrlKey) && (key === 'c' || key === 'v' || key === 'x')) {
|
|
371
377
|
// A clipboard chord replaces the current carry. Let the global keyboard
|
|
@@ -456,12 +462,39 @@ function removeUnusedPasteMaterials(materialIds: SceneMaterialId[]) {
|
|
|
456
462
|
}
|
|
457
463
|
}
|
|
458
464
|
|
|
465
|
+
const REFUSED_PASTE_REASONS: Record<PasteRefusal, [one: string, many: string]> = {
|
|
466
|
+
'no-access': [
|
|
467
|
+
'you no longer have access to the project it came from.',
|
|
468
|
+
'you no longer have access to the project they came from.',
|
|
469
|
+
],
|
|
470
|
+
'no-copies': [
|
|
471
|
+
'this version is not available to copy from the source project.',
|
|
472
|
+
'these versions are not available to copy from the source project.',
|
|
473
|
+
],
|
|
474
|
+
failed: ["it couldn't be copied. Try again.", "they couldn't be copied. Try again."],
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Why scripted objects were left out of a paste, or of a build loaded from a file. */
|
|
478
|
+
export function refusedObjectsNotice(
|
|
479
|
+
count: number,
|
|
480
|
+
refusal: PasteRefusal,
|
|
481
|
+
action: 'pasted' | 'loaded' = 'pasted',
|
|
482
|
+
) {
|
|
483
|
+
const [one, many] = REFUSED_PASTE_REASONS[refusal]
|
|
484
|
+
return count === 1
|
|
485
|
+
? `1 object wasn't ${action}: ${one}`
|
|
486
|
+
: `${count} objects weren't ${action}: ${many}`
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let pastePending = false
|
|
490
|
+
|
|
459
491
|
/**
|
|
460
492
|
* Paste the Pascal scene payload from the browser clipboard onto the active
|
|
461
493
|
* level, then carry the clones under the cursor until click-to-place. Escape
|
|
462
494
|
* removes the uncommitted clones and any scene materials imported with them.
|
|
463
495
|
*/
|
|
464
496
|
export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promise<boolean> {
|
|
497
|
+
if (pastePending) return false
|
|
465
498
|
const activeScope = useInteractionScope.getState().scope
|
|
466
499
|
if (activeScope.kind === 'placing' || activeScope.kind === 'moving') {
|
|
467
500
|
emitter.emit('tool:cancel')
|
|
@@ -476,7 +509,18 @@ export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promis
|
|
|
476
509
|
// above there is nothing to abandon.
|
|
477
510
|
if (isBrushMode(useEditor.getState().mode)) useEditor.getState().setMode('select')
|
|
478
511
|
|
|
479
|
-
|
|
512
|
+
pastePending = true
|
|
513
|
+
let result: Awaited<ReturnType<typeof pasteSystemEditorClipboardToLevel>>
|
|
514
|
+
try {
|
|
515
|
+
result = await pasteSystemEditorClipboardToLevel(targetLevelId)
|
|
516
|
+
} finally {
|
|
517
|
+
pastePending = false
|
|
518
|
+
}
|
|
519
|
+
if (result?.refusal) {
|
|
520
|
+
useFloorplanMode
|
|
521
|
+
.getState()
|
|
522
|
+
.showNotice(refusedObjectsNotice(result.refusedIds.length, result.refusal))
|
|
523
|
+
}
|
|
480
524
|
if (!result || result.pastedIds.length === 0) return false
|
|
481
525
|
|
|
482
526
|
const discardPaste = () => {
|
|
@@ -547,7 +591,16 @@ export function cutSelectionToEditorClipboard(): boolean {
|
|
|
547
591
|
*/
|
|
548
592
|
export function deleteSelection(): boolean {
|
|
549
593
|
const selectedIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
550
|
-
if (selectedIds.length === 0)
|
|
594
|
+
if (selectedIds.length === 0) {
|
|
595
|
+
const room = useEditor.getState().room
|
|
596
|
+
if (!room) return false
|
|
597
|
+
requestRoomDeletion(room.zoneId)
|
|
598
|
+
return true
|
|
599
|
+
}
|
|
600
|
+
// A drilled piece of a room (a wall, a separator, its ceiling) deleted: back to the room.
|
|
601
|
+
const origin = captureElementActionOrigin(selectedIds)
|
|
602
|
+
if (selectedIds.length === 1 && useScene.getState().nodes[selectedIds[0]!]?.type === 'separator')
|
|
603
|
+
return deleteSelectedSeparator(selectedIds[0]!, origin)
|
|
551
604
|
|
|
552
605
|
const commitDelete = () => {
|
|
553
606
|
const detail =
|
|
@@ -562,6 +615,7 @@ export function deleteSelection(): boolean {
|
|
|
562
615
|
}
|
|
563
616
|
useScene.getState().deleteNodes(selectedIds)
|
|
564
617
|
useViewer.getState().setSelection({ selectedIds: [] })
|
|
618
|
+
completeElementAction(origin)
|
|
565
619
|
}
|
|
566
620
|
|
|
567
621
|
if (selectedIds.length >= BULK_DELETE_THRESHOLD) {
|
|
@@ -17,6 +17,7 @@ import { type Camera, Plane, type Raycaster, Vector2, Vector3 } from 'three'
|
|
|
17
17
|
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
18
18
|
import { isHistoryShortcut } from '../../lib/history'
|
|
19
19
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
20
|
+
import { isTypingTarget } from '../../lib/typing-target'
|
|
20
21
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
21
22
|
import useEditor, {
|
|
22
23
|
isAlignmentGuideActive,
|
|
@@ -380,6 +381,7 @@ export function armGroupMove3d(args: {
|
|
|
380
381
|
// `use-keyboard` arms, which would otherwise act on stale store state
|
|
381
382
|
// (or clear the multi-selection) mid-session.
|
|
382
383
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
384
|
+
if (isTypingTarget(e.target)) return
|
|
383
385
|
const key = e.key.toLowerCase()
|
|
384
386
|
if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
|
|
385
387
|
// Armed but still under the drag threshold: engage first (exactly what
|