@pascal-app/editor 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +663 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +790 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +284 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +230 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +187 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +392 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +122 -0
- package/src/lib/paint-pass-through.ts +78 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +265 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +1017 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +580 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +201 -0
- package/src/store/view-layout.ts +199 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type BuildingNode,
|
|
5
|
+
type CeilingNode,
|
|
6
|
+
derivedFieldViolations,
|
|
7
|
+
emitter,
|
|
8
|
+
isDerivedNode,
|
|
9
|
+
type LevelNode,
|
|
10
|
+
resolveLevelId,
|
|
11
|
+
useScene,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { useEffect } from 'react'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
import { markToolCancelConsumed } from '../hooks/use-keyboard'
|
|
17
|
+
import useEditor from '../store/use-editor'
|
|
18
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
19
|
+
import { selectRoom } from './room-selection-commands'
|
|
20
|
+
import { completeElementAction } from './room-zone-routing'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The explicit "Edit ceiling" session: while it holds a ceiling, and only then,
|
|
24
|
+
* that ceiling shows its corner brackets, polygon editor and height handle.
|
|
25
|
+
*
|
|
26
|
+
* A tiny store rather than an interaction scope: a non-idle scope turns off
|
|
27
|
+
* selection picking and hides every overlay, while the session is a standing
|
|
28
|
+
* mode in which the rest of the scene must stay hoverable and clickable. The
|
|
29
|
+
* polygon drags inside it still run their own `reshaping` scope.
|
|
30
|
+
*/
|
|
31
|
+
export type CeilingEditSession = {
|
|
32
|
+
ceilingId: CeilingNode['id']
|
|
33
|
+
/** The room the ceiling belonged to at start; a polygon edit detaches the link. */
|
|
34
|
+
zoneId: string | null
|
|
35
|
+
levelId: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const useCeilingEditSession = create<{ session: CeilingEditSession | null }>(() => ({
|
|
39
|
+
session: null,
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
/** The ceiling whose edit controls may show, or null outside a session. */
|
|
43
|
+
export function useCeilingEditCeilingId(): string | null {
|
|
44
|
+
return useCeilingEditSession((state) => state.session?.ceilingId ?? null)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getCeilingEditSession(): CeilingEditSession | null {
|
|
48
|
+
return useCeilingEditSession.getState().session
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Nodes = Record<string, AnyNode>
|
|
52
|
+
|
|
53
|
+
function resolveCeiling(nodes: Nodes, target: string): CeilingNode | null {
|
|
54
|
+
const node = nodes[target]
|
|
55
|
+
if (node?.type === 'ceiling') return node
|
|
56
|
+
if (node?.type !== 'zone') return null
|
|
57
|
+
const linked = Object.values(nodes).filter(
|
|
58
|
+
(candidate): candidate is CeilingNode =>
|
|
59
|
+
candidate.type === 'ceiling' && candidate.zoneId === target,
|
|
60
|
+
)
|
|
61
|
+
return (
|
|
62
|
+
linked.find((ceiling) => ceiling.parentId === node.parentId && ceiling.boundary === 'auto') ??
|
|
63
|
+
linked.find((ceiling) => ceiling.parentId === node.parentId) ??
|
|
64
|
+
linked[0] ??
|
|
65
|
+
null
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let stopLifecycle: (() => void) | null = null
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Starts editing the ceiling `target` names: a ceiling id, or a room zone id
|
|
73
|
+
* (its linked ceiling). Selects the ceiling on its level, going inside the
|
|
74
|
+
* building first. Returns false when there is no such ceiling.
|
|
75
|
+
*/
|
|
76
|
+
export function startCeilingEdit(target: string): boolean {
|
|
77
|
+
const nodes = useScene.getState().nodes as Nodes
|
|
78
|
+
const ceiling = resolveCeiling(nodes, target)
|
|
79
|
+
if (!ceiling) return false
|
|
80
|
+
endCeilingEdit()
|
|
81
|
+
|
|
82
|
+
const levelId = resolveLevelId(ceiling, nodes)
|
|
83
|
+
const zone = ceiling.zoneId ? nodes[ceiling.zoneId] : nodes[target]
|
|
84
|
+
const zoneId = zone?.type === 'zone' ? zone.id : null
|
|
85
|
+
if (useEditor.getState().phase === 'site') useEditor.getState().setPhase('building')
|
|
86
|
+
|
|
87
|
+
useCeilingEditSession.setState({ session: { ceilingId: ceiling.id, zoneId, levelId } })
|
|
88
|
+
const level = nodes[levelId]
|
|
89
|
+
const selection = useViewer.getState().selection
|
|
90
|
+
useViewer.getState().setSelection({
|
|
91
|
+
...(level?.parentId && level.parentId !== selection.buildingId
|
|
92
|
+
? { buildingId: level.parentId as BuildingNode['id'] }
|
|
93
|
+
: {}),
|
|
94
|
+
...(levelId !== selection.levelId ? { levelId: levelId as LevelNode['id'] } : {}),
|
|
95
|
+
selectedIds: [ceiling.id],
|
|
96
|
+
})
|
|
97
|
+
stopLifecycle = watchSession()
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The scene-graph entry: a plain tree click that left exactly this ceiling
|
|
103
|
+
* selected opens its session. Modifier multi-selection does not.
|
|
104
|
+
*/
|
|
105
|
+
export function startCeilingEditFromTreeSelection(nodeId: string): boolean {
|
|
106
|
+
const { selectedIds } = useViewer.getState().selection
|
|
107
|
+
if (selectedIds.length !== 1 || selectedIds[0] !== nodeId) return false
|
|
108
|
+
return startCeilingEdit(nodeId)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function endCeilingEdit() {
|
|
112
|
+
stopLifecycle?.()
|
|
113
|
+
stopLifecycle = null
|
|
114
|
+
const session = getCeilingEditSession()
|
|
115
|
+
if (!session) return
|
|
116
|
+
useCeilingEditSession.setState({ session: null })
|
|
117
|
+
// The hole editor mounts only inside the session; a hole scope it leaves behind
|
|
118
|
+
// would keep selection picking off with no editor to end it.
|
|
119
|
+
useInteractionScope
|
|
120
|
+
.getState()
|
|
121
|
+
.endIf(
|
|
122
|
+
(scope) =>
|
|
123
|
+
scope.kind === 'reshaping' &&
|
|
124
|
+
scope.reshape === 'hole' &&
|
|
125
|
+
scope.nodeId === session.ceilingId,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Ends the session with the editor that owns it, so none outlives an unmount. */
|
|
130
|
+
export function useCeilingEditSessionOwner() {
|
|
131
|
+
useEffect(() => endCeilingEdit, [])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Writes a ceiling's holes. Manual holes are authored data the derived-node
|
|
136
|
+
* guard allows on an auto ceiling, so this is a plain update that keeps the
|
|
137
|
+
* room link (only a polygon edit detaches); a write that would change a
|
|
138
|
+
* room-cut hole is refused (returns false).
|
|
139
|
+
*/
|
|
140
|
+
export function setCeilingHoles(
|
|
141
|
+
ceilingId: string,
|
|
142
|
+
patch: Pick<CeilingNode, 'holes'> & Partial<Pick<CeilingNode, 'holeMetadata'>>,
|
|
143
|
+
): boolean {
|
|
144
|
+
const scene = useScene.getState()
|
|
145
|
+
const ceiling = scene.nodes[ceilingId as AnyNodeId]
|
|
146
|
+
if (ceiling?.type !== 'ceiling') return false
|
|
147
|
+
if (isDerivedNode(ceiling) && derivedFieldViolations(ceiling, patch).length > 0) return false
|
|
148
|
+
scene.updateNode(ceilingId as AnyNodeId, patch)
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Escape: leaves the session for the ceiling's room, or for nothing when it has none. */
|
|
153
|
+
export function exitCeilingEditToRoom(): boolean {
|
|
154
|
+
const session = getCeilingEditSession()
|
|
155
|
+
if (!session) return false
|
|
156
|
+
endCeilingEdit()
|
|
157
|
+
const zone = session.zoneId ? useScene.getState().nodes[session.zoneId as AnyNodeId] : undefined
|
|
158
|
+
if (zone?.type !== 'zone') {
|
|
159
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
const room = { levelId: session.levelId, zoneId: zone.id }
|
|
163
|
+
// A room the index no longer knows (its walls just opened) is still where the user came from.
|
|
164
|
+
if (!completeElementAction({ room, point: null })) selectRoom(room)
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function watchSession() {
|
|
169
|
+
const active = () => getCeilingEditSession()
|
|
170
|
+
const stopViewer = useViewer.subscribe((state) => {
|
|
171
|
+
const session = active()
|
|
172
|
+
if (!session) return
|
|
173
|
+
const { selectedIds, levelId } = state.selection
|
|
174
|
+
if (
|
|
175
|
+
levelId !== session.levelId ||
|
|
176
|
+
selectedIds.length !== 1 ||
|
|
177
|
+
selectedIds[0] !== session.ceilingId
|
|
178
|
+
)
|
|
179
|
+
endCeilingEdit()
|
|
180
|
+
})
|
|
181
|
+
const stopEditor = useEditor.subscribe((state, previous) => {
|
|
182
|
+
if (state.phase !== previous.phase) endCeilingEdit()
|
|
183
|
+
})
|
|
184
|
+
const stopScene = useScene.subscribe((state) => {
|
|
185
|
+
const session = active()
|
|
186
|
+
if (session && state.nodes[session.ceilingId as AnyNodeId]?.type !== 'ceiling') endCeilingEdit()
|
|
187
|
+
})
|
|
188
|
+
// A reshape in flight owns Escape (it cancels the drag); only an idle session exits.
|
|
189
|
+
const onToolCancel = () => {
|
|
190
|
+
if (
|
|
191
|
+
!active() ||
|
|
192
|
+
useEditor.getState().mode !== 'select' ||
|
|
193
|
+
useInteractionScope.getState().scope.kind !== 'idle'
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
exitCeilingEditToRoom()
|
|
197
|
+
markToolCancelConsumed()
|
|
198
|
+
}
|
|
199
|
+
emitter.on('tool:cancel', onToolCancel)
|
|
200
|
+
return () => {
|
|
201
|
+
stopViewer()
|
|
202
|
+
stopEditor()
|
|
203
|
+
stopScene()
|
|
204
|
+
emitter.off('tool:cancel', onToolCancel)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -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,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'
|
|
@@ -107,6 +107,7 @@ export function resolveDirectManipulationNode(
|
|
|
107
107
|
): AnyNode {
|
|
108
108
|
const target = nodes[resolveSelectionProxyId(node, nodes)] ?? node
|
|
109
109
|
const parentFrame = nodeRegistry.get(target.type)?.capabilities?.movable?.parentFrame
|
|
110
|
+
if (parentFrame?.independent) return target
|
|
110
111
|
const parent = parentFrame?.resolveParent(target, nodes as Readonly<Record<string, AnyNode>>)
|
|
111
112
|
return parent && canDirectRotateNode(parent) ? parent : target
|
|
112
113
|
}
|
|
@@ -116,6 +117,7 @@ export function resolveMoveActionNode(
|
|
|
116
117
|
nodes: Readonly<Record<string, AnyNode | undefined>>,
|
|
117
118
|
): AnyNode {
|
|
118
119
|
const parentFrame = nodeRegistry.get(node.type)?.capabilities?.movable?.parentFrame
|
|
120
|
+
if (parentFrame?.independent) return node
|
|
119
121
|
const parent = parentFrame?.resolveParent(node, nodes as Readonly<Record<string, AnyNode>>)
|
|
120
122
|
return parent && (parent.type === node.type || canDirectRotateNode(parent)) ? parent : node
|
|
121
123
|
}
|
|
@@ -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,17 @@
|
|
|
1
|
+
import { getProjectFileSink, type ProjectFileSink } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
/** Persist before reporting a successful download; a failed save is shown by the export UI. */
|
|
4
|
+
export async function downloadProjectFile(
|
|
5
|
+
blob: Blob,
|
|
6
|
+
name: string,
|
|
7
|
+
sink: ProjectFileSink | null = getProjectFileSink(),
|
|
8
|
+
) {
|
|
9
|
+
await sink?.({ blob, name })
|
|
10
|
+
if (typeof document === 'undefined') return
|
|
11
|
+
const url = URL.createObjectURL(blob)
|
|
12
|
+
const anchor = document.createElement('a')
|
|
13
|
+
anchor.href = url
|
|
14
|
+
anchor.download = name
|
|
15
|
+
anchor.click()
|
|
16
|
+
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
17
|
+
}
|
|
@@ -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,55 @@
|
|
|
1
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
import useEditor, { type CaptureMode } from '../store/use-editor'
|
|
4
|
+
import { useBelowGrade, useGroundWindowBox } from './below-grade'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether the editor shows every level. Preview and the walkthrough show the
|
|
8
|
+
* whole building; a scene capture shows what its level picker says (the
|
|
9
|
+
* whole building, or a level and the ones below it). A preset capture frames
|
|
10
|
+
* its isolated item and keeps the editing rule.
|
|
11
|
+
*/
|
|
12
|
+
export function showsWholeBuilding(state: {
|
|
13
|
+
isPreviewMode: boolean
|
|
14
|
+
isFirstPersonMode: boolean
|
|
15
|
+
captureMode: CaptureMode
|
|
16
|
+
captureLevelId: string | null
|
|
17
|
+
}): boolean {
|
|
18
|
+
if (state.captureMode.mode === 'standard') return state.captureLevelId === null
|
|
19
|
+
return state.isPreviewMode || state.isFirstPersonMode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The editor's one level display: stacked, with every level above the
|
|
24
|
+
* selected one hidden — the selected level and the ones below stay, so the
|
|
25
|
+
* storey being worked on is never covered. There is no Stack / Exploded /
|
|
26
|
+
* Solo choice while editing; a mode left over from the viewer (or persisted
|
|
27
|
+
* by an older build) is set back to stacked, and kept there.
|
|
28
|
+
*
|
|
29
|
+
* The walkthrough shows the whole building, so the upper levels come back
|
|
30
|
+
* while it is on; Preview hands the display to the viewer's own floor rule
|
|
31
|
+
* (`useViewerFloorDisplay`). A level below grade is marked as shown, and its
|
|
32
|
+
* ground veiled, except on the site, where the ground is what is being worked on.
|
|
33
|
+
*/
|
|
34
|
+
export function useEditorLevelDisplay(showWholeBuilding: boolean) {
|
|
35
|
+
const sitePhase = useEditor((state) => state.phase === 'site')
|
|
36
|
+
useBelowGrade(!(showWholeBuilding || sitePhase))
|
|
37
|
+
// The ground window (a hillside in the eye's way) follows the shown level, and the whole
|
|
38
|
+
// building while it stands whole — except on the site, where the ground is the work.
|
|
39
|
+
useGroundWindowBox(!sitePhase)
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (showWholeBuilding) return
|
|
42
|
+
const keepStacked = (mode: string) => {
|
|
43
|
+
if (mode !== 'stacked') useViewer.getState().setLevelMode('stacked')
|
|
44
|
+
}
|
|
45
|
+
useViewer.setState({ hideLevelsAboveSelection: true })
|
|
46
|
+
keepStacked(useViewer.getState().levelMode)
|
|
47
|
+
const unsubscribe = useViewer.subscribe((state, previous) => {
|
|
48
|
+
if (state.levelMode !== previous.levelMode) keepStacked(state.levelMode)
|
|
49
|
+
})
|
|
50
|
+
return () => {
|
|
51
|
+
unsubscribe()
|
|
52
|
+
useViewer.setState({ hideLevelsAboveSelection: false })
|
|
53
|
+
}
|
|
54
|
+
}, [showWholeBuilding])
|
|
55
|
+
}
|