@pascal-app/editor 1.0.3 → 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 +11 -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 +202 -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 +36 -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 +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -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 +269 -197
- package/src/components/editor/index.tsx +294 -169
- 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/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 +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 +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- 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-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- 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 +291 -227
- 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 +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- 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 +93 -46
- 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/move-registry-node-tool.tsx +3 -1
- 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/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- 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 +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- 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/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 +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- 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 +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-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 +4 -4
- 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/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/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- 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.tsx +5 -6
- 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 +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- 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 +246 -39
- 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/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 +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 +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 +59 -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 +485 -0
- package/src/lib/glb-export.ts +237 -119
- 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 +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- 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 +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- 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/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 +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- 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 +24 -36
- package/src/lib/scene.ts +33 -57
- 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 +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -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/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-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-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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
DoorNode,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
registerNode,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import * as THREE from 'three'
|
|
11
|
+
import { nodesAwaitingExportGeometry, waitForExportGeometry } from './glb-export'
|
|
12
|
+
|
|
13
|
+
const node = (id: string, type: string, fields: Record<string, unknown> = {}) =>
|
|
14
|
+
({
|
|
15
|
+
object: 'node',
|
|
16
|
+
id,
|
|
17
|
+
type,
|
|
18
|
+
parentId: 'level_a',
|
|
19
|
+
visible: true,
|
|
20
|
+
metadata: {},
|
|
21
|
+
...fields,
|
|
22
|
+
}) as unknown as AnyNode
|
|
23
|
+
|
|
24
|
+
const definition = (kind: string, extra: Record<string, unknown> = {}) =>
|
|
25
|
+
({
|
|
26
|
+
kind,
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
schema: DoorNode,
|
|
29
|
+
category: 'utility',
|
|
30
|
+
defaults: () => ({}) as never,
|
|
31
|
+
capabilities: {},
|
|
32
|
+
bake: 'replace',
|
|
33
|
+
...extra,
|
|
34
|
+
}) as AnyNodeDefinition
|
|
35
|
+
|
|
36
|
+
let restoreRegistry: () => void
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
40
|
+
registerNode(definition('test:plant'))
|
|
41
|
+
registerNode(definition('test:baked', { bakeGeometry: () => new THREE.Group() }))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
restoreRegistry()
|
|
46
|
+
sceneRegistry.clear()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Live objects as the editor registers them: still empty, i.e. the export
|
|
50
|
+
// geometry has not mounted.
|
|
51
|
+
function mount(ids: string[]) {
|
|
52
|
+
for (const id of ids) sceneRegistry.nodes.set(id, new THREE.Group())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('waitForExportGeometry', () => {
|
|
56
|
+
const nodes: Record<string, AnyNode> = {
|
|
57
|
+
level_a: node('level_a', 'level', { parentId: null }),
|
|
58
|
+
plant_a: node('plant_a', 'test:plant'),
|
|
59
|
+
plant_hidden: node('plant_hidden', 'test:plant', { visible: false }),
|
|
60
|
+
baked_a: node('baked_a', 'test:baked'),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('waits only for included collective kinds that mount their own geometry', () => {
|
|
64
|
+
mount(['plant_a', 'plant_hidden', 'baked_a'])
|
|
65
|
+
expect(nodesAwaitingExportGeometry(nodes)).toEqual(['plant_a'])
|
|
66
|
+
expect(nodesAwaitingExportGeometry(nodes, { onlyVisible: false })).toEqual([
|
|
67
|
+
'plant_a',
|
|
68
|
+
'plant_hidden',
|
|
69
|
+
])
|
|
70
|
+
expect(nodesAwaitingExportGeometry(nodes, { excludedNodeTypes: ['test:plant'] })).toEqual([])
|
|
71
|
+
sceneRegistry.nodes.get('plant_a')!.add(new THREE.Mesh())
|
|
72
|
+
expect(nodesAwaitingExportGeometry(nodes)).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('times out on its own timer when geometry never mounts and frames never run', async () => {
|
|
76
|
+
mount(['plant_a'])
|
|
77
|
+
const started = Date.now()
|
|
78
|
+
await waitForExportGeometry(nodes, {}, 300)
|
|
79
|
+
const elapsed = Date.now() - started
|
|
80
|
+
expect(elapsed).toBeGreaterThanOrEqual(280)
|
|
81
|
+
expect(elapsed).toBeLessThan(1500)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('returns as soon as nothing is pending', async () => {
|
|
85
|
+
mount(['plant_a', 'baked_a'])
|
|
86
|
+
const started = Date.now()
|
|
87
|
+
await waitForExportGeometry(nodes, { excludedNodeTypes: ['test:plant'] }, 5_000)
|
|
88
|
+
expect(Date.now() - started).toBeLessThan(1_000)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
area,
|
|
4
|
+
emitter,
|
|
5
|
+
type FloorOpeningNode,
|
|
6
|
+
intersection,
|
|
7
|
+
openingsForSurface,
|
|
8
|
+
removeFloorOpening,
|
|
9
|
+
structureChangeBatch,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { create } from 'zustand'
|
|
14
|
+
import { markToolCancelConsumed } from '../hooks/use-keyboard'
|
|
15
|
+
import useEditor from '../store/use-editor'
|
|
16
|
+
import { cancelOpeningDraft } from './floor-opening-draft'
|
|
17
|
+
import { completeElementAction } from './room-zone-routing'
|
|
18
|
+
|
|
19
|
+
// "Edit floor": the room's floor openings, outlined in 3D and on the plan,
|
|
20
|
+
// each a click away, with "Cut opening" to draw another. Like "Edit ceiling"
|
|
21
|
+
// it is a standing mode rather than an interaction scope, so the scene stays
|
|
22
|
+
// clickable; it ends with Done or Escape (back to the room), when the room is
|
|
23
|
+
// deselected, or when another room is picked.
|
|
24
|
+
|
|
25
|
+
export type FloorEditSession = { zoneId: string; levelId: string }
|
|
26
|
+
|
|
27
|
+
export const useFloorEditSession = create<{ session: FloorEditSession | null }>(() => ({
|
|
28
|
+
session: null,
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
type Nodes = Readonly<Record<string, AnyNode>>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The openings through one surface of a room: those whose cut reaches that
|
|
35
|
+
* surface on the room's level (a floor opening here, a ceiling opening from
|
|
36
|
+
* the storey below) and overlap the room's outline.
|
|
37
|
+
*/
|
|
38
|
+
export function roomSurfaceOpenings(
|
|
39
|
+
nodes: Nodes,
|
|
40
|
+
zoneId: string,
|
|
41
|
+
surface: 'floor' | 'ceiling',
|
|
42
|
+
): FloorOpeningNode[] {
|
|
43
|
+
const zone = nodes[zoneId]
|
|
44
|
+
if (zone?.type !== 'zone' || !zone.parentId) return []
|
|
45
|
+
const outline = { outer: zone.polygon, holes: zone.holes ?? [] }
|
|
46
|
+
const mezzanine = zone.floor?.support === 'open'
|
|
47
|
+
return openingsForSurface(nodes, zone.parentId, surface).filter((opening) =>
|
|
48
|
+
mezzanine
|
|
49
|
+
? opening.hostZoneId === zoneId
|
|
50
|
+
: !opening.hostZoneId && area(intersection(opening.polygon, outline)) > 1e-6,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let stop: (() => void) | null = null
|
|
55
|
+
|
|
56
|
+
export function startFloorEdit(zoneId: string): boolean {
|
|
57
|
+
const zone =
|
|
58
|
+
useScene.getState().nodes[zoneId as keyof ReturnType<typeof useScene.getState>['nodes']]
|
|
59
|
+
if (zone?.type !== 'zone' || !zone.parentId) return false
|
|
60
|
+
endFloorEdit()
|
|
61
|
+
useFloorEditSession.setState({ session: { zoneId, levelId: zone.parentId } })
|
|
62
|
+
stop = watch(zoneId)
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function endFloorEdit() {
|
|
67
|
+
stop?.()
|
|
68
|
+
stop = null
|
|
69
|
+
if (useFloorEditSession.getState().session) useFloorEditSession.setState({ session: null })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Done / Escape: ends the session on the room it edited. */
|
|
73
|
+
export function finishFloorEdit() {
|
|
74
|
+
const session = useFloorEditSession.getState().session
|
|
75
|
+
if (!session) return false
|
|
76
|
+
endFloorEdit()
|
|
77
|
+
completeElementAction({ room: session, point: null })
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function watch(zoneId: string) {
|
|
82
|
+
const stopEditor = useEditor.subscribe((state) => {
|
|
83
|
+
if (state.room?.zoneId !== zoneId && !openingSelectedIn(zoneId)) endFloorEdit()
|
|
84
|
+
})
|
|
85
|
+
const stopScene = useScene.subscribe((state) => {
|
|
86
|
+
if (state.nodes[zoneId as keyof typeof state.nodes]?.type !== 'zone') endFloorEdit()
|
|
87
|
+
})
|
|
88
|
+
const onToolCancel = () => {
|
|
89
|
+
if (useViewer.getState().selection.selectedIds.length) return
|
|
90
|
+
if (finishFloorEdit()) markToolCancelConsumed()
|
|
91
|
+
}
|
|
92
|
+
emitter.on('tool:cancel', onToolCancel)
|
|
93
|
+
return () => {
|
|
94
|
+
stopEditor()
|
|
95
|
+
stopScene()
|
|
96
|
+
emitter.off('tool:cancel', onToolCancel)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function openingSelectedIn(zoneId: string) {
|
|
101
|
+
const nodes = useScene.getState().nodes as Nodes
|
|
102
|
+
const [id] = useViewer.getState().selection.selectedIds
|
|
103
|
+
return (
|
|
104
|
+
!!id &&
|
|
105
|
+
nodes[id]?.type === 'floor-opening' &&
|
|
106
|
+
roomSurfaceOpenings(nodes, zoneId, 'floor').some((opening) => opening.id === id)
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The room an opening belongs to: the selected room, else the room it overlaps most. */
|
|
111
|
+
export function openingRoom(nodes: Nodes, opening: FloorOpeningNode): string | null {
|
|
112
|
+
if (opening.hostZoneId) return opening.hostZoneId
|
|
113
|
+
const selected = useEditor.getState().room
|
|
114
|
+
if (selected && selected.levelId === opening.parentId) return selected.zoneId
|
|
115
|
+
let best: { id: string; overlap: number } | null = null
|
|
116
|
+
for (const node of Object.values(nodes)) {
|
|
117
|
+
if (node.type !== 'zone' || node.parentId !== opening.parentId || node.spaceRole !== 'room')
|
|
118
|
+
continue
|
|
119
|
+
const overlap = area(
|
|
120
|
+
intersection(opening.polygon, { outer: node.polygon, holes: node.holes ?? [] }),
|
|
121
|
+
)
|
|
122
|
+
if (overlap > (best?.overlap ?? 1e-6)) best = { id: node.id, overlap }
|
|
123
|
+
}
|
|
124
|
+
return best?.id ?? null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Deletes an opening (one undo step) and lands on its room. */
|
|
128
|
+
export function deleteFloorOpening(openingId: string) {
|
|
129
|
+
const nodes = useScene.getState().nodes as Nodes
|
|
130
|
+
const opening = nodes[openingId]
|
|
131
|
+
if (opening?.type !== 'floor-opening' || useScene.getState().readOnly) return false
|
|
132
|
+
if (opening.source === 'stair' || opening.source === 'elevator') return false
|
|
133
|
+
const zoneId = openingRoom(nodes, opening)
|
|
134
|
+
cancelOpeningDraft()
|
|
135
|
+
const plan = removeFloorOpening(nodes, openingId)
|
|
136
|
+
useScene.getState().applyNodeChanges(structureChangeBatch(plan.changes))
|
|
137
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
138
|
+
if (zoneId && opening.parentId)
|
|
139
|
+
completeElementAction({ room: { levelId: opening.parentId, zoneId }, point: null })
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Done in the opening's panel: back to its room. */
|
|
144
|
+
export function finishFloorOpening(openingId: string) {
|
|
145
|
+
const nodes = useScene.getState().nodes as Nodes
|
|
146
|
+
const opening = nodes[openingId]
|
|
147
|
+
if (opening?.type !== 'floor-opening') return false
|
|
148
|
+
const zoneId = openingRoom(nodes, opening)
|
|
149
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
150
|
+
if (zoneId && opening.parentId)
|
|
151
|
+
completeElementAction({ room: { levelId: opening.parentId, zoneId }, point: null })
|
|
152
|
+
return true
|
|
153
|
+
}
|