@pascal-app/editor 1.0.1 → 1.0.4
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 +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- 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 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -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-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- 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 +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- 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 +132 -0
- 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 +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -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 +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- 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 +670 -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.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- 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 +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- 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 +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- 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-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- 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/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- 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 +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- 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/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- 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 +20 -14
- 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 +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- 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-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- 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 +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- 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 +725 -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/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- 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 +176 -211
- 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 +0 -27
- 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 +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- 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-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -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 +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- 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 +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -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/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- 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 +283 -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 +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -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 +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -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/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -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 +232 -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 +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -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 +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -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 +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- 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 +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- 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 +186 -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 +407 -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 +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -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 +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -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 +111 -0
- package/src/lib/paint-pass-through.ts +76 -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 +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- 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 +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- 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 +266 -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 +973 -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 +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- 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 +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -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 +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -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 +569 -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-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-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- 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/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- 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 +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -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 +175 -0
- package/src/store/view-layout.ts +196 -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/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/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,110 @@
|
|
|
1
|
+
import type { AnyNode, WallFace, WallNode } from '@pascal-app/core'
|
|
2
|
+
import { getWallFaceBaseAt } from '@pascal-app/viewer'
|
|
3
|
+
|
|
4
|
+
// Snapping for the paint tool's wall region gestures, in face coordinates:
|
|
5
|
+
// `u` is metres from the wall start along the reference line (the chord on a
|
|
6
|
+
// curved wall), `v` metres above the face's own base. Modes mirror the 'polygon'
|
|
7
|
+
// snap context: `grid` rounds to the grid step, `lines` catches the face's own
|
|
8
|
+
// features within a tolerance, `off` (or Alt held) leaves the pointer raw.
|
|
9
|
+
|
|
10
|
+
export type WallRegionSnapMode = 'grid' | 'lines' | 'off'
|
|
11
|
+
|
|
12
|
+
export type WallRegionSnap = {
|
|
13
|
+
mode: WallRegionSnapMode
|
|
14
|
+
/** Grid step (m) used by `grid`. */
|
|
15
|
+
gridStep: number
|
|
16
|
+
/** Catch distance (m) used by `lines`. */
|
|
17
|
+
tolerance: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One face's base runs, as `getWallFinishData(geometry).faceBase[face]` carries them. */
|
|
21
|
+
export type WallFaceBaseRuns = readonly { start: number; end: number; y: number }[] | null
|
|
22
|
+
|
|
23
|
+
export type WallRegionSnapTargets = { u: number[]; v: number[] }
|
|
24
|
+
|
|
25
|
+
/** The wainscot height a Horizontal region starts at. */
|
|
26
|
+
export const DEFAULT_WAINSCOT_HEIGHT = 0.9
|
|
27
|
+
/** Regions narrower or shorter than this are a slip, not a gesture. */
|
|
28
|
+
export const MIN_WALL_REGION_SIZE = 0.02
|
|
29
|
+
export const DEFAULT_LINE_SNAP_TOLERANCE = 0.1
|
|
30
|
+
|
|
31
|
+
export function resolveWallRegionSnapMode(flags: {
|
|
32
|
+
grid: boolean
|
|
33
|
+
magnetic: boolean
|
|
34
|
+
alt?: boolean
|
|
35
|
+
}): WallRegionSnapMode {
|
|
36
|
+
if (flags.alt) return 'off'
|
|
37
|
+
if (flags.grid) return 'grid'
|
|
38
|
+
if (flags.magnetic) return 'lines'
|
|
39
|
+
return 'off'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Base (local y) of a face at station `u`: 0 when the faces share the wall base. */
|
|
43
|
+
export function faceBaseAt(runs: WallFaceBaseRuns, u: number): number {
|
|
44
|
+
return runs?.length ? getWallFaceBaseAt({ faceBase: { a: [...runs], b: [] } }, 'a', u) : 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uniqueSorted(values: number[]): number[] {
|
|
48
|
+
const out: number[] = []
|
|
49
|
+
for (const value of values.filter(Number.isFinite).sort((a, b) => a - b))
|
|
50
|
+
if (out.length === 0 || value - out.at(-1)! > 1e-6) out.push(value)
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a region on this face snaps to: the wall ends, openings' jambs, sills
|
|
56
|
+
* and heads, other regions' bounds on the same face, the face base and the
|
|
57
|
+
* wainscot height. Opening heights are local y, so each is measured above the
|
|
58
|
+
* face base under the opening's centre.
|
|
59
|
+
*/
|
|
60
|
+
export function wallRegionSnapTargets(
|
|
61
|
+
wall: Pick<WallNode, 'start' | 'end' | 'children' | 'faceRegions'>,
|
|
62
|
+
face: WallFace,
|
|
63
|
+
nodes: Readonly<Record<string, AnyNode | undefined>>,
|
|
64
|
+
runs: WallFaceBaseRuns = null,
|
|
65
|
+
options: { excludeRegionId?: string } = {},
|
|
66
|
+
): WallRegionSnapTargets {
|
|
67
|
+
const length = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1])
|
|
68
|
+
const u = [0, length]
|
|
69
|
+
const v = [0, DEFAULT_WAINSCOT_HEIGHT]
|
|
70
|
+
for (const childId of wall.children ?? []) {
|
|
71
|
+
const child = nodes[childId]
|
|
72
|
+
if (child?.type !== 'door' && child?.type !== 'window') continue
|
|
73
|
+
const [x, y] = child.position
|
|
74
|
+
u.push(x - child.width / 2, x + child.width / 2)
|
|
75
|
+
const base = faceBaseAt(runs, x)
|
|
76
|
+
v.push(y - child.height / 2 - base, y + child.height / 2 - base)
|
|
77
|
+
}
|
|
78
|
+
for (const region of wall.faceRegions ?? []) {
|
|
79
|
+
if (region.face !== face || region.id === options.excludeRegionId) continue
|
|
80
|
+
for (const bound of [region.u0, region.u1]) if (bound !== undefined) u.push(bound)
|
|
81
|
+
for (const bound of [region.v0, region.v1]) if (bound !== undefined) v.push(bound)
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
u: uniqueSorted(u.filter((value) => value >= -1e-6 && value <= length + 1e-6)),
|
|
85
|
+
v: uniqueSorted(v.filter((value) => value >= -1e-6)),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Snaps one face coordinate. `target` is the feature it caught in `lines` mode. */
|
|
90
|
+
export function snapWallRegionValue(
|
|
91
|
+
value: number,
|
|
92
|
+
targets: readonly number[],
|
|
93
|
+
snap: WallRegionSnap,
|
|
94
|
+
): { value: number; target: number | null } {
|
|
95
|
+
if (snap.mode === 'grid' && snap.gridStep > 0) {
|
|
96
|
+
const stepped = Math.round(value / snap.gridStep) * snap.gridStep
|
|
97
|
+
return { value: Math.round(stepped * 1e6) / 1e6, target: null }
|
|
98
|
+
}
|
|
99
|
+
if (snap.mode === 'lines') {
|
|
100
|
+
let best: number | null = null
|
|
101
|
+
for (const target of targets)
|
|
102
|
+
if (
|
|
103
|
+
Math.abs(target - value) <= snap.tolerance &&
|
|
104
|
+
(best === null || Math.abs(target - value) < Math.abs(best - value))
|
|
105
|
+
)
|
|
106
|
+
best = target
|
|
107
|
+
if (best !== null) return { value: best, target: best }
|
|
108
|
+
}
|
|
109
|
+
return { value, target: null }
|
|
110
|
+
}
|
|
@@ -84,3 +84,17 @@ export function closeWindowOpenState(windowId: AnyNodeId, options?: WindowOpenAn
|
|
|
84
84
|
const currentOpenAmount = getDisplayedWindowValue(windowId, node.operationState)
|
|
85
85
|
startWindowOpenAnimation(windowId, 'operationState', currentOpenAmount, 0, options)
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
export function openWindowOpenState(windowId: AnyNodeId, options?: WindowOpenAnimationOptions) {
|
|
89
|
+
const node = useScene.getState().nodes[windowId]
|
|
90
|
+
if (
|
|
91
|
+
node?.type !== 'window' ||
|
|
92
|
+
node.openingKind === 'opening' ||
|
|
93
|
+
!isOperableWindowType(node.windowType)
|
|
94
|
+
) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const currentOpenAmount = getDisplayedWindowValue(windowId, node.operationState)
|
|
99
|
+
startWindowOpenAnimation(windowId, 'operationState', currentOpenAmount, 1, options)
|
|
100
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { BuildingNode, LevelNode, useScene } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { getActiveBuildingId } from './world-grid-snap'
|
|
5
|
+
|
|
6
|
+
const originalNodes = useScene.getState().nodes
|
|
7
|
+
const originalSelection = useViewer.getState().selection
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
useScene.setState({ nodes: originalNodes } as never)
|
|
11
|
+
useViewer.setState({ selection: originalSelection } as never)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('getActiveBuildingId', () => {
|
|
15
|
+
test('prefers the active level owner over a stale selected building', () => {
|
|
16
|
+
const staleBuilding = BuildingNode.parse({})
|
|
17
|
+
const activeBuilding = BuildingNode.parse({})
|
|
18
|
+
const level = LevelNode.parse({ parentId: activeBuilding.id })
|
|
19
|
+
useScene.setState({
|
|
20
|
+
nodes: {
|
|
21
|
+
[staleBuilding.id]: staleBuilding,
|
|
22
|
+
[activeBuilding.id]: activeBuilding,
|
|
23
|
+
[level.id]: level,
|
|
24
|
+
},
|
|
25
|
+
} as never)
|
|
26
|
+
useViewer.setState({
|
|
27
|
+
selection: {
|
|
28
|
+
buildingId: staleBuilding.id,
|
|
29
|
+
levelId: level.id,
|
|
30
|
+
zoneId: null,
|
|
31
|
+
selectedIds: [],
|
|
32
|
+
},
|
|
33
|
+
} as never)
|
|
34
|
+
|
|
35
|
+
expect(getActiveBuildingId()).toBe(activeBuilding.id)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('resolves legacy level ownership from the building children list', () => {
|
|
39
|
+
const level = LevelNode.parse({})
|
|
40
|
+
const building = BuildingNode.parse({ children: [level.id] })
|
|
41
|
+
useScene.setState({ nodes: { [building.id]: building, [level.id]: level } } as never)
|
|
42
|
+
useViewer.setState({
|
|
43
|
+
selection: { buildingId: null, levelId: level.id, zoneId: null, selectedIds: [] },
|
|
44
|
+
} as never)
|
|
45
|
+
|
|
46
|
+
expect(getActiveBuildingId()).toBe(building.id)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type BuildingPose,
|
|
16
16
|
type ResolveAlignmentInBuildingResult,
|
|
17
17
|
resolveAlignmentInBuildingWorld,
|
|
18
|
+
resolveBuildingForLevel,
|
|
18
19
|
snapWorldXZToBuildingLocal,
|
|
19
20
|
useLiveTransforms,
|
|
20
21
|
useScene,
|
|
@@ -39,7 +40,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
39
40
|
* guides drift off the visible grid mid-drag (and through any post-drag
|
|
40
41
|
* frame where the live override is still set).
|
|
41
42
|
*/
|
|
42
|
-
export function
|
|
43
|
+
export function getActiveBuildingId(): AnyNodeId | null {
|
|
43
44
|
const sel = useViewer.getState().selection
|
|
44
45
|
const nodes = useScene.getState().nodes
|
|
45
46
|
// Match `use-floorplan-scene-data.ts`: prefer the active level's
|
|
@@ -48,12 +49,15 @@ export function getActiveBuildingPose(): BuildingPose | null {
|
|
|
48
49
|
// different level, the level path is the authoritative one.
|
|
49
50
|
let buildingId: AnyNodeId | null = null
|
|
50
51
|
if (sel.levelId) {
|
|
51
|
-
|
|
52
|
-
if (level && level.type === 'level' && level.parentId) {
|
|
53
|
-
buildingId = level.parentId as AnyNodeId
|
|
54
|
-
}
|
|
52
|
+
buildingId = resolveBuildingForLevel(sel.levelId as AnyNodeId, nodes)
|
|
55
53
|
}
|
|
56
54
|
if (!buildingId) buildingId = sel.buildingId ?? null
|
|
55
|
+
return buildingId
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getActiveBuildingPose(): BuildingPose | null {
|
|
59
|
+
const buildingId = getActiveBuildingId()
|
|
60
|
+
const nodes = useScene.getState().nodes
|
|
57
61
|
const building = buildingId ? nodes[buildingId] : null
|
|
58
62
|
if (building?.type !== 'building') return null
|
|
59
63
|
const live = useLiveTransforms.getState().transforms.get(buildingId as string)
|
package/src/lib/zone-content.ts
CHANGED
|
@@ -50,7 +50,10 @@ function polygonContainsWithTolerance(
|
|
|
50
50
|
})
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function polygonMatchesZoneFootprint(
|
|
53
|
+
export function polygonMatchesZoneFootprint(
|
|
54
|
+
surfacePolygon: Point2D[],
|
|
55
|
+
footprint: Point2D[],
|
|
56
|
+
): boolean {
|
|
54
57
|
if (surfacePolygon.length < 3) return false
|
|
55
58
|
return (
|
|
56
59
|
polygonContainsWithTolerance(footprint, surfacePolygon, SURFACE_POLYGON_TOLERANCE) &&
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import { nextZoneName } from './zone-name'
|
|
4
|
+
|
|
5
|
+
const zone = (id: string, name?: string) => ({ id, type: 'zone', name }) as unknown as AnyNode
|
|
6
|
+
|
|
7
|
+
describe('nextZoneName', () => {
|
|
8
|
+
test('room zones do not consume zone numbers', () => {
|
|
9
|
+
expect(nextZoneName({ a: zone('a', 'Room'), b: zone('b', 'Kitchen') })).toBe('Zone 1')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('continues after the highest existing Zone N', () => {
|
|
13
|
+
expect(nextZoneName({ a: zone('a', 'Zone 1'), b: zone('b', 'Zone 4'), c: zone('c') })).toBe(
|
|
14
|
+
'Zone 5',
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
const ZONE_NAME = /^Zone (\d+)$/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Next free "Zone N" name. Rooms own zones too ("Room", "Kitchen", …), so
|
|
7
|
+
* counting every zone would skip numbers; only existing "Zone N" names count.
|
|
8
|
+
*/
|
|
9
|
+
export function nextZoneName(nodes: Record<string, AnyNode>): string {
|
|
10
|
+
let highest = 0
|
|
11
|
+
for (const node of Object.values(nodes)) {
|
|
12
|
+
if (node.type !== 'zone') continue
|
|
13
|
+
const match = ZONE_NAME.exec(node.name ?? '')
|
|
14
|
+
if (match) highest = Math.max(highest, Number(match[1]))
|
|
15
|
+
}
|
|
16
|
+
return `Zone ${highest + 1}`
|
|
17
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createElement } from 'react'
|
|
3
|
+
import type { SidebarTab } from '../components/ui/sidebar/tab-bar'
|
|
4
|
+
import { useShownSidebarPanel } from '../lib/sidebar-panel'
|
|
5
|
+
import { withSelectionHarness } from '../test-utils/selection-harness'
|
|
6
|
+
import useEditor, { DEFAULT_PERSISTED_EDITOR_UI_STATE, editorUiStateOnOpen } from './use-editor'
|
|
7
|
+
import { sceneLayout } from './view-layout'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A project always opens in select mode.
|
|
11
|
+
*
|
|
12
|
+
* The armed tool used to ride along in the persisted UI preferences, so whatever
|
|
13
|
+
* the last session left armed came back on the next load and the first canvas
|
|
14
|
+
* click drew a wall (or dropped an item) instead of selecting what the user
|
|
15
|
+
* clicked on. `partialize` no longer writes the tool, and a blob written before
|
|
16
|
+
* that must not re-arm one either — which is what these cover. Everything else
|
|
17
|
+
* in the blob is a real preference and still has to come back.
|
|
18
|
+
*/
|
|
19
|
+
describe('opening a project', () => {
|
|
20
|
+
test('a persisted wall tool is not armed, and the preferences beside it survive', () => {
|
|
21
|
+
const state = editorUiStateOnOpen({
|
|
22
|
+
phase: 'building',
|
|
23
|
+
toolMode: { mode: 'build', tool: 'wall' },
|
|
24
|
+
mode: 'build',
|
|
25
|
+
tool: 'wall',
|
|
26
|
+
viewLayouts: { edit: sceneLayout('split') },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(state.toolMode).toEqual({ mode: 'select' })
|
|
30
|
+
expect(state.mode).toBe('select')
|
|
31
|
+
expect(state.tool).toBeNull()
|
|
32
|
+
expect(state.phase).toBe('building')
|
|
33
|
+
expect(state.viewLayouts).toEqual({ edit: sceneLayout('split') })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('the 2D plan opens in select mode as well', () => {
|
|
37
|
+
const state = editorUiStateOnOpen({
|
|
38
|
+
phase: 'building',
|
|
39
|
+
toolMode: { mode: 'build', tool: 'slab' },
|
|
40
|
+
mode: 'build',
|
|
41
|
+
tool: 'slab',
|
|
42
|
+
viewLayouts: { edit: sceneLayout('2d') },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(state.viewLayouts).toEqual({ edit: sceneLayout('2d') })
|
|
46
|
+
expect(state.mode).toBe('select')
|
|
47
|
+
expect(state.tool).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('a persisted item tool leaves no catalog category armed either', () => {
|
|
51
|
+
const state = editorUiStateOnOpen({
|
|
52
|
+
phase: 'building',
|
|
53
|
+
toolMode: { mode: 'build', tool: 'item' },
|
|
54
|
+
mode: 'build',
|
|
55
|
+
tool: 'item',
|
|
56
|
+
catalogCategory: 'kitchen',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
expect(state.phase).toBe('building')
|
|
60
|
+
expect(state.mode).toBe('select')
|
|
61
|
+
expect(state.tool).toBeNull()
|
|
62
|
+
expect(state.catalogCategory).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a phase stored before Structure and Furnish merged opens inside the building', () => {
|
|
66
|
+
// Browsers still hold `structure` / `furnish` from before the merge.
|
|
67
|
+
for (const stored of ['structure', 'furnish']) {
|
|
68
|
+
expect(editorUiStateOnOpen({ phase: stored as never }).phase).toBe('building')
|
|
69
|
+
}
|
|
70
|
+
expect(editorUiStateOnOpen({ phase: 'site' }).phase).toBe('site')
|
|
71
|
+
expect(editorUiStateOnOpen({ phase: 'garden' as never }).phase).toBe('site')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('the brush modes do not come back armed', () => {
|
|
75
|
+
// Paint and sculpt hold an interaction scope for the whole mode, so opening
|
|
76
|
+
// into one is the same class of surprise as opening into a build tool.
|
|
77
|
+
expect(editorUiStateOnOpen({ phase: 'site', mode: 'terrain-sculpt' }).mode).toBe('select')
|
|
78
|
+
expect(editorUiStateOnOpen({ phase: 'building', mode: 'material-paint' }).mode).toBe('select')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('a blob from the Elements/Rooms layer days opens in select mode, its layer dropped', () => {
|
|
82
|
+
const state = editorUiStateOnOpen({
|
|
83
|
+
phase: 'building',
|
|
84
|
+
structureLayer: 'zones',
|
|
85
|
+
toolMode: { mode: 'build', tool: 'zone' },
|
|
86
|
+
mode: 'build',
|
|
87
|
+
tool: 'zone',
|
|
88
|
+
} as never)
|
|
89
|
+
|
|
90
|
+
expect(state).not.toHaveProperty('structureLayer')
|
|
91
|
+
expect(state.mode).toBe('select')
|
|
92
|
+
expect(state.tool).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('an empty blob opens on the defaults', () => {
|
|
96
|
+
expect(editorUiStateOnOpen(null)).toEqual(DEFAULT_PERSISTED_EDITOR_UI_STATE)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const STORAGE_KEY = 'pascal-editor-ui-preferences'
|
|
101
|
+
const savedWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
if (savedWindow) Object.defineProperty(globalThis, 'window', savedWindow)
|
|
104
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/** A browser's localStorage, seeded with what an earlier session left behind (a string as is). */
|
|
108
|
+
function stubStorage(stored: Record<string, unknown> = {}) {
|
|
109
|
+
const values = new Map(
|
|
110
|
+
Object.entries(stored).map(
|
|
111
|
+
([key, value]) => [key, typeof value === 'string' ? value : JSON.stringify(value)] as const,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
const localStorage: Partial<Storage> = {
|
|
115
|
+
getItem: (key) => values.get(key) ?? null,
|
|
116
|
+
setItem: (key, value) => void values.set(key, value),
|
|
117
|
+
removeItem: (key) => void values.delete(key),
|
|
118
|
+
}
|
|
119
|
+
Object.defineProperty(globalThis, 'window', { configurable: true, value: { localStorage } })
|
|
120
|
+
return { read: (key: string) => JSON.parse(values.get(key) ?? 'null') }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let fresh = 0
|
|
124
|
+
/** The editor store as a page load creates it: not yet rehydrated (`skipHydration`). */
|
|
125
|
+
async function loadEditorStore() {
|
|
126
|
+
const { default: useEditor } = await import(`./use-editor.tsx?load=${++fresh}`)
|
|
127
|
+
return useEditor as typeof import('./use-editor').default
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe('reopening the editor', () => {
|
|
131
|
+
// The editor rehydrates in a mount effect, after the host's own mount effects
|
|
132
|
+
// have written to the store. Each write saves the whole state, so a write
|
|
133
|
+
// saved before rehydrate would replace the stored preferences with the
|
|
134
|
+
// defaults before they are read.
|
|
135
|
+
test('a write before the store rehydrates leaves the stored panel to come back', async () => {
|
|
136
|
+
const storage = stubStorage({
|
|
137
|
+
[STORAGE_KEY]: { state: { sidebarPanels: { edit: 'ai' } }, version: 0 },
|
|
138
|
+
})
|
|
139
|
+
const useEditor = await loadEditorStore()
|
|
140
|
+
|
|
141
|
+
useEditor.getState().setCanFindNode(true)
|
|
142
|
+
await useEditor.persist.rehydrate()
|
|
143
|
+
|
|
144
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('ai')
|
|
145
|
+
expect(useEditor.getState().canFindNode).toBe(true)
|
|
146
|
+
expect(storage.read(STORAGE_KEY).state.sidebarPanels).toEqual({ edit: 'ai' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('each workspace reopens its own panel, and a reload opens Edit on its own', async () => {
|
|
150
|
+
stubStorage()
|
|
151
|
+
const firstSession = await loadEditorStore()
|
|
152
|
+
await firstSession.persist.rehydrate()
|
|
153
|
+
const editor = () => firstSession.getState()
|
|
154
|
+
|
|
155
|
+
editor().setActiveSidebarPanel('ai')
|
|
156
|
+
editor().setWorkspaceMode('studio')
|
|
157
|
+
editor().setActiveSidebarPanel('studio-materials')
|
|
158
|
+
editor().setWorkspaceMode('edit')
|
|
159
|
+
expect(editor().activeSidebarPanel).toBe('ai')
|
|
160
|
+
editor().setWorkspaceMode('studio')
|
|
161
|
+
expect(editor().activeSidebarPanel).toBe('studio-materials')
|
|
162
|
+
|
|
163
|
+
const nextSession = await loadEditorStore()
|
|
164
|
+
await nextSession.persist.rehydrate()
|
|
165
|
+
expect(nextSession.getState().workspaceMode).toBe('edit')
|
|
166
|
+
expect(nextSession.getState().activeSidebarPanel).toBe('ai')
|
|
167
|
+
nextSession.getState().setWorkspaceMode('studio')
|
|
168
|
+
expect(nextSession.getState().activeSidebarPanel).toBe('studio-materials')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('an unreadable stored blob opens on the defaults, and the next change replaces it', async () => {
|
|
172
|
+
const storage = stubStorage({ [STORAGE_KEY]: '{"state":{"sidebarPanels"' })
|
|
173
|
+
const useEditor = await loadEditorStore()
|
|
174
|
+
await useEditor.persist.rehydrate()
|
|
175
|
+
|
|
176
|
+
expect(useEditor.persist.hasHydrated()).toBe(true)
|
|
177
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('build')
|
|
178
|
+
useEditor.getState().setActiveSidebarPanel('ai')
|
|
179
|
+
expect(storage.read(STORAGE_KEY).state.sidebarPanels).toEqual({ edit: 'ai' })
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("a tab whose preferences did not change leaves another tab's panel stored", async () => {
|
|
183
|
+
const storage = stubStorage()
|
|
184
|
+
const staleTab = await loadEditorStore()
|
|
185
|
+
await staleTab.persist.rehydrate()
|
|
186
|
+
staleTab.getState().setActiveSidebarPanel('build')
|
|
187
|
+
const otherTab = await loadEditorStore()
|
|
188
|
+
await otherTab.persist.rehydrate()
|
|
189
|
+
otherTab.getState().setActiveSidebarPanel('ai')
|
|
190
|
+
|
|
191
|
+
staleTab.getState().setCanFindNode(true)
|
|
192
|
+
|
|
193
|
+
expect(storage.read(STORAGE_KEY).state.sidebarPanels).toEqual({ edit: 'ai' })
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('the rail', () => {
|
|
198
|
+
test('a remembered panel the rail lacks shows the first tab, keeps the pick, and comes back', async () => {
|
|
199
|
+
const tab = (id: string): SidebarTab => ({ id, label: id })
|
|
200
|
+
let shown = ''
|
|
201
|
+
function Rail({ tabs }: { tabs: SidebarTab[] }) {
|
|
202
|
+
shown = useShownSidebarPanel(tabs)
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await withSelectionHarness(async ({ render }) => {
|
|
207
|
+
useEditor.setState({
|
|
208
|
+
workspaceMode: 'edit',
|
|
209
|
+
activeSidebarPanel: 'nature',
|
|
210
|
+
sidebarPanels: { edit: 'nature' },
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await render(createElement(Rail, { tabs: [tab('build'), tab('paint')] }))
|
|
214
|
+
expect(shown).toBe('build')
|
|
215
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('build')
|
|
216
|
+
expect(useEditor.getState().sidebarPanels).toEqual({ edit: 'nature' })
|
|
217
|
+
|
|
218
|
+
await render(createElement(Rail, { tabs: [tab('build'), tab('paint'), tab('nature')] }))
|
|
219
|
+
expect(shown).toBe('nature')
|
|
220
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('nature')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { armPaintFromShortcut } from '../hooks/use-keyboard'
|
|
3
|
+
import {
|
|
4
|
+
bindPaintPickHold,
|
|
5
|
+
isPaintErasing,
|
|
6
|
+
paintPickReturnMode,
|
|
7
|
+
paintRegionModeActive,
|
|
8
|
+
resetPaintMode,
|
|
9
|
+
usePaintRegionMode,
|
|
10
|
+
} from '../lib/paint-region-mode'
|
|
11
|
+
import useEditor from './use-editor'
|
|
12
|
+
|
|
13
|
+
const TILE = { materialPreset: 'library:tile', sourceTarget: 'wall' } as const
|
|
14
|
+
const sub = () => usePaintRegionMode.getState().mode
|
|
15
|
+
const editorMode = () => useEditor.getState().mode
|
|
16
|
+
|
|
17
|
+
function reset() {
|
|
18
|
+
useEditor.getState().setPhase('building')
|
|
19
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
20
|
+
useEditor.getState().setActivePaintMaterial(null)
|
|
21
|
+
resetPaintMode()
|
|
22
|
+
}
|
|
23
|
+
beforeEach(reset)
|
|
24
|
+
afterEach(reset)
|
|
25
|
+
|
|
26
|
+
describe('paint sub-mode state machine', () => {
|
|
27
|
+
test('the eraser enters paint mode directly from select, with no colour picked', () => {
|
|
28
|
+
expect(useEditor.getState().activePaintMaterial).toBeNull()
|
|
29
|
+
useEditor.getState().armMaterialPaint(undefined, 'erase')
|
|
30
|
+
expect(editorMode()).toBe('material-paint')
|
|
31
|
+
expect(sub()).toBe('erase')
|
|
32
|
+
expect(isPaintErasing()).toBe(true)
|
|
33
|
+
// Erasing is not a region gesture: whole-surface clicks own the pointer.
|
|
34
|
+
expect(paintRegionModeActive(editorMode())).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('the paint tool enters on the last painting sub-mode, never on erase', () => {
|
|
38
|
+
useEditor.getState().armMaterialPaint(undefined, 'rectangle')
|
|
39
|
+
usePaintRegionMode.getState().setMode('erase')
|
|
40
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
41
|
+
useEditor.getState().armMaterialPaint()
|
|
42
|
+
expect(editorMode()).toBe('material-paint')
|
|
43
|
+
expect(sub()).toBe('rectangle')
|
|
44
|
+
expect(paintRegionModeActive(editorMode())).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('the P shortcut while erasing switches back to painting', () => {
|
|
48
|
+
useEditor.getState().armMaterialPaint(undefined, 'polygon')
|
|
49
|
+
usePaintRegionMode.getState().setMode('erase')
|
|
50
|
+
armPaintFromShortcut()
|
|
51
|
+
expect(editorMode()).toBe('material-paint')
|
|
52
|
+
expect(sub()).toBe('polygon')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('leaving paint mode by any route ends erasing and clears the notice', () => {
|
|
56
|
+
for (const leave of [
|
|
57
|
+
() => useEditor.getState().armToolMode({ mode: 'select' }),
|
|
58
|
+
() => useEditor.getState().setMode('delete'),
|
|
59
|
+
() => useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' }),
|
|
60
|
+
() => useEditor.getState().armToolMode({ mode: 'terrain-sculpt' }),
|
|
61
|
+
]) {
|
|
62
|
+
reset()
|
|
63
|
+
useEditor.getState().armMaterialPaint(undefined, 'rectangle')
|
|
64
|
+
usePaintRegionMode.getState().setMode('erase')
|
|
65
|
+
usePaintRegionMode.getState().setNotice('Up to 8 regions per wall face')
|
|
66
|
+
leave()
|
|
67
|
+
expect(editorMode()).not.toBe('material-paint')
|
|
68
|
+
expect(sub()).toBe('rectangle')
|
|
69
|
+
expect(usePaintRegionMode.getState().notice).toBeNull()
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('picking a colour while erasing paints with it on the last painting sub-mode', () => {
|
|
74
|
+
useEditor.getState().armMaterialPaint(undefined, 'rectangle')
|
|
75
|
+
useEditor.getState().armMaterialPaint(undefined, 'erase')
|
|
76
|
+
useEditor.getState().armMaterialPaint(TILE)
|
|
77
|
+
expect(sub()).toBe('rectangle')
|
|
78
|
+
expect(useEditor.getState().activePaintMaterial?.materialPreset).toBe('library:tile')
|
|
79
|
+
|
|
80
|
+
usePaintRegionMode.getState().setMode('erase')
|
|
81
|
+
useEditor.getState().setActivePaintMaterial(TILE)
|
|
82
|
+
expect(sub()).toBe('rectangle')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('switching sub-mode inside paint mode keeps the mode and remembers the drawing one', () => {
|
|
86
|
+
useEditor.getState().armMaterialPaint(TILE)
|
|
87
|
+
for (const mode of ['rectangle', 'erase', 'polygon', 'erase', 'surface'] as const) {
|
|
88
|
+
usePaintRegionMode.getState().setMode(mode)
|
|
89
|
+
expect(editorMode()).toBe('material-paint')
|
|
90
|
+
expect(sub()).toBe(mode)
|
|
91
|
+
}
|
|
92
|
+
expect(usePaintRegionMode.getState().drawMode).toBe('surface')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a new project starts on whole-surface paint', () => {
|
|
96
|
+
useEditor.getState().armMaterialPaint(undefined, 'polygon')
|
|
97
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
98
|
+
resetPaintMode()
|
|
99
|
+
useEditor.getState().armMaterialPaint()
|
|
100
|
+
expect(sub()).toBe('surface')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('eyedropper sub-mode', () => {
|
|
105
|
+
const key = (type: string, init: KeyboardEventInit) => {
|
|
106
|
+
const event = new Event(type, { cancelable: true }) as KeyboardEvent
|
|
107
|
+
Object.assign(event, { key: init.key, repeat: init.repeat ?? false })
|
|
108
|
+
Object.defineProperty(event, 'target', { value: null })
|
|
109
|
+
return event
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test('the pick icon enters from select and a pick returns to the sub-mode it came from', () => {
|
|
113
|
+
useEditor.getState().armMaterialPaint(undefined, 'rectangle')
|
|
114
|
+
usePaintRegionMode.getState().setMode('pick')
|
|
115
|
+
expect(sub()).toBe('pick')
|
|
116
|
+
expect(paintRegionModeActive(editorMode())).toBe(false)
|
|
117
|
+
expect(paintPickReturnMode()).toBe('rectangle')
|
|
118
|
+
// What the paint path does with a picked material.
|
|
119
|
+
useEditor.getState().armMaterialPaint(TILE, paintPickReturnMode())
|
|
120
|
+
expect(sub()).toBe('rectangle')
|
|
121
|
+
expect(useEditor.getState().activePaintMaterial?.materialPreset).toBe('library:tile')
|
|
122
|
+
|
|
123
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
124
|
+
useEditor.getState().armMaterialPaint(undefined, 'pick')
|
|
125
|
+
expect(editorMode()).toBe('material-paint')
|
|
126
|
+
expect(sub()).toBe('pick')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('picking while erasing returns to painting, not erasing', () => {
|
|
130
|
+
useEditor.getState().armMaterialPaint(undefined, 'polygon')
|
|
131
|
+
usePaintRegionMode.getState().setMode('erase')
|
|
132
|
+
usePaintRegionMode.getState().setMode('pick')
|
|
133
|
+
expect(paintPickReturnMode()).toBe('polygon')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('Alt held while painting picks; releasing, blur or Escape goes back; never while erasing', () => {
|
|
137
|
+
const target = new EventTarget()
|
|
138
|
+
const unbind = bindPaintPickHold(target)
|
|
139
|
+
try {
|
|
140
|
+
useEditor.getState().armMaterialPaint(TILE, 'surface')
|
|
141
|
+
target.dispatchEvent(key('keydown', { key: 'Alt' }))
|
|
142
|
+
expect(sub()).toBe('pick')
|
|
143
|
+
target.dispatchEvent(key('keyup', { key: 'Alt' }))
|
|
144
|
+
expect(sub()).toBe('surface')
|
|
145
|
+
|
|
146
|
+
target.dispatchEvent(key('keydown', { key: 'Alt' }))
|
|
147
|
+
expect(sub()).toBe('pick')
|
|
148
|
+
target.dispatchEvent(new Event('blur'))
|
|
149
|
+
expect(sub()).toBe('surface')
|
|
150
|
+
|
|
151
|
+
target.dispatchEvent(key('keydown', { key: 'Alt' }))
|
|
152
|
+
const escapeKey = key('keydown', { key: 'Escape' })
|
|
153
|
+
target.dispatchEvent(escapeKey)
|
|
154
|
+
expect(escapeKey.defaultPrevented).toBe(true)
|
|
155
|
+
expect(sub()).toBe('surface')
|
|
156
|
+
target.dispatchEvent(key('keyup', { key: 'Alt' }))
|
|
157
|
+
|
|
158
|
+
// Erasing picks nothing: Alt does not turn the eyedropper on.
|
|
159
|
+
usePaintRegionMode.getState().setMode('erase')
|
|
160
|
+
target.dispatchEvent(key('keydown', { key: 'Alt' }))
|
|
161
|
+
expect(sub()).toBe('erase')
|
|
162
|
+
target.dispatchEvent(key('keyup', { key: 'Alt' }))
|
|
163
|
+
|
|
164
|
+
// The region sub-modes keep Alt for free placement.
|
|
165
|
+
usePaintRegionMode.getState().setMode('rectangle')
|
|
166
|
+
target.dispatchEvent(key('keydown', { key: 'Alt' }))
|
|
167
|
+
expect(sub()).toBe('rectangle')
|
|
168
|
+
target.dispatchEvent(key('keyup', { key: 'Alt' }))
|
|
169
|
+
|
|
170
|
+
// A pick taken while held is not undone by the release.
|
|
171
|
+
usePaintRegionMode.getState().setMode('surface')
|
|
172
|
+
target.dispatchEvent(key('keydown', { key: 'Alt' }))
|
|
173
|
+
useEditor.getState().armMaterialPaint(TILE, paintPickReturnMode())
|
|
174
|
+
target.dispatchEvent(key('keyup', { key: 'Alt' }))
|
|
175
|
+
expect(sub()).toBe('surface')
|
|
176
|
+
} finally {
|
|
177
|
+
unbind()
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('leaving paint mode ends the eyedropper', () => {
|
|
182
|
+
useEditor.getState().armMaterialPaint(undefined, 'pick')
|
|
183
|
+
usePaintRegionMode.setState({ picked: TILE })
|
|
184
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
185
|
+
expect(sub()).toBe('surface')
|
|
186
|
+
expect(usePaintRegionMode.getState().picked).toBeNull()
|
|
187
|
+
})
|
|
188
|
+
})
|