@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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
area,
|
|
5
|
+
containsPoint,
|
|
6
|
+
createMezzanine,
|
|
7
|
+
generateId,
|
|
8
|
+
getStoredLevelHeight,
|
|
9
|
+
type LevelNode,
|
|
10
|
+
type MultiPolygon,
|
|
11
|
+
type StructurePlan,
|
|
12
|
+
useScene,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
import { getRoomSelectionIndex } from '../hooks/use-selected-room'
|
|
17
|
+
import useEditor from '../store/use-editor'
|
|
18
|
+
import {
|
|
19
|
+
CROSSING_MESSAGE,
|
|
20
|
+
clipFloorRegion,
|
|
21
|
+
floorRegionAxisAngle,
|
|
22
|
+
floorRegionSelfIntersects,
|
|
23
|
+
} from './floor-region-geometry'
|
|
24
|
+
import {
|
|
25
|
+
cancelFloorRegion,
|
|
26
|
+
type FloorDraftCommit,
|
|
27
|
+
type FloorDraftPurpose,
|
|
28
|
+
type FloorRegionRoom,
|
|
29
|
+
useFloorRegionDraft,
|
|
30
|
+
} from './floor-region-session'
|
|
31
|
+
import { type FloorRegionPoint, floorRegionSnapTargets } from './floor-region-snap'
|
|
32
|
+
import { beginGesture, type GestureHandle } from './gesture-lifecycle'
|
|
33
|
+
import {
|
|
34
|
+
MEZZANINE_FAILED_MESSAGE,
|
|
35
|
+
MEZZANINE_OUTSIDE_MESSAGE,
|
|
36
|
+
MEZZANINE_TOO_SMALL_MESSAGE,
|
|
37
|
+
mezzanineConflictMessage,
|
|
38
|
+
} from './mezzanine-messages'
|
|
39
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
40
|
+
import { sfxEmitter } from './sfx-bus'
|
|
41
|
+
|
|
42
|
+
// "Add mezzanine": draws a mezzanine outline inside one room with the floor
|
|
43
|
+
// region draft (rectangle or polygon), snapped to the room's walls, and builds
|
|
44
|
+
// it with core's `createMezzanine` at the default half-storey elevation (one
|
|
45
|
+
// undo step). The tool is one lifecycle gesture from `startMezzanineDraft`
|
|
46
|
+
// until the mezzanine is built or the tool is cancelled (Escape, a mode,
|
|
47
|
+
// level or selection change, the room gone); refused outlines stay red with a
|
|
48
|
+
// short label and the tool stays armed for another try.
|
|
49
|
+
|
|
50
|
+
export const MEZZANINE_DRAFT_HANDLE = 'mezzanine-draft'
|
|
51
|
+
|
|
52
|
+
export type MezzanineShape = 'rectangle' | 'polygon'
|
|
53
|
+
|
|
54
|
+
export const useMezzanineDraft = create<{
|
|
55
|
+
/** The room the mezzanine is drawn in while the tool is armed. */
|
|
56
|
+
host: FloorRegionRoom | null
|
|
57
|
+
shape: MezzanineShape
|
|
58
|
+
}>(() => ({ host: null, shape: 'rectangle' }))
|
|
59
|
+
|
|
60
|
+
/** The mezzanine's default elevation: half the storey, as `createMezzanine` rounds it. */
|
|
61
|
+
export function defaultMezzanineElevation(level: LevelNode) {
|
|
62
|
+
return Math.round(getStoredLevelHeight(level) * 10) / 20
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ON_BOUNDARY = 1e-4
|
|
66
|
+
|
|
67
|
+
function distanceToSegment(p: FloorRegionPoint, a: FloorRegionPoint, b: FloorRegionPoint) {
|
|
68
|
+
const dx = b[0] - a[0]
|
|
69
|
+
const dz = b[1] - a[1]
|
|
70
|
+
const lengthSq = dx * dx + dz * dz
|
|
71
|
+
const t = lengthSq
|
|
72
|
+
? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dz) / lengthSq))
|
|
73
|
+
: 0
|
|
74
|
+
return Math.hypot(a[0] + t * dx - p[0], a[1] + t * dz - p[1])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Inside the polygon, or on its boundary (a point snapped onto a wall line). */
|
|
78
|
+
function insideOrOn(polygon: MultiPolygon, point: FloorRegionPoint) {
|
|
79
|
+
if (containsPoint(polygon, point)) return true
|
|
80
|
+
return polygon.some(({ outer, holes }) =>
|
|
81
|
+
[outer, ...holes].some((ring) =>
|
|
82
|
+
ring.some((a, i) => distanceToSegment(point, a, ring[(i + 1) % ring.length]!) <= ON_BOUNDARY),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function withoutRepeats(ring: FloorRegionPoint[]) {
|
|
88
|
+
return ring.filter((point, i) => {
|
|
89
|
+
const next = ring[(i + 1) % ring.length]!
|
|
90
|
+
return Math.hypot(next[0] - point[0], next[1] - point[1]) > ON_BOUNDARY
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refuse(message: string): FloorDraftCommit {
|
|
95
|
+
return { ok: false, message }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Why a mezzanine outline cannot be built, or its plan. The outline is clipped
|
|
100
|
+
* to the host's clear floor (inside the wall faces) first; what the clip
|
|
101
|
+
* leaves must be one piece of at least 1 m².
|
|
102
|
+
*/
|
|
103
|
+
export function planMezzanine(
|
|
104
|
+
nodes: Record<string, AnyNode>,
|
|
105
|
+
host: FloorRegionRoom,
|
|
106
|
+
polygon: readonly FloorRegionPoint[],
|
|
107
|
+
): { plan: StructurePlan & { zoneId: string } } | { message: string } {
|
|
108
|
+
const clip = clipFloorRegion(polygon, host.clear)
|
|
109
|
+
if (!clip)
|
|
110
|
+
return {
|
|
111
|
+
message:
|
|
112
|
+
area([{ outer: polygon.map(([x, z]) => [x, z]), holes: [] }]) < 1
|
|
113
|
+
? MEZZANINE_TOO_SMALL_MESSAGE
|
|
114
|
+
: MEZZANINE_OUTSIDE_MESSAGE,
|
|
115
|
+
}
|
|
116
|
+
if (clip.pieces.length !== 1) return { message: MEZZANINE_OUTSIDE_MESSAGE }
|
|
117
|
+
if (clip.area < 1) return { message: MEZZANINE_TOO_SMALL_MESSAGE }
|
|
118
|
+
const outline = withoutRepeats(clip.polygon)
|
|
119
|
+
let plan: StructurePlan & { zoneId: string }
|
|
120
|
+
try {
|
|
121
|
+
// The height the preview was drawn at, not one recomputed from a storey
|
|
122
|
+
// that may have changed while the tool was armed.
|
|
123
|
+
plan = createMezzanine(nodes, {
|
|
124
|
+
hostZoneId: host.zoneId,
|
|
125
|
+
polygon: outline,
|
|
126
|
+
elevation: host.elevation,
|
|
127
|
+
mintId: generateId,
|
|
128
|
+
})
|
|
129
|
+
} catch {
|
|
130
|
+
return {
|
|
131
|
+
message: floorRegionSelfIntersects(outline)
|
|
132
|
+
? CROSSING_MESSAGE
|
|
133
|
+
: area([{ outer: outline, holes: [] }]) < 1
|
|
134
|
+
? MEZZANINE_TOO_SMALL_MESSAGE
|
|
135
|
+
: MEZZANINE_FAILED_MESSAGE,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const conflict = plan.conflicts?.[0]
|
|
139
|
+
if (!conflict) return { plan }
|
|
140
|
+
return { message: mezzanineConflictMessage(conflict.code) ?? MEZZANINE_FAILED_MESSAGE }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The tool's lifecycle handle, from start until built or cancelled.
|
|
144
|
+
let tool: GestureHandle | null = null
|
|
145
|
+
|
|
146
|
+
function reset() {
|
|
147
|
+
if (useMezzanineDraft.getState().host) useMezzanineDraft.setState({ host: null })
|
|
148
|
+
const { draft } = useFloorRegionDraft.getState()
|
|
149
|
+
if (draft?.room.purpose === MEZZANINE) useFloorRegionDraft.setState({ draft: null, hover: null })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function commitMezzanine(host: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
|
|
153
|
+
const result = planMezzanine(useScene.getState().nodes, host, polygon)
|
|
154
|
+
if ('message' in result) return refuse(result.message)
|
|
155
|
+
const handle = tool
|
|
156
|
+
tool = null
|
|
157
|
+
try {
|
|
158
|
+
if (handle?.active) handle.finish(() => applyRoomPlan(result.plan))
|
|
159
|
+
else applyRoomPlan(result.plan)
|
|
160
|
+
} finally {
|
|
161
|
+
reset()
|
|
162
|
+
}
|
|
163
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
164
|
+
useEditor.getState().selectRoom({ levelId: host.levelId, zoneId: result.plan.zoneId })
|
|
165
|
+
return { ok: true } as const
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function hostGone(host: FloorRegionRoom) {
|
|
169
|
+
const scene = useScene.getState()
|
|
170
|
+
const zone = scene.nodes[host.zoneId as AnyNodeId]
|
|
171
|
+
return (
|
|
172
|
+
scene.readOnly ||
|
|
173
|
+
zone?.type !== 'zone' ||
|
|
174
|
+
zone.parentId !== host.levelId ||
|
|
175
|
+
useEditor.getState().room?.zoneId !== host.zoneId
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function beginTool(host: FloorRegionRoom) {
|
|
180
|
+
const handle = beginGesture({
|
|
181
|
+
kind: 'mezzanine-draft',
|
|
182
|
+
scope: { kind: 'handle-drag', nodeId: host.zoneId, handle: MEZZANINE_DRAFT_HANDLE },
|
|
183
|
+
stale: () => hostGone(host),
|
|
184
|
+
onCancel: () => {
|
|
185
|
+
if (tool === handle) tool = null
|
|
186
|
+
reset()
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
tool = handle
|
|
190
|
+
useMezzanineDraft.setState({ host })
|
|
191
|
+
return handle
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const MEZZANINE: FloorDraftPurpose = {
|
|
195
|
+
persistent: true,
|
|
196
|
+
own: (room) => (tool?.active ? tool : beginTool(room)),
|
|
197
|
+
commit: commitMezzanine,
|
|
198
|
+
pointMessage: (room, point) => (insideOrOn(room.clear, point) ? null : MEZZANINE_OUTSIDE_MESSAGE),
|
|
199
|
+
fill: '#818cf8',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The room a mezzanine is drawn in: its clear floor (inside the wall faces,
|
|
204
|
+
* which is what core accepts) bounds the outline, which snaps to those faces
|
|
205
|
+
* and the level's other mezzanines. Null when the room cannot host one.
|
|
206
|
+
*/
|
|
207
|
+
export function describeMezzanineHost(
|
|
208
|
+
nodes: Record<string, AnyNode>,
|
|
209
|
+
hostZoneId: string,
|
|
210
|
+
): FloorRegionRoom | null {
|
|
211
|
+
const zone = nodes[hostZoneId]
|
|
212
|
+
if (zone?.type !== 'zone' || zone.floor?.support === 'open' || !zone.parentId) return null
|
|
213
|
+
const level = nodes[zone.parentId]
|
|
214
|
+
if (level?.type !== 'level') return null
|
|
215
|
+
const record = getRoomSelectionIndex(level.id)
|
|
216
|
+
.update(nodes)
|
|
217
|
+
.find((room) => room.zoneId === hostZoneId)
|
|
218
|
+
if (!record) return null
|
|
219
|
+
const clear: MultiPolygon = record.clearPolygon
|
|
220
|
+
const mezzanines = Object.values(nodes).flatMap((node) =>
|
|
221
|
+
node.type === 'zone' && node.floor?.support === 'open' && node.parentId === level.id
|
|
222
|
+
? [node.polygon]
|
|
223
|
+
: [],
|
|
224
|
+
)
|
|
225
|
+
return {
|
|
226
|
+
zoneId: hostZoneId,
|
|
227
|
+
levelId: level.id,
|
|
228
|
+
clear,
|
|
229
|
+
elevation: defaultMezzanineElevation(level),
|
|
230
|
+
angle: floorRegionAxisAngle(clear),
|
|
231
|
+
targets: floorRegionSnapTargets(clear, mezzanines),
|
|
232
|
+
purpose: MEZZANINE,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Arms "Add mezzanine" in `hostZoneId`: select mode, the room's level and the
|
|
238
|
+
* room selected first, then the tool's gesture, so setting them up cannot
|
|
239
|
+
* cancel it. Returns false (changing nothing) when the room cannot host one.
|
|
240
|
+
*/
|
|
241
|
+
export function startMezzanineDraft(
|
|
242
|
+
hostZoneId: string,
|
|
243
|
+
shape: MezzanineShape = 'rectangle',
|
|
244
|
+
): boolean {
|
|
245
|
+
const nodes = useScene.getState().nodes
|
|
246
|
+
if (useScene.getState().readOnly) return false
|
|
247
|
+
const host = describeMezzanineHost(nodes, hostZoneId)
|
|
248
|
+
if (!host) return false
|
|
249
|
+
cancelMezzanineDraft()
|
|
250
|
+
cancelFloorRegion()
|
|
251
|
+
useMezzanineDraft.setState({ shape })
|
|
252
|
+
if (useEditor.getState().mode !== 'select') useEditor.getState().setMode('select')
|
|
253
|
+
if (useViewer.getState().selection.levelId !== host.levelId)
|
|
254
|
+
useViewer.getState().setSelection({ levelId: host.levelId as LevelNode['id'] })
|
|
255
|
+
if (useEditor.getState().room?.zoneId !== hostZoneId)
|
|
256
|
+
useEditor.getState().selectRoom({ levelId: host.levelId, zoneId: hostZoneId })
|
|
257
|
+
beginTool(host)
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function cancelMezzanineDraft() {
|
|
262
|
+
const current = tool
|
|
263
|
+
tool = null
|
|
264
|
+
if (current?.active) current.cancel()
|
|
265
|
+
else reset()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function mezzanineDraftActive() {
|
|
269
|
+
return !!useMezzanineDraft.getState().host
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Switches rectangle ↔ polygon; the outline in hand is dropped, the tool stays. */
|
|
273
|
+
export function setMezzanineShape(shape: MezzanineShape) {
|
|
274
|
+
if (useMezzanineDraft.getState().shape === shape) return
|
|
275
|
+
const { draft } = useFloorRegionDraft.getState()
|
|
276
|
+
if (draft?.room.purpose === MEZZANINE) useFloorRegionDraft.setState({ draft: null })
|
|
277
|
+
useMezzanineDraft.setState({ shape })
|
|
278
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CROSSING_MESSAGE } from './floor-region-geometry'
|
|
2
|
+
|
|
3
|
+
// Short labels for what core refuses a mezzanine — drawing, moving, turning,
|
|
4
|
+
// copying, pushing an edge or adding its stair all read the same words.
|
|
5
|
+
|
|
6
|
+
export const MEZZANINE_OUTSIDE_MESSAGE = 'Outside the room'
|
|
7
|
+
export const MEZZANINE_TOO_SMALL_MESSAGE = 'At least 1 m²'
|
|
8
|
+
export const MEZZANINE_OVERLAP_MESSAGE = 'Overlaps a mezzanine'
|
|
9
|
+
export const MEZZANINE_ELEVATION_MESSAGE = 'Too high or too low'
|
|
10
|
+
export const MEZZANINE_FAILED_MESSAGE = "Can't add a mezzanine here"
|
|
11
|
+
export const MEZZANINE_NO_STAIR_MESSAGE = 'No room for stairs'
|
|
12
|
+
|
|
13
|
+
const CONFLICT_MESSAGES: Record<string, string> = {
|
|
14
|
+
'outside-host': MEZZANINE_OUTSIDE_MESSAGE,
|
|
15
|
+
'overlaps-mezzanine': MEZZANINE_OVERLAP_MESSAGE,
|
|
16
|
+
'mezzanine-overlap': MEZZANINE_OVERLAP_MESSAGE,
|
|
17
|
+
'too-small': MEZZANINE_TOO_SMALL_MESSAGE,
|
|
18
|
+
'self-intersecting': CROSSING_MESSAGE,
|
|
19
|
+
'mezzanine-elevation': MEZZANINE_ELEVATION_MESSAGE,
|
|
20
|
+
'no-room-for-stair': MEZZANINE_NO_STAIR_MESSAGE,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The label for a core mezzanine conflict code, or null when it is not one of them. */
|
|
24
|
+
export function mezzanineConflictMessage(code: string | undefined): string | null {
|
|
25
|
+
return (code && CONFLICT_MESSAGES[code]) || null
|
|
26
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
clearSceneHistory,
|
|
7
|
+
createMezzanine,
|
|
8
|
+
createZone,
|
|
9
|
+
deleteZone,
|
|
10
|
+
generateId,
|
|
11
|
+
initSpaceDetectionSync,
|
|
12
|
+
LevelNode,
|
|
13
|
+
type SlabNode,
|
|
14
|
+
useScene,
|
|
15
|
+
type ZoneNode,
|
|
16
|
+
} from '@pascal-app/core'
|
|
17
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
18
|
+
import { deleteConfirmationContent } from '../components/editor/delete-confirmation-dialog'
|
|
19
|
+
import { resolveFloorRegionRoom } from '../components/editor/floor-region-controls'
|
|
20
|
+
import { MEZZANINE_GESTURE_HINTS, mezzanineGesture } from '../components/ui/helpers/helper-manager'
|
|
21
|
+
import {
|
|
22
|
+
MEZZANINE_EDGE_DRAG_LABEL,
|
|
23
|
+
mezzanineElevationBounds,
|
|
24
|
+
ROOM_ELEVATION_DRAG_LABEL,
|
|
25
|
+
} from './room-handle-drag'
|
|
26
|
+
import { RoomSelectionIndex, resolveRoomHit } from './room-selection'
|
|
27
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
28
|
+
import { ROOM_MOVE_DRAG_LABEL } from './room-transform-session'
|
|
29
|
+
import { zoneAtLevelPoint } from './units'
|
|
30
|
+
|
|
31
|
+
const LEVEL = 'level_mezzanine_selection'
|
|
32
|
+
let hostId = '' as ZoneNode['id']
|
|
33
|
+
let mezzanineId = '' as ZoneNode['id']
|
|
34
|
+
let stop = () => {}
|
|
35
|
+
const savedRaf = globalThis.requestAnimationFrame
|
|
36
|
+
const savedCaf = globalThis.cancelAnimationFrame
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
globalThis.requestAnimationFrame ??= ((callback: (time: number) => void) => {
|
|
40
|
+
callback(0)
|
|
41
|
+
return 0
|
|
42
|
+
}) as typeof requestAnimationFrame
|
|
43
|
+
globalThis.cancelAnimationFrame ??= (() => {}) as typeof cancelAnimationFrame
|
|
44
|
+
const building = BuildingNode.parse({ id: 'building_mezzanine_selection', children: [LEVEL] })
|
|
45
|
+
const level = LevelNode.parse({ id: LEVEL, parentId: building.id, height: 5 })
|
|
46
|
+
useScene.setState({
|
|
47
|
+
nodes: { [building.id]: building, [level.id]: level },
|
|
48
|
+
rootNodeIds: [building.id],
|
|
49
|
+
dirtyNodes: new Set(),
|
|
50
|
+
materials: {},
|
|
51
|
+
collections: {},
|
|
52
|
+
readOnly: false,
|
|
53
|
+
})
|
|
54
|
+
useScene.temporal.getState().resume()
|
|
55
|
+
stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
|
|
56
|
+
const host = createZone(useScene.getState().nodes, {
|
|
57
|
+
levelId: LEVEL,
|
|
58
|
+
polygon: [
|
|
59
|
+
[0, 0],
|
|
60
|
+
[8, 0],
|
|
61
|
+
[8, 6],
|
|
62
|
+
[0, 6],
|
|
63
|
+
],
|
|
64
|
+
enclose: true,
|
|
65
|
+
mintId: generateId,
|
|
66
|
+
})
|
|
67
|
+
applyRoomPlan(host)
|
|
68
|
+
hostId = host.zoneId as ZoneNode['id']
|
|
69
|
+
const mezzanine = createMezzanine(useScene.getState().nodes, {
|
|
70
|
+
hostZoneId: hostId,
|
|
71
|
+
polygon: [
|
|
72
|
+
[0.5, 0.5],
|
|
73
|
+
[4, 0.5],
|
|
74
|
+
[4, 3],
|
|
75
|
+
[0.5, 3],
|
|
76
|
+
],
|
|
77
|
+
mintId: generateId,
|
|
78
|
+
})
|
|
79
|
+
expect(mezzanine.conflicts ?? []).toEqual([])
|
|
80
|
+
applyRoomPlan(mezzanine)
|
|
81
|
+
mezzanineId = mezzanine.zoneId as ZoneNode['id']
|
|
82
|
+
useViewer.getState().setSelection({ buildingId: building.id, levelId: LEVEL, selectedIds: [] })
|
|
83
|
+
clearSceneHistory()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(() => stop())
|
|
87
|
+
|
|
88
|
+
afterAll(() => {
|
|
89
|
+
globalThis.requestAnimationFrame = savedRaf
|
|
90
|
+
globalThis.cancelAnimationFrame = savedCaf
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const nodes = () => useScene.getState().nodes as Record<string, AnyNode>
|
|
94
|
+
|
|
95
|
+
describe('mezzanine selection', () => {
|
|
96
|
+
test('the room index lists the mezzanine as its own room beside the untouched host', () => {
|
|
97
|
+
const index = new RoomSelectionIndex(LEVEL)
|
|
98
|
+
const records = index.update(nodes())
|
|
99
|
+
const host = records.find((room) => room.zoneId === hostId)!
|
|
100
|
+
const mezzanine = records.find((room) => room.zoneId === mezzanineId)!
|
|
101
|
+
expect(host.mezzanine).toBeNull()
|
|
102
|
+
expect(host.area).toBeCloseTo(48, 0)
|
|
103
|
+
expect(mezzanine.mezzanine).toEqual({ hostZoneId: hostId })
|
|
104
|
+
expect(mezzanine.name).toBe('Mezzanine')
|
|
105
|
+
expect(mezzanine.area).toBeCloseTo(8.75)
|
|
106
|
+
expect(mezzanine.spans).toEqual([])
|
|
107
|
+
expect(mezzanine.boundaryWallIds).toEqual([])
|
|
108
|
+
const plate = nodes()[mezzanine.slabId as AnyNodeId] as SlabNode
|
|
109
|
+
expect(plate.support).toBe('open')
|
|
110
|
+
expect(host.slabId).not.toBe(mezzanine.slabId)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('the mezzanine plate and railing pick the mezzanine; the host floor under it picks the host', () => {
|
|
114
|
+
const index = new RoomSelectionIndex(LEVEL)
|
|
115
|
+
const records = index.update(nodes())
|
|
116
|
+
const mezzanine = records.find((room) => room.zoneId === mezzanineId)!
|
|
117
|
+
const host = records.find((room) => room.zoneId === hostId)!
|
|
118
|
+
const plate = nodes()[mezzanine.slabId as AnyNodeId]!
|
|
119
|
+
const hostFloor = nodes()[host.slabId as AnyNodeId]!
|
|
120
|
+
// Anywhere on the mezzanine plate — its top, underside or a railing post.
|
|
121
|
+
expect(resolveRoomHit(index, LEVEL, plate, [2, 1.5], '3d')?.key.zoneId).toBe(mezzanineId)
|
|
122
|
+
expect(resolveRoomHit(index, LEVEL, plate, [3.99, 2.99], '3d')?.key.zoneId).toBe(mezzanineId)
|
|
123
|
+
// The host floor under the mezzanine, and a point-only (plan) hit there.
|
|
124
|
+
expect(resolveRoomHit(index, LEVEL, hostFloor, [2, 1.5], '3d')?.key.zoneId).toBe(hostId)
|
|
125
|
+
expect(resolveRoomHit(index, LEVEL, null, [2, 1.5])?.key.zoneId).toBe(hostId)
|
|
126
|
+
expect(resolveRoomHit(index, LEVEL, hostFloor, [6, 4], '3d')?.key.zoneId).toBe(hostId)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('units pick the zone by the hit height: on the mezzanine or under it', () => {
|
|
130
|
+
const elevation = (nodes()[mezzanineId as AnyNodeId] as ZoneNode).floor!.elevation!
|
|
131
|
+
expect(elevation).toBeCloseTo(2.5)
|
|
132
|
+
expect(zoneAtLevelPoint(2, 1.5, elevation)?.id).toBe(mezzanineId)
|
|
133
|
+
expect(zoneAtLevelPoint(2, 1.5, elevation - 0.2)?.id).toBe(mezzanineId)
|
|
134
|
+
expect(zoneAtLevelPoint(2, 1.5, 0.05)?.id).toBe(hostId)
|
|
135
|
+
expect(zoneAtLevelPoint(6, 4, elevation)?.id).toBe(hostId)
|
|
136
|
+
// No height (the plan, from above): the mezzanine on top.
|
|
137
|
+
expect(zoneAtLevelPoint(2, 1.5)?.id).toBe(mezzanineId)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('the elevation handle stays inside the heights core accepts', () => {
|
|
141
|
+
expect(mezzanineElevationBounds(nodes(), hostId)).toBeNull()
|
|
142
|
+
const bounds = mezzanineElevationBounds(nodes(), mezzanineId)!
|
|
143
|
+
expect(bounds.min).toBeCloseTo(0.21)
|
|
144
|
+
expect(bounds.max).toBeCloseTo(4.7)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('a mezzanine covering its whole host still wins on its plate (units)', () => {
|
|
148
|
+
// Stretch the mezzanine over the host's whole footprint: same area, host listed first.
|
|
149
|
+
const hostPolygon = (nodes()[hostId as AnyNodeId] as ZoneNode).polygon
|
|
150
|
+
useScene.getState().updateNode(mezzanineId as AnyNodeId, { polygon: hostPolygon } as never)
|
|
151
|
+
const elevation = (nodes()[mezzanineId as AnyNodeId] as ZoneNode).floor!.elevation!
|
|
152
|
+
expect(zoneAtLevelPoint(2, 1.5, elevation)?.id).toBe(mezzanineId)
|
|
153
|
+
expect(zoneAtLevelPoint(6, 4, elevation)?.id).toBe(mezzanineId)
|
|
154
|
+
expect(zoneAtLevelPoint(6, 4)?.id).toBe(mezzanineId)
|
|
155
|
+
expect(zoneAtLevelPoint(2, 1.5, 0.05)?.id).toBe(hostId)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('plan-view floor drafts pick the topmost floor: the mezzanine over its host', () => {
|
|
159
|
+
expect(resolveFloorRegionRoom(LEVEL, [2, 1.5])?.zoneId).toBe(mezzanineId)
|
|
160
|
+
expect(resolveFloorRegionRoom(LEVEL, [6, 4])?.zoneId).toBe(hostId)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('raising, resizing and moving a mezzanine are titled Mezzanine; a room keeps its HUDs', () => {
|
|
164
|
+
const gesture = (nodeId: string, label: string) => mezzanineGesture({ nodeId, label }, nodes())
|
|
165
|
+
expect(gesture(mezzanineId, ROOM_ELEVATION_DRAG_LABEL)).toBe('raise')
|
|
166
|
+
expect(gesture(mezzanineId, MEZZANINE_EDGE_DRAG_LABEL)).toBe('resize')
|
|
167
|
+
expect(gesture(mezzanineId, ROOM_MOVE_DRAG_LABEL)).toBe('move')
|
|
168
|
+
expect(gesture(hostId, ROOM_ELEVATION_DRAG_LABEL)).toBeNull()
|
|
169
|
+
expect(gesture(hostId, ROOM_MOVE_DRAG_LABEL)).toBeNull()
|
|
170
|
+
expect(gesture(mezzanineId, 'height')).toBeNull()
|
|
171
|
+
expect(mezzanineGesture(null, nodes())).toBeNull()
|
|
172
|
+
expect(MEZZANINE_GESTURE_HINTS.move[0]!.label).toBe('Place inside the room')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('the delete dialog speaks of the mezzanine, and of its items only when it has some', () => {
|
|
176
|
+
const request = (contents: 'delete' | 'keep' = 'delete') => ({
|
|
177
|
+
count: 1,
|
|
178
|
+
room: deleteZone(nodes(), { zoneId: mezzanineId, contents }).payload,
|
|
179
|
+
onConfirm: () => {},
|
|
180
|
+
onKeepContents: () => {},
|
|
181
|
+
})
|
|
182
|
+
const empty = deleteConfirmationContent(request(), nodes())
|
|
183
|
+
expect(empty.title).toBe('Delete Mezzanine?')
|
|
184
|
+
expect(empty.description).toMatch(/^Removes the mezzanine floor/)
|
|
185
|
+
expect(empty.description).not.toContain('item')
|
|
186
|
+
expect(empty.keepLabel).toBeNull()
|
|
187
|
+
const hasCeiling = Object.values(nodes()).some(
|
|
188
|
+
(node) => node.type === 'ceiling' && node.zoneId === mezzanineId,
|
|
189
|
+
)
|
|
190
|
+
expect(empty.description.includes('ceiling')).toBe(hasCeiling)
|
|
191
|
+
const withItems = request()
|
|
192
|
+
withItems.room = { ...withItems.room, itemIds: ['item_a', 'item_b'] }
|
|
193
|
+
const loaded = deleteConfirmationContent(withItems, nodes())
|
|
194
|
+
expect(loaded.description).toContain('2 items are on it. Keep them on the floor below')
|
|
195
|
+
expect(loaded.keepLabel).toBe('Keep items')
|
|
196
|
+
// A room's dialog is unchanged.
|
|
197
|
+
const room = deleteConfirmationContent(
|
|
198
|
+
{
|
|
199
|
+
count: 1,
|
|
200
|
+
room: deleteZone(nodes(), { zoneId: hostId, contents: 'delete' }).payload,
|
|
201
|
+
onConfirm: () => {},
|
|
202
|
+
},
|
|
203
|
+
nodes(),
|
|
204
|
+
)
|
|
205
|
+
expect(room.description).not.toContain('mezzanine')
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
planMezzanineStair,
|
|
4
|
+
runAsSingleSceneHistoryStep,
|
|
5
|
+
useScene,
|
|
6
|
+
} from '@pascal-app/core'
|
|
7
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
8
|
+
import useEditor from '../store/use-editor'
|
|
9
|
+
import { MEZZANINE_NO_STAIR_MESSAGE, mezzanineConflictMessage } from './mezzanine-messages'
|
|
10
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
11
|
+
import { sfxEmitter } from './sfx-bus'
|
|
12
|
+
|
|
13
|
+
export type MezzanineStairResult = { ok: true; stairId: string } | { ok: false; message: string }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* "Add stairs": core places a straight flight from the host floor up to one of
|
|
17
|
+
* the mezzanine's open edges (its railing opens there), as one undo step. The
|
|
18
|
+
* new stair is selected, like a new mezzanine is, so it can be adjusted at once.
|
|
19
|
+
*/
|
|
20
|
+
export function addMezzanineStairs(zoneId: string): MezzanineStairResult {
|
|
21
|
+
const nodes = useScene.getState().nodes
|
|
22
|
+
if (useScene.getState().readOnly || !nodes[zoneId as AnyNodeId])
|
|
23
|
+
return { ok: false, message: MEZZANINE_NO_STAIR_MESSAGE }
|
|
24
|
+
let plan: ReturnType<typeof planMezzanineStair>
|
|
25
|
+
try {
|
|
26
|
+
plan = planMezzanineStair(nodes, zoneId)
|
|
27
|
+
} catch {
|
|
28
|
+
return { ok: false, message: MEZZANINE_NO_STAIR_MESSAGE }
|
|
29
|
+
}
|
|
30
|
+
const conflict = plan.conflicts?.[0]
|
|
31
|
+
if (conflict || !plan.stairId)
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
message: mezzanineConflictMessage(conflict?.code) ?? MEZZANINE_NO_STAIR_MESSAGE,
|
|
35
|
+
}
|
|
36
|
+
// The stair and its flight land as one step.
|
|
37
|
+
let applied = false
|
|
38
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
39
|
+
applied = applyRoomPlan(plan)
|
|
40
|
+
})
|
|
41
|
+
if (!applied) return { ok: false, message: MEZZANINE_NO_STAIR_MESSAGE }
|
|
42
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
43
|
+
useEditor.getState().clearRoom()
|
|
44
|
+
useViewer.getState().setSelection({ selectedIds: [plan.stairId as AnyNodeId] })
|
|
45
|
+
return { ok: true, stairId: plan.stairId! }
|
|
46
|
+
}
|
package/src/lib/model-export.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { GlbExportOptions } from './glb-export'
|
|
2
2
|
|
|
3
|
-
export type ModelExportFormat = 'glb' | 'usdz' | 'stl' | 'obj' | 'print-stl' | 'print-3mf'
|
|
3
|
+
export type ModelExportFormat = 'glb' | 'usdz' | 'stl' | 'obj' | 'ifc' | 'print-stl' | 'print-3mf'
|
|
4
4
|
|
|
5
5
|
export type ModelExportOptions = Pick<
|
|
6
6
|
GlbExportOptions,
|
|
7
7
|
'onlyVisible' | 'excludedNodeTypes' | 'includedPresentationIds'
|
|
8
8
|
> & {
|
|
9
9
|
download?: boolean
|
|
10
|
+
/** Names the IFC project and its file. */
|
|
11
|
+
projectName?: string
|
|
10
12
|
printScale?: number
|
|
11
13
|
printScope?: 'whole' | 'levels'
|
|
12
14
|
printContent?: 'structure' | 'everything'
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
cloneNodesInto,
|
|
5
|
+
collectSubtree,
|
|
6
|
+
isMovable,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { copyCreateOps } from './fresh-planar-placement'
|
|
2
11
|
|
|
3
12
|
export function registryMoveDisabled(node: AnyNode): boolean {
|
|
4
13
|
const def = nodeRegistry.get(node.type)
|
|
@@ -11,11 +20,13 @@ export function registryMoveDisabled(node: AnyNode): boolean {
|
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export function duplicateWithoutMove(node: AnyNode) {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
if (!subtree || !
|
|
17
|
-
|
|
23
|
+
const subtree = collectSubtree(useScene.getState().nodes, node.id)
|
|
24
|
+
const parentId = node.parentId as AnyNodeId | null
|
|
25
|
+
if (!subtree || !parentId) return null
|
|
26
|
+
const cloned = cloneNodesInto([subtree.root, ...subtree.descendants], {
|
|
18
27
|
rootId: node.id,
|
|
19
|
-
parentId
|
|
28
|
+
parentId,
|
|
20
29
|
})
|
|
30
|
+
useScene.getState().createNodes(copyCreateOps(cloned, parentId))
|
|
31
|
+
return cloned.rootId
|
|
21
32
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A node's human label, the same in the baked artifact (`extras.label`) and the
|
|
5
|
+
* viewer's breadcrumb: an explicit name wins, items fall back to their catalog
|
|
6
|
+
* asset name, other kinds to a type word.
|
|
7
|
+
*/
|
|
8
|
+
export function nodeDisplayLabel(node: AnyNode): string {
|
|
9
|
+
if (node.name) return node.name
|
|
10
|
+
switch (node.type) {
|
|
11
|
+
case 'item':
|
|
12
|
+
return (node as { asset?: { name?: string } }).asset?.name || 'Item'
|
|
13
|
+
case 'wall':
|
|
14
|
+
return 'Wall'
|
|
15
|
+
case 'door':
|
|
16
|
+
return 'Door'
|
|
17
|
+
case 'window':
|
|
18
|
+
return 'Window'
|
|
19
|
+
case 'cabinet':
|
|
20
|
+
case 'cabinet-module':
|
|
21
|
+
return 'Cabinet'
|
|
22
|
+
case 'slab':
|
|
23
|
+
return 'Slab'
|
|
24
|
+
case 'ceiling':
|
|
25
|
+
return 'Ceiling'
|
|
26
|
+
case 'roof':
|
|
27
|
+
return 'Roof'
|
|
28
|
+
case 'fence':
|
|
29
|
+
return 'Fence'
|
|
30
|
+
case 'column':
|
|
31
|
+
return 'Column'
|
|
32
|
+
case 'stair':
|
|
33
|
+
return 'Stairs'
|
|
34
|
+
default:
|
|
35
|
+
return node.type
|
|
36
|
+
}
|
|
37
|
+
}
|