@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,179 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
area,
|
|
6
|
+
CeilingNode,
|
|
7
|
+
clearSceneHistory,
|
|
8
|
+
LevelNode,
|
|
9
|
+
useScene,
|
|
10
|
+
ZoneNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import useEditor from '../store/use-editor'
|
|
13
|
+
import {
|
|
14
|
+
describeCeilingRegionRoom,
|
|
15
|
+
floorRegionRoomGone,
|
|
16
|
+
moveFloorRegion,
|
|
17
|
+
PICK_MATERIAL_MESSAGE,
|
|
18
|
+
pressFloorRegion,
|
|
19
|
+
releaseFloorRegion,
|
|
20
|
+
useFloorRegionDraft,
|
|
21
|
+
} from './floor-region-session'
|
|
22
|
+
import type { FloorRegionPoint } from './floor-region-snap'
|
|
23
|
+
import { paintRegionHoverHint, usePaintRegionMode } from './paint-region-mode'
|
|
24
|
+
|
|
25
|
+
// Rectangle and Polygon on a ceiling: the floor's outline gesture, drawn on the
|
|
26
|
+
// ceiling's underside, committing one region to the room (an automatic
|
|
27
|
+
// ceiling) or to the ceiling (a manual one) in one undo step.
|
|
28
|
+
|
|
29
|
+
const square = (x: number, z: number, size: number): [number, number][] => [
|
|
30
|
+
[x, z],
|
|
31
|
+
[x + size, z],
|
|
32
|
+
[x + size, z + size],
|
|
33
|
+
[x, z + size],
|
|
34
|
+
]
|
|
35
|
+
const OFF = { mode: 'off', step: 0.5 } as const
|
|
36
|
+
const CLOSE = 0.2
|
|
37
|
+
|
|
38
|
+
const level = LevelNode.parse({ id: 'level_ceiling_region' })
|
|
39
|
+
const zone = ZoneNode.parse({
|
|
40
|
+
id: 'zone_ceiling_region',
|
|
41
|
+
name: 'Room',
|
|
42
|
+
parentId: level.id,
|
|
43
|
+
polygon: square(0, 0, 4),
|
|
44
|
+
floor: { regions: [{ id: 'rug', polygon: square(1, 1, 1), finish: 'library:tile' }] },
|
|
45
|
+
})
|
|
46
|
+
// A floor opening cut the room's ceiling in two; both parts are the room's.
|
|
47
|
+
const west = CeilingNode.parse({
|
|
48
|
+
id: 'ceiling_west',
|
|
49
|
+
parentId: level.id,
|
|
50
|
+
polygon: [
|
|
51
|
+
[0, 0],
|
|
52
|
+
[2, 0],
|
|
53
|
+
[2, 4],
|
|
54
|
+
[0, 4],
|
|
55
|
+
],
|
|
56
|
+
boundary: 'auto',
|
|
57
|
+
zoneId: zone.id,
|
|
58
|
+
height: 2.5,
|
|
59
|
+
})
|
|
60
|
+
const east = CeilingNode.parse({
|
|
61
|
+
...west,
|
|
62
|
+
id: 'ceiling_east',
|
|
63
|
+
polygon: [
|
|
64
|
+
[2.5, 0],
|
|
65
|
+
[4, 0],
|
|
66
|
+
[4, 4],
|
|
67
|
+
[2.5, 4],
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
const manual = CeilingNode.parse({
|
|
71
|
+
id: 'ceiling_manual',
|
|
72
|
+
parentId: level.id,
|
|
73
|
+
polygon: square(6, 0, 2),
|
|
74
|
+
height: 2.2,
|
|
75
|
+
})
|
|
76
|
+
const nodes = Object.fromEntries(
|
|
77
|
+
[level, zone, west, east, manual].map((node) => [node.id, node]),
|
|
78
|
+
) as Record<string, AnyNode>
|
|
79
|
+
|
|
80
|
+
const stubbed: string[] = []
|
|
81
|
+
const before = useScene.getState()
|
|
82
|
+
const node = <T extends AnyNode>(id: string) => useScene.getState().nodes[id as AnyNodeId] as T
|
|
83
|
+
const history = () => useScene.temporal.getState().pastStates.length
|
|
84
|
+
|
|
85
|
+
function drag(ceiling: CeilingNode, from: FloorRegionPoint, to: FloorRegionPoint) {
|
|
86
|
+
const room = describeCeilingRegionRoom(useScene.getState().nodes, ceiling)
|
|
87
|
+
pressFloorRegion('rectangle', room, from, OFF, CLOSE)
|
|
88
|
+
moveFloorRegion(to, OFF, CLOSE)
|
|
89
|
+
releaseFloorRegion(CLOSE)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
globalThis.requestAnimationFrame ??= (callback) => {
|
|
94
|
+
callback(0)
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
globalThis.cancelAnimationFrame ??= () => {}
|
|
98
|
+
if (!globalThis.window) {
|
|
99
|
+
globalThis.window = new EventTarget() as Window & typeof globalThis
|
|
100
|
+
stubbed.push('window')
|
|
101
|
+
}
|
|
102
|
+
useScene.setState({ nodes, materials: {}, readOnly: false })
|
|
103
|
+
useScene.temporal.getState().resume()
|
|
104
|
+
clearSceneHistory()
|
|
105
|
+
useEditor.setState({
|
|
106
|
+
mode: 'material-paint',
|
|
107
|
+
activePaintMaterial: { materialPreset: 'library:blue', sourceTarget: 'ceiling' } as never,
|
|
108
|
+
})
|
|
109
|
+
usePaintRegionMode.getState().setMode('rectangle')
|
|
110
|
+
useFloorRegionDraft.setState({ draft: null, hover: null })
|
|
111
|
+
})
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
useScene.setState(before)
|
|
114
|
+
usePaintRegionMode.getState().setMode('surface')
|
|
115
|
+
useFloorRegionDraft.setState({ draft: null, hover: null })
|
|
116
|
+
useEditor.setState({ mode: 'select', activePaintMaterial: null })
|
|
117
|
+
})
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
120
|
+
for (const key of stubbed.splice(0)) delete (globalThis as Record<string, unknown>)[key]
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('an automatic ceiling draws on every part of its room, at the plane it draws at', () => {
|
|
124
|
+
const room = describeCeilingRegionRoom(nodes, west)
|
|
125
|
+
expect(room).toMatchObject({ zoneId: zone.id, ceilingId: west.id, levelId: level.id })
|
|
126
|
+
expect(room.elevation).toBeCloseTo(2.49)
|
|
127
|
+
expect(area(room.clear)).toBeCloseTo(8 + 6)
|
|
128
|
+
expect(describeCeilingRegionRoom(nodes, manual)).toMatchObject({ zoneId: manual.id })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('a rectangle on a room ceiling paints one room region, clipped to the ceiling', () => {
|
|
132
|
+
drag(west, [1, 1], [3, 3])
|
|
133
|
+
const regions = node<ZoneNode>(zone.id).ceiling?.regions ?? []
|
|
134
|
+
expect(regions).toHaveLength(1)
|
|
135
|
+
expect(regions[0]!.finish).toBe('library:blue')
|
|
136
|
+
// Across the opening gap: 1 × 2 on the west part, 0.5 × 2 on the east part.
|
|
137
|
+
expect(area([{ outer: regions[0]!.polygon, holes: [] }])).toBeGreaterThan(2.9)
|
|
138
|
+
expect(history()).toBe(1)
|
|
139
|
+
// The floor is untouched.
|
|
140
|
+
expect(node<ZoneNode>(zone.id).floor).toEqual(zone.floor)
|
|
141
|
+
expect(useFloorRegionDraft.getState().draft).toBeNull()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a polygon on a manual ceiling paints the ceiling itself', () => {
|
|
145
|
+
usePaintRegionMode.getState().setMode('polygon')
|
|
146
|
+
const room = describeCeilingRegionRoom(useScene.getState().nodes, manual)
|
|
147
|
+
for (const point of [
|
|
148
|
+
[6.2, 0.2],
|
|
149
|
+
[7.5, 0.2],
|
|
150
|
+
[7.5, 1.5],
|
|
151
|
+
[6.2, 0.2],
|
|
152
|
+
] as FloorRegionPoint[]) {
|
|
153
|
+
pressFloorRegion('polygon', room, point, OFF, CLOSE)
|
|
154
|
+
releaseFloorRegion(CLOSE)
|
|
155
|
+
}
|
|
156
|
+
expect(node<CeilingNode>(manual.id).regions).toHaveLength(1)
|
|
157
|
+
expect(node<ZoneNode>(zone.id).ceiling).toBeUndefined()
|
|
158
|
+
expect(history()).toBe(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('outside the ceiling, or with nothing to paint with, nothing is written', () => {
|
|
162
|
+
drag(manual, [10, 10], [12, 12])
|
|
163
|
+
expect(node<CeilingNode>(manual.id).regions).toBeUndefined()
|
|
164
|
+
useEditor.setState({ activePaintMaterial: null })
|
|
165
|
+
drag(manual, [6.5, 0.5], [7.5, 1.5])
|
|
166
|
+
expect(node<CeilingNode>(manual.id).regions).toBeUndefined()
|
|
167
|
+
expect(usePaintRegionMode.getState().notice).toBe(PICK_MATERIAL_MESSAGE)
|
|
168
|
+
expect(history()).toBe(0)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('a ceiling draft ends when its ceiling goes; the hints name ceilings', () => {
|
|
172
|
+
const room = describeCeilingRegionRoom(nodes, manual)
|
|
173
|
+
pressFloorRegion('rectangle', room, [6.5, 0.5], OFF, CLOSE)
|
|
174
|
+
const { [manual.id]: _, ...rest } = nodes
|
|
175
|
+
expect(floorRegionRoomGone(rest)).toBe(true)
|
|
176
|
+
expect(floorRegionRoomGone(nodes)).toBe(false)
|
|
177
|
+
expect(paintRegionHoverHint('polygon')).toBe('Hover a floor or ceiling')
|
|
178
|
+
expect(paintRegionHoverHint('rectangle')).toBe('Hover a wall, floor or ceiling')
|
|
179
|
+
})
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { resolveSelectModeHelpHints } from './contextual-help'
|
|
2
|
+
import { resolveRotateHandleHelpHints, resolveSelectModeHelpHints } from './contextual-help'
|
|
3
|
+
|
|
4
|
+
test('rotation advertises Alt as the temporary free-rotation override', () => {
|
|
5
|
+
expect(resolveRotateHandleHelpHints(false)).toEqual([
|
|
6
|
+
{ keys: ['Alt'], label: 'Hold to rotate freely', active: false },
|
|
7
|
+
])
|
|
8
|
+
expect(resolveRotateHandleHelpHints(true)).toEqual([
|
|
9
|
+
{ keys: ['Alt'], label: 'Rotating freely (no angle step)', active: true },
|
|
10
|
+
])
|
|
11
|
+
})
|
|
3
12
|
|
|
4
13
|
describe('resolveSelectModeHelpHints', () => {
|
|
5
14
|
test('stays hidden in idle select mode with no selection', () => {
|
|
@@ -117,4 +126,21 @@ describe('resolveSelectModeHelpHints', () => {
|
|
|
117
126
|
},
|
|
118
127
|
])
|
|
119
128
|
})
|
|
129
|
+
|
|
130
|
+
test('shows the single-side radius hint for rounded openings', () => {
|
|
131
|
+
expect(
|
|
132
|
+
resolveSelectModeHelpHints({
|
|
133
|
+
selectedCount: 1,
|
|
134
|
+
hasMovableSelection: true,
|
|
135
|
+
hasRotatableSelection: false,
|
|
136
|
+
hasOpeningRadiusSelection: true,
|
|
137
|
+
commandPressed: false,
|
|
138
|
+
shiftPressed: false,
|
|
139
|
+
}),
|
|
140
|
+
).toContainEqual({
|
|
141
|
+
keys: ['Shift'],
|
|
142
|
+
label: 'Adjust one side’s corner radius',
|
|
143
|
+
active: false,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
120
146
|
})
|
|
@@ -11,8 +11,8 @@ export type ContextualShortcutHint = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
|
|
14
|
-
// contextual HUD can surface the
|
|
15
|
-
//
|
|
14
|
+
// contextual HUD can surface the rotation snapping mode for the duration.
|
|
15
|
+
// Distinct from resize handles,
|
|
16
16
|
// which route their own measurement label here.
|
|
17
17
|
export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
|
|
18
18
|
|
|
@@ -29,15 +29,12 @@ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
|
|
|
29
29
|
export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
|
|
30
30
|
export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
// (free rotation), the same toggle wall drafting exposes. `active` lights the
|
|
34
|
-
// pill while Shift is held.
|
|
35
|
-
export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
|
|
32
|
+
export function resolveRotateHandleHelpHints(altPressed: boolean): ContextualShortcutHint[] {
|
|
36
33
|
return [
|
|
37
34
|
{
|
|
38
|
-
keys: [
|
|
39
|
-
label:
|
|
40
|
-
active:
|
|
35
|
+
keys: [ALT_KEY],
|
|
36
|
+
label: altPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
|
|
37
|
+
active: altPressed,
|
|
41
38
|
},
|
|
42
39
|
]
|
|
43
40
|
}
|
|
@@ -46,6 +43,7 @@ export type SelectModeHelpContext = {
|
|
|
46
43
|
selectedCount: number
|
|
47
44
|
hasMovableSelection: boolean
|
|
48
45
|
hasRotatableSelection: boolean
|
|
46
|
+
hasOpeningRadiusSelection?: boolean
|
|
49
47
|
commandPressed: boolean
|
|
50
48
|
shiftPressed: boolean
|
|
51
49
|
// When a single MEP node is selected its in-world handle rig (click a dot to
|
|
@@ -67,6 +65,7 @@ export function resolveSelectModeHelpHints({
|
|
|
67
65
|
selectedCount,
|
|
68
66
|
hasMovableSelection,
|
|
69
67
|
hasRotatableSelection,
|
|
68
|
+
hasOpeningRadiusSelection = false,
|
|
70
69
|
commandPressed,
|
|
71
70
|
shiftPressed,
|
|
72
71
|
mepSelection = null,
|
|
@@ -142,6 +141,14 @@ export function resolveSelectModeHelpHints({
|
|
|
142
141
|
})
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
if (hasOpeningRadiusSelection) {
|
|
145
|
+
hints.push({
|
|
146
|
+
keys: [SHIFT_KEY],
|
|
147
|
+
label: 'Adjust one side’s corner radius',
|
|
148
|
+
active: shiftPressed,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
145
152
|
hints.push({
|
|
146
153
|
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
|
|
147
154
|
label: 'Add or remove objects from the selection',
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CONTINUATION_PROFILES,
|
|
4
|
+
continuationContextOf,
|
|
5
|
+
keyCyclableContinuationContext,
|
|
6
|
+
nextContinuation,
|
|
7
|
+
} from './continuation'
|
|
3
8
|
|
|
4
9
|
describe('canopy continuation', () => {
|
|
5
10
|
test('maps the canopy tool to its own single and continuous profile', () => {
|
|
@@ -9,3 +14,16 @@ describe('canopy continuation', () => {
|
|
|
9
14
|
expect(nextContinuation('canopy', 'continuous')).toBe('single')
|
|
10
15
|
})
|
|
11
16
|
})
|
|
17
|
+
|
|
18
|
+
describe('wall drawing mode', () => {
|
|
19
|
+
test('is picked in the Build panel, never cycled by C or a HUD chip', () => {
|
|
20
|
+
expect(CONTINUATION_PROFILES.wall.options).toEqual(['room', 'single', 'rectangle'])
|
|
21
|
+
expect(keyCyclableContinuationContext('wall')).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('other contexts still cycle from the keyboard', () => {
|
|
25
|
+
expect(keyCyclableContinuationContext('fence')).toBe('fence')
|
|
26
|
+
expect(keyCyclableContinuationContext('point')).toBe('point')
|
|
27
|
+
expect(keyCyclableContinuationContext(null)).toBeNull()
|
|
28
|
+
})
|
|
29
|
+
})
|
package/src/lib/continuation.ts
CHANGED
|
@@ -8,26 +8,37 @@ export const CONTINUATION_PROFILES: Record<
|
|
|
8
8
|
default: ContinuationMode
|
|
9
9
|
labels: Record<string, string>
|
|
10
10
|
icons: Record<string, string>
|
|
11
|
+
/**
|
|
12
|
+
* Picked once from the Build panel before drawing (`wall-draw-variant.ts`),
|
|
13
|
+
* never cycled by C or a HUD chip mid-draw.
|
|
14
|
+
*/
|
|
15
|
+
chosenInPanel?: boolean
|
|
11
16
|
}
|
|
12
17
|
> = {
|
|
18
|
+
// The wall tool's drawing mode: a chain that closes into a room ('room', the
|
|
19
|
+
// Build panel's Polygon), one wall per two clicks ('single', Walls), or a
|
|
20
|
+
// two-corner box of four walls ('rectangle', Rectangle).
|
|
13
21
|
wall: {
|
|
14
|
-
options: ['room', 'single'],
|
|
22
|
+
options: ['room', 'single', 'rectangle'],
|
|
15
23
|
default: 'room',
|
|
16
|
-
labels: { room: '
|
|
17
|
-
icons: { room: 'lucide:
|
|
24
|
+
labels: { room: 'Polygon room', single: 'Walls', rectangle: 'Rectangle room' },
|
|
25
|
+
icons: { room: 'lucide:pentagon', single: 'lucide:minus', rectangle: 'lucide:square' },
|
|
26
|
+
chosenInPanel: true,
|
|
18
27
|
},
|
|
19
28
|
fence: {
|
|
20
|
-
options: ['single', 'continuous', 'curved'],
|
|
29
|
+
options: ['single', 'continuous', 'curved', 'freehand'],
|
|
21
30
|
default: 'continuous',
|
|
22
31
|
labels: {
|
|
23
32
|
continuous: 'Continuous',
|
|
24
33
|
single: 'Single fence',
|
|
25
34
|
curved: 'Curved fence',
|
|
35
|
+
freehand: 'Freehand fence',
|
|
26
36
|
},
|
|
27
37
|
icons: {
|
|
28
38
|
continuous: 'lucide:waypoints',
|
|
29
39
|
single: 'lucide:minus',
|
|
30
40
|
curved: 'lucide:spline',
|
|
41
|
+
freehand: 'lucide:scribble',
|
|
31
42
|
},
|
|
32
43
|
},
|
|
33
44
|
point: {
|
|
@@ -62,6 +73,16 @@ export function nextContinuation(
|
|
|
62
73
|
return profile.options[(index + 1) % profile.options.length] ?? profile.default
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The context C and the HUD chip may cycle — null when there is none, or when
|
|
78
|
+
* its mode is chosen in the Build panel instead.
|
|
79
|
+
*/
|
|
80
|
+
export function keyCyclableContinuationContext(
|
|
81
|
+
context: ContinuationContext | null,
|
|
82
|
+
): ContinuationContext | null {
|
|
83
|
+
return context && !CONTINUATION_PROFILES[context].chosenInPanel ? context : null
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
export function continuationContextOf(kind: string): ContinuationContext | null {
|
|
66
87
|
if (kind === 'wall') return 'wall'
|
|
67
88
|
if (kind === 'fence') return 'fence'
|
|
@@ -86,3 +86,17 @@ export function closeDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimatio
|
|
|
86
86
|
const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
|
|
87
87
|
startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, 0, options)
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
export function openDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimationOptions) {
|
|
91
|
+
const node = useScene.getState().nodes[doorId]
|
|
92
|
+
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
|
93
|
+
|
|
94
|
+
if (isOperationDoorType(node.doorType)) {
|
|
95
|
+
const currentOpenAmount = getDisplayedDoorValue(doorId, 'operationState', node.operationState)
|
|
96
|
+
startDoorOpenAnimation(doorId, 'operationState', currentOpenAmount, 1, options)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
|
|
101
|
+
startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, DOOR_SWING_OPEN_ANGLE, options)
|
|
102
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { BuildingNode, LevelNode, useScene } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import useEditor, { type CaptureMode } from '../store/use-editor'
|
|
5
|
+
import { showsWholeBuilding } from './editor-level-display'
|
|
6
|
+
|
|
7
|
+
const building = BuildingNode.parse({ id: 'building_capture', children: [] })
|
|
8
|
+
const levels = [0, 1, 2].map((level) =>
|
|
9
|
+
LevelNode.parse({ id: `level_capture_${level}`, level, parentId: building.id }),
|
|
10
|
+
)
|
|
11
|
+
const [ground, first, second] = levels.map((level) => level.id) as [
|
|
12
|
+
LevelNode['id'],
|
|
13
|
+
LevelNode['id'],
|
|
14
|
+
LevelNode['id'],
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const wholeBuilding = () => {
|
|
18
|
+
const state = useEditor.getState()
|
|
19
|
+
return showsWholeBuilding({
|
|
20
|
+
isPreviewMode: state.isPreviewMode,
|
|
21
|
+
isFirstPersonMode: state.isFirstPersonMode,
|
|
22
|
+
captureMode: state.captureMode,
|
|
23
|
+
captureLevelId: state.captureLevelId,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
const activeLevel = () => useViewer.getState().selection.levelId
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
useScene.setState({
|
|
30
|
+
nodes: Object.fromEntries(
|
|
31
|
+
[{ ...building, children: levels.map((level) => level.id) }, ...levels].map((node) => [
|
|
32
|
+
node.id,
|
|
33
|
+
node,
|
|
34
|
+
]),
|
|
35
|
+
),
|
|
36
|
+
rootNodeIds: [building.id],
|
|
37
|
+
} as never)
|
|
38
|
+
useViewer.getState().setSelection({ buildingId: building.id, levelId: second })
|
|
39
|
+
})
|
|
40
|
+
afterEach(() => useEditor.getState().setCaptureMode(false))
|
|
41
|
+
|
|
42
|
+
describe('capture level picker', () => {
|
|
43
|
+
test('editing hides the levels above; capture opens on the whole building', () => {
|
|
44
|
+
expect(wholeBuilding()).toBe(false)
|
|
45
|
+
useEditor.getState().setCaptureMode(true)
|
|
46
|
+
expect(useEditor.getState().captureLevelId).toBeNull()
|
|
47
|
+
expect(wholeBuilding()).toBe(true)
|
|
48
|
+
expect(activeLevel()).toBe(second)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('picking a level shows it and the levels below, as while editing', () => {
|
|
52
|
+
useEditor.getState().setCaptureMode(true)
|
|
53
|
+
useEditor.getState().setCaptureLevel(first)
|
|
54
|
+
expect(wholeBuilding()).toBe(false)
|
|
55
|
+
expect(activeLevel()).toBe(first)
|
|
56
|
+
useEditor.getState().setCaptureLevel(null)
|
|
57
|
+
expect(wholeBuilding()).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("leaving capture restores the editor's active level and the editing rule", () => {
|
|
61
|
+
useEditor.getState().setCaptureMode(true)
|
|
62
|
+
useEditor.getState().setCaptureLevel(ground)
|
|
63
|
+
useEditor.getState().setCaptureMode(false)
|
|
64
|
+
expect(activeLevel()).toBe(second)
|
|
65
|
+
expect(useEditor.getState().captureLevelId).toBeNull()
|
|
66
|
+
expect(wholeBuilding()).toBe(false)
|
|
67
|
+
// The next capture opens on the whole building again.
|
|
68
|
+
useEditor.getState().setCaptureMode({ mode: 'standard', crop: 'area' })
|
|
69
|
+
expect(wholeBuilding()).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('a level picked outside capture is ignored', () => {
|
|
73
|
+
useEditor.getState().setCaptureLevel(ground)
|
|
74
|
+
expect(activeLevel()).toBe(second)
|
|
75
|
+
expect(useEditor.getState().captureLevelId).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('preset captures and the walkthrough keep their own rules', () => {
|
|
79
|
+
const preset: CaptureMode = { mode: 'preset', isolated: [] }
|
|
80
|
+
const base = { isPreviewMode: false, isFirstPersonMode: false, captureLevelId: null }
|
|
81
|
+
expect(showsWholeBuilding({ ...base, captureMode: preset })).toBe(false)
|
|
82
|
+
expect(showsWholeBuilding({ ...base, captureMode: { mode: 'idle' } })).toBe(false)
|
|
83
|
+
expect(
|
|
84
|
+
showsWholeBuilding({ ...base, isFirstPersonMode: true, captureMode: { mode: 'idle' } }),
|
|
85
|
+
).toBe(true)
|
|
86
|
+
// A walk capture on a picked level still shows only that level and below.
|
|
87
|
+
expect(
|
|
88
|
+
showsWholeBuilding({
|
|
89
|
+
...base,
|
|
90
|
+
isFirstPersonMode: true,
|
|
91
|
+
captureMode: { mode: 'standard' },
|
|
92
|
+
captureLevelId: first,
|
|
93
|
+
}),
|
|
94
|
+
).toBe(false)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
import type { CaptureMode } from '../store/use-editor'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether the editor shows every level. Preview and the walkthrough show the
|
|
7
|
+
* whole building; a scene capture shows what its level picker says (the
|
|
8
|
+
* whole building, or a level and the ones below it). A preset capture frames
|
|
9
|
+
* its isolated item and keeps the editing rule.
|
|
10
|
+
*/
|
|
11
|
+
export function showsWholeBuilding(state: {
|
|
12
|
+
isPreviewMode: boolean
|
|
13
|
+
isFirstPersonMode: boolean
|
|
14
|
+
captureMode: CaptureMode
|
|
15
|
+
captureLevelId: string | null
|
|
16
|
+
}): boolean {
|
|
17
|
+
if (state.captureMode.mode === 'standard') return state.captureLevelId === null
|
|
18
|
+
return state.isPreviewMode || state.isFirstPersonMode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The editor's one level display: stacked, with every level above the
|
|
23
|
+
* selected one hidden — the selected level and the ones below stay, so the
|
|
24
|
+
* storey being worked on is never covered. There is no Stack / Exploded /
|
|
25
|
+
* Solo choice while editing; a mode left over from the viewer (or persisted
|
|
26
|
+
* by an older build) is set back to stacked, and kept there.
|
|
27
|
+
*
|
|
28
|
+
* The walkthrough shows the whole building, so the upper levels come back
|
|
29
|
+
* while it is on; Preview hands the display to the viewer's own floor rule
|
|
30
|
+
* (`useViewerFloorDisplay`).
|
|
31
|
+
*/
|
|
32
|
+
export function useEditorLevelDisplay(showWholeBuilding: boolean) {
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (showWholeBuilding) return
|
|
35
|
+
const keepStacked = (mode: string) => {
|
|
36
|
+
if (mode !== 'stacked') useViewer.getState().setLevelMode('stacked')
|
|
37
|
+
}
|
|
38
|
+
useViewer.setState({ hideLevelsAboveSelection: true })
|
|
39
|
+
keepStacked(useViewer.getState().levelMode)
|
|
40
|
+
const unsubscribe = useViewer.subscribe((state, previous) => {
|
|
41
|
+
if (state.levelMode !== previous.levelMode) keepStacked(state.levelMode)
|
|
42
|
+
})
|
|
43
|
+
return () => {
|
|
44
|
+
unsubscribe()
|
|
45
|
+
useViewer.setState({ hideLevelsAboveSelection: false })
|
|
46
|
+
}
|
|
47
|
+
}, [showWholeBuilding])
|
|
48
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { IconRef, LazyComponent } from '@pascal-app/core'
|
|
2
|
+
import useEditor from '../store/use-editor'
|
|
3
|
+
import { VIEW_2D, VIEW_3D } from '../store/view-layout'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A view fills a pane of the stage, beside or instead of the 3D scene and the
|
|
7
|
+
* 2D plan. Hosts and plugins register views the way they register rail panels;
|
|
8
|
+
* a plugin registers as many as it needs, each tagged with its `pluginId`.
|
|
9
|
+
*/
|
|
10
|
+
export type EditorHostView = {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
icon: IconRef
|
|
14
|
+
component: LazyComponent
|
|
15
|
+
/** The view exists in a project only while this plugin is installed there. */
|
|
16
|
+
pluginId?: string
|
|
17
|
+
/** Workspaces the view is offered in. Default `['edit']`, like panels. */
|
|
18
|
+
workspaces?: readonly string[]
|
|
19
|
+
description?: string
|
|
20
|
+
/** Shown in the view bar until the user unpins it. Default false: the "More views" menu only. */
|
|
21
|
+
defaultPinned?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A view as the stage needs it. The scene views have no component: the stage owns their canvases. */
|
|
25
|
+
export type EditorViewDescriptor = Omit<EditorHostView, 'component'> & {
|
|
26
|
+
component?: LazyComponent
|
|
27
|
+
/** 3D and 2D: the views the level selector, tool dock and scene overlays belong to. */
|
|
28
|
+
scene?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SCENE_VIEWS: readonly EditorViewDescriptor[] = [
|
|
32
|
+
{
|
|
33
|
+
id: VIEW_3D,
|
|
34
|
+
label: '3D',
|
|
35
|
+
icon: { kind: 'url', src: '/icons/building.webp' },
|
|
36
|
+
scene: true,
|
|
37
|
+
defaultPinned: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: VIEW_2D,
|
|
41
|
+
label: '2D',
|
|
42
|
+
icon: { kind: 'url', src: '/icons/blueprint.webp' },
|
|
43
|
+
scene: true,
|
|
44
|
+
defaultPinned: true,
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function isSceneView(viewId: string): boolean {
|
|
49
|
+
return viewId === VIEW_3D || viewId === VIEW_2D
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isDevMode(): boolean {
|
|
53
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV) {
|
|
54
|
+
return process.env.NODE_ENV !== 'production'
|
|
55
|
+
}
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class EditorHostViewRegistryImpl {
|
|
60
|
+
private readonly views = new Map<string, EditorHostView>()
|
|
61
|
+
private readonly listeners = new Set<() => void>()
|
|
62
|
+
private cached: EditorHostView[] = []
|
|
63
|
+
|
|
64
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
65
|
+
this.listeners.add(onChange)
|
|
66
|
+
return () => {
|
|
67
|
+
this.listeners.delete(onChange)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getSnapshot = (): EditorHostView[] => this.cached
|
|
72
|
+
|
|
73
|
+
reset(): void {
|
|
74
|
+
this.views.clear()
|
|
75
|
+
this.emit()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
registerView(view: EditorHostView): void {
|
|
79
|
+
if (typeof view.id !== 'string' || view.id.length === 0) {
|
|
80
|
+
throw new Error('[editor:host-views] view id must be a non-empty string')
|
|
81
|
+
}
|
|
82
|
+
if (isSceneView(view.id)) {
|
|
83
|
+
throw new Error(`[editor:host-views] "${view.id}" is a built-in view`)
|
|
84
|
+
}
|
|
85
|
+
if (this.views.has(view.id)) {
|
|
86
|
+
if (isDevMode()) {
|
|
87
|
+
console.warn(`[editor:host-views] re-registering view "${view.id}" (HMR)`)
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error(`[editor:host-views] duplicate view id: "${view.id}" already registered`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.views.set(view.id, view)
|
|
93
|
+
this.emit()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private emit(): void {
|
|
97
|
+
this.cached = Array.from(this.views.values())
|
|
98
|
+
for (const listener of this.listeners) listener()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const editorHostViewRegistry = new EditorHostViewRegistryImpl()
|
|
103
|
+
|
|
104
|
+
export function registerEditorHostView(view: EditorHostView): void {
|
|
105
|
+
editorHostViewRegistry.registerView(view)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Show a view: focus it if it is on screen, else put it in the focused pane —
|
|
110
|
+
* or, with `beside`, split and open it next to what is there.
|
|
111
|
+
*/
|
|
112
|
+
export function openEditorView(viewId: string, options?: { beside?: boolean }): void {
|
|
113
|
+
useEditor.getState().showView(viewId, options)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The views a project can show in a workspace: the scene views, then every
|
|
118
|
+
* registered view offered there whose plugin (if any) is installed.
|
|
119
|
+
*/
|
|
120
|
+
export function availableViews({
|
|
121
|
+
registered,
|
|
122
|
+
workspaceMode,
|
|
123
|
+
installedPlugins,
|
|
124
|
+
}: {
|
|
125
|
+
registered: readonly EditorViewDescriptor[]
|
|
126
|
+
workspaceMode: string
|
|
127
|
+
installedPlugins: readonly string[]
|
|
128
|
+
}): EditorViewDescriptor[] {
|
|
129
|
+
return [
|
|
130
|
+
...SCENE_VIEWS,
|
|
131
|
+
...registered.filter(
|
|
132
|
+
(view) =>
|
|
133
|
+
(view.workspaces ?? ['edit']).includes(workspaceMode) &&
|
|
134
|
+
(!view.pluginId || installedPlugins.includes(view.pluginId)),
|
|
135
|
+
),
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isViewPinned(
|
|
140
|
+
view: EditorViewDescriptor,
|
|
141
|
+
pinnedViews: Readonly<Record<string, boolean>>,
|
|
142
|
+
): boolean {
|
|
143
|
+
return pinnedViews[view.id] ?? view.defaultPinned ?? false
|
|
144
|
+
}
|