@pascal-app/editor 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
floorFootprintName,
|
|
6
|
+
floorFootprintSupportClass,
|
|
7
|
+
floorPlateAtGroundContact,
|
|
8
|
+
MIN_GROUND_FLOOR_THICKNESS,
|
|
9
|
+
MIN_SLAB_THICKNESS,
|
|
10
|
+
roomDrawnFloor,
|
|
11
|
+
useScene,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { Check, ChevronDown, ChevronRight } from 'lucide-react'
|
|
15
|
+
import Image from 'next/image'
|
|
16
|
+
import { type ReactNode, useMemo, useState } from 'react'
|
|
17
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
18
|
+
import { startCeilingEdit } from '../../../lib/ceiling-edit-session'
|
|
19
|
+
import {
|
|
20
|
+
applyRoomFloorConstruction,
|
|
21
|
+
floorLift,
|
|
22
|
+
foundationHeight,
|
|
23
|
+
openFloorFoundation,
|
|
24
|
+
roomFloorOwner,
|
|
25
|
+
roomFootprint,
|
|
26
|
+
} from '../../../lib/floor-footprints'
|
|
27
|
+
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
28
|
+
import { startMezzanineDraft } from '../../../lib/mezzanine-draft'
|
|
29
|
+
import { resolveRoomAssemblyHeights } from '../../../lib/room-assembly-overlay'
|
|
30
|
+
import { roomBuiltOn, separateFloorSummary } from '../../../lib/room-built-on'
|
|
31
|
+
import type { RoomConstructionState } from '../../../lib/room-construction'
|
|
32
|
+
import { roomConstructionState } from '../../../lib/room-construction'
|
|
33
|
+
import {
|
|
34
|
+
addRoomConstruction,
|
|
35
|
+
adoptExistingCeiling,
|
|
36
|
+
floorHeightRefusalText,
|
|
37
|
+
lockRoomOutsideFaces,
|
|
38
|
+
type RoomConstructionPart,
|
|
39
|
+
type RoomConstructionResult,
|
|
40
|
+
removeRoomConstruction,
|
|
41
|
+
replaceRoomCeiling,
|
|
42
|
+
roomFloorBase,
|
|
43
|
+
roomOutsideFacesLocked,
|
|
44
|
+
roomRelativeFloorHeight,
|
|
45
|
+
separateFloorOffset,
|
|
46
|
+
setRoomFloor,
|
|
47
|
+
setRoomRelativeFloorHeight,
|
|
48
|
+
unlockRoomOutsideFaces,
|
|
49
|
+
} from '../../../lib/room-construction-commands'
|
|
50
|
+
import { ownFloorElevationBounds } from '../../../lib/room-handle-drag'
|
|
51
|
+
import type { RoomSelectionRecord } from '../../../lib/room-selection'
|
|
52
|
+
import { cn } from '../../../lib/utils'
|
|
53
|
+
import { MetricControl } from '../controls/metric-control'
|
|
54
|
+
import {
|
|
55
|
+
DropdownMenu,
|
|
56
|
+
DropdownMenuContent,
|
|
57
|
+
DropdownMenuItem,
|
|
58
|
+
DropdownMenuSeparator,
|
|
59
|
+
DropdownMenuTrigger,
|
|
60
|
+
} from '../primitives/dropdown-menu'
|
|
61
|
+
import { Switch } from '../primitives/switch'
|
|
62
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
|
|
63
|
+
import { RoomOpeningsField } from './room-openings-field'
|
|
64
|
+
import { ShapeChoice } from './shape-choice'
|
|
65
|
+
|
|
66
|
+
/** The buttons a row offers — derived together with its presence. */
|
|
67
|
+
export function constructionActions(state: RoomConstructionState, part: RoomConstructionPart) {
|
|
68
|
+
return state[part].actions
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The Walls switch reads three ways. A full ring is on and a click takes the
|
|
73
|
+
* room's own walls away (shared ones stay); a partial ring shows half on and
|
|
74
|
+
* a click closes every open side; no walls is off and a click builds them.
|
|
75
|
+
*/
|
|
76
|
+
export function wallsSwitch(state: RoomConstructionState) {
|
|
77
|
+
const { state: presence, actions } = state.walls
|
|
78
|
+
const full = presence === 'present'
|
|
79
|
+
return {
|
|
80
|
+
checked: full,
|
|
81
|
+
mixed: presence === 'partial',
|
|
82
|
+
command: full ? ('remove' as const) : ('add' as const),
|
|
83
|
+
disabled: full ? !actions.remove : !actions.add,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const pill =
|
|
88
|
+
'rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50'
|
|
89
|
+
|
|
90
|
+
const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
|
|
91
|
+
|
|
92
|
+
export const DRAWN_FLOOR_SWITCH_HINT = 'This floor is a drawn slab: select it to edit or delete it.'
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One "Built with" row: its icon, name and one-line summary, a chevron that
|
|
96
|
+
* opens its settings, and the switch (or button) on the right. An excluded
|
|
97
|
+
* part reads "Not included" and has nothing to open.
|
|
98
|
+
*/
|
|
99
|
+
export function ConstructionPart({
|
|
100
|
+
part,
|
|
101
|
+
label,
|
|
102
|
+
icon,
|
|
103
|
+
on,
|
|
104
|
+
meta,
|
|
105
|
+
expanded,
|
|
106
|
+
onToggleExpanded,
|
|
107
|
+
control,
|
|
108
|
+
footer,
|
|
109
|
+
children,
|
|
110
|
+
}: {
|
|
111
|
+
part: string
|
|
112
|
+
label: string
|
|
113
|
+
icon: string
|
|
114
|
+
on: boolean
|
|
115
|
+
meta: string
|
|
116
|
+
expanded: boolean
|
|
117
|
+
onToggleExpanded: () => void
|
|
118
|
+
/** The switch, or a button, at the end of the row. */
|
|
119
|
+
control?: ReactNode
|
|
120
|
+
/** Always shown under the row (open or not). */
|
|
121
|
+
footer?: ReactNode
|
|
122
|
+
/** The settings the chevron opens; none means the row does not open. */
|
|
123
|
+
children?: ReactNode
|
|
124
|
+
}) {
|
|
125
|
+
const opens = on && !!children
|
|
126
|
+
const open = opens && expanded
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={cn(
|
|
130
|
+
'rounded-lg bg-accent/30 transition-colors hover:bg-accent/50',
|
|
131
|
+
open && 'bg-accent/50',
|
|
132
|
+
)}
|
|
133
|
+
data-construction-row={part}
|
|
134
|
+
data-expanded={open || undefined}
|
|
135
|
+
>
|
|
136
|
+
<div className="flex items-center gap-1 pr-2.5">
|
|
137
|
+
<button
|
|
138
|
+
aria-expanded={opens ? open : undefined}
|
|
139
|
+
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-lg py-2 pr-1 pl-2.5 text-left disabled:cursor-default"
|
|
140
|
+
disabled={!opens}
|
|
141
|
+
onClick={onToggleExpanded}
|
|
142
|
+
type="button"
|
|
143
|
+
>
|
|
144
|
+
<Image
|
|
145
|
+
alt=""
|
|
146
|
+
className={cn('shrink-0 object-contain', !on && 'opacity-40')}
|
|
147
|
+
height={20}
|
|
148
|
+
src={icon}
|
|
149
|
+
width={20}
|
|
150
|
+
/>
|
|
151
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
152
|
+
<span className="font-medium text-[13px] text-foreground">{label}</span>
|
|
153
|
+
<span className="text-pretty text-muted-foreground text-xs">
|
|
154
|
+
{on ? meta : 'Not included'}
|
|
155
|
+
</span>
|
|
156
|
+
</span>
|
|
157
|
+
{opens && (
|
|
158
|
+
<ChevronRight
|
|
159
|
+
className={cn(
|
|
160
|
+
'size-3.5 shrink-0 text-muted-foreground transition-transform',
|
|
161
|
+
open && 'rotate-90',
|
|
162
|
+
)}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</button>
|
|
166
|
+
{control}
|
|
167
|
+
</div>
|
|
168
|
+
{footer}
|
|
169
|
+
{open && (
|
|
170
|
+
<div className="flex flex-col gap-2 pr-2.5 pb-3 pl-[42px] text-muted-foreground text-xs">
|
|
171
|
+
{children}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A label and its value (or control) on one line inside an open row. */
|
|
179
|
+
export function PartField({ label, children }: { label: string; children: ReactNode }) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex min-h-7 items-center justify-between gap-3">
|
|
182
|
+
<span>{label}</span>
|
|
183
|
+
{children}
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The room floor's height above the floor it stands on (its footprint's floor;
|
|
190
|
+
* for a mezzanine, the room below): typing 0.15 on a raised house lifts the
|
|
191
|
+
* room 15 cm above the house floor. A refused height says why, right here.
|
|
192
|
+
*/
|
|
193
|
+
export function FloorHeightField({
|
|
194
|
+
zoneId,
|
|
195
|
+
bounds,
|
|
196
|
+
}: {
|
|
197
|
+
zoneId: string
|
|
198
|
+
/** Allowed heights, in the same relative terms. */
|
|
199
|
+
bounds?: { min: number; max: number } | null
|
|
200
|
+
}) {
|
|
201
|
+
const height = useScene((scene) => roomRelativeFloorHeight(scene.nodes, zoneId))
|
|
202
|
+
const unit = useViewer((s) => s.unit)
|
|
203
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
204
|
+
const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
|
|
205
|
+
const [draft, setDraft] = useState<number | null>(null)
|
|
206
|
+
const [error, setError] = useState<string | null>(null)
|
|
207
|
+
return (
|
|
208
|
+
<div className="flex flex-col gap-1">
|
|
209
|
+
<MetricControl
|
|
210
|
+
className="h-8 text-xs"
|
|
211
|
+
label="Height"
|
|
212
|
+
max={bounds?.max}
|
|
213
|
+
min={bounds?.min ?? -3}
|
|
214
|
+
onChange={setDraft}
|
|
215
|
+
onCommit={(value) => {
|
|
216
|
+
setDraft(null)
|
|
217
|
+
if (Math.abs(value - height) <= 1e-6) return
|
|
218
|
+
const result = setRoomRelativeFloorHeight(zoneId, value)
|
|
219
|
+
setError(result.status === 'conflict' ? floorHeightRefusalText(result, length) : null)
|
|
220
|
+
}}
|
|
221
|
+
precision={2}
|
|
222
|
+
step={0.05}
|
|
223
|
+
unit="m"
|
|
224
|
+
value={draft ?? height}
|
|
225
|
+
/>
|
|
226
|
+
{error && (
|
|
227
|
+
<p className="text-destructive text-xs" data-floor-height-error role="status">
|
|
228
|
+
{error}
|
|
229
|
+
</p>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A room standing on a drawn slab has no height of its own — the slab's
|
|
237
|
+
* elevation is the floor's — so instead of a height it names that slab (and
|
|
238
|
+
* how many other rooms stand on it) and selects it, as the tree would.
|
|
239
|
+
*/
|
|
240
|
+
export function DrawnFloorRow({ zoneId }: { zoneId: string }) {
|
|
241
|
+
const floor = useScene(
|
|
242
|
+
useShallow((scene) => {
|
|
243
|
+
const drawn = roomDrawnFloor(scene.nodes, zoneId)
|
|
244
|
+
if (!drawn) return null
|
|
245
|
+
const slab = scene.nodes[drawn.slabId as AnyNodeId]
|
|
246
|
+
return {
|
|
247
|
+
id: drawn.slabId,
|
|
248
|
+
name: (slab?.type === 'slab' && slab.name?.trim()) || 'Slab',
|
|
249
|
+
shared: drawn.sharedZoneIds.length,
|
|
250
|
+
ambiguous: drawn.ambiguous,
|
|
251
|
+
}
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
if (!floor) return null
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex min-h-7 items-center justify-between gap-3" data-drawn-floor={floor.id}>
|
|
257
|
+
<span className="min-w-0">
|
|
258
|
+
{floor.ambiguous ? 'Floor is several drawn slabs, mostly ' : 'Floor is a drawn slab: '}
|
|
259
|
+
<span className="text-foreground">{floor.name}</span>
|
|
260
|
+
{floor.shared > 0 && ` (shared with ${count(floor.shared, 'room')})`}
|
|
261
|
+
</span>
|
|
262
|
+
<button
|
|
263
|
+
className={cn(pill, 'shrink-0')}
|
|
264
|
+
data-select-drawn-floor
|
|
265
|
+
onClick={() => openFloorFoundation(floor.id)}
|
|
266
|
+
type="button"
|
|
267
|
+
>
|
|
268
|
+
Select
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* "Built on: Shared floor ▾" — the floor the room stands on, picked from the
|
|
276
|
+
* floors it touches on its level (each by its footprint name: "Shared floor",
|
|
277
|
+
* "Lanai floor", a name the user gave it) or a new floor of its own. Hidden
|
|
278
|
+
* where there is no choice (see `roomBuiltOn`). A refused move says why,
|
|
279
|
+
* right here.
|
|
280
|
+
*/
|
|
281
|
+
export function BuiltOnField({ zoneId }: { zoneId: string }) {
|
|
282
|
+
const nodes = useScene((scene) => scene.nodes)
|
|
283
|
+
const model = useMemo(() => roomBuiltOn(nodes, zoneId), [nodes, zoneId])
|
|
284
|
+
const [error, setError] = useState<string | null>(null)
|
|
285
|
+
if (!model) return null
|
|
286
|
+
const choose = (key: string | null) => {
|
|
287
|
+
const result = setRoomFloor(zoneId, key)
|
|
288
|
+
setError(result.status === 'conflict' ? result.message : null)
|
|
289
|
+
}
|
|
290
|
+
return (
|
|
291
|
+
<div className="flex flex-col gap-1" data-built-on={model.current.key ?? 'shared'}>
|
|
292
|
+
<PartField label="Built on">
|
|
293
|
+
<DropdownMenu>
|
|
294
|
+
<DropdownMenuTrigger asChild>
|
|
295
|
+
<button
|
|
296
|
+
aria-label="Built on"
|
|
297
|
+
className="flex min-w-0 items-center gap-1.5 rounded-full border border-border/50 px-2.5 py-0.5 text-foreground text-xs transition-colors hover:bg-accent"
|
|
298
|
+
data-built-on-trigger
|
|
299
|
+
type="button"
|
|
300
|
+
>
|
|
301
|
+
<span className="truncate">{model.current.name}</span>
|
|
302
|
+
<ChevronDown className="size-3 shrink-0 text-muted-foreground" />
|
|
303
|
+
</button>
|
|
304
|
+
</DropdownMenuTrigger>
|
|
305
|
+
<DropdownMenuContent align="end" className="min-w-40">
|
|
306
|
+
{model.choices.map((choice) => (
|
|
307
|
+
<DropdownMenuItem
|
|
308
|
+
data-built-on-choice={choice.key ?? 'shared'}
|
|
309
|
+
key={choice.plateId ?? 'shared'}
|
|
310
|
+
onSelect={() => {
|
|
311
|
+
if (choice.plateId !== model.current.plateId) choose(choice.key)
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<span className="flex-1 truncate">{choice.name}</span>
|
|
315
|
+
{choice.plateId === model.current.plateId && <Check className="size-3.5" />}
|
|
316
|
+
</DropdownMenuItem>
|
|
317
|
+
))}
|
|
318
|
+
<DropdownMenuSeparator />
|
|
319
|
+
<DropdownMenuItem data-built-on-choice="new" onSelect={() => choose('new')}>
|
|
320
|
+
+ New floor
|
|
321
|
+
</DropdownMenuItem>
|
|
322
|
+
</DropdownMenuContent>
|
|
323
|
+
</DropdownMenu>
|
|
324
|
+
</PartField>
|
|
325
|
+
{error && (
|
|
326
|
+
<p className="text-destructive text-xs" data-built-on-error role="status">
|
|
327
|
+
{error}
|
|
328
|
+
</p>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* How tall the room is inside: floor to its ceiling, or to its walls' tops
|
|
336
|
+
* without one. Null when neither exists.
|
|
337
|
+
*/
|
|
338
|
+
export function roomInsideHeight(
|
|
339
|
+
room: RoomSelectionRecord,
|
|
340
|
+
nodes: Record<string, import('@pascal-app/core').AnyNode>,
|
|
341
|
+
): { floor: number; ceiling: number | null; height: number | null } {
|
|
342
|
+
const { floorY, wallTops, ceiling } = resolveRoomAssemblyHeights(room, nodes)
|
|
343
|
+
const top = ceiling?.y ?? (wallTops.size ? Math.max(...wallTops.values()) : null)
|
|
344
|
+
return {
|
|
345
|
+
floor: floorY,
|
|
346
|
+
ceiling: ceiling ? ceiling.y - floorY : null,
|
|
347
|
+
height: top === null ? null : top - floorY,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* "Sits on: Shared floor · Raised 0.30 m ›" — what the room's floor is built
|
|
353
|
+
* as and how it stands, opening its panel: the footprint's Floor & foundation
|
|
354
|
+
* (a room on its own floor, its own plate: "Lanai floor"), or the drawn slab
|
|
355
|
+
* the floor was taken from ("· Drawn slab"). A room without a floor says so
|
|
356
|
+
* rather than naming the footprint next to it.
|
|
357
|
+
*/
|
|
358
|
+
function SitsOn({ zoneId, noFloor }: { zoneId: string; noFloor: boolean }) {
|
|
359
|
+
const unit = useViewer((s) => s.unit)
|
|
360
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
361
|
+
const summary = useScene(
|
|
362
|
+
useShallow((scene) => {
|
|
363
|
+
const plate = roomFloorOwner(scene.nodes, zoneId) ?? roomFootprint(scene.nodes, zoneId)
|
|
364
|
+
if (!plate) return { id: '', name: '', drawn: false, lift: 0, supported: false }
|
|
365
|
+
if (plate.plateRole !== 'base')
|
|
366
|
+
return {
|
|
367
|
+
id: plate.id as string,
|
|
368
|
+
name: plate.name?.trim() || 'Slab',
|
|
369
|
+
drawn: true,
|
|
370
|
+
lift: 0,
|
|
371
|
+
supported: false,
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
id: plate.id as string,
|
|
375
|
+
name: floorFootprintName(scene.nodes, plate),
|
|
376
|
+
drawn: false,
|
|
377
|
+
...footprintStanding(scene.nodes, plate),
|
|
378
|
+
}
|
|
379
|
+
}),
|
|
380
|
+
)
|
|
381
|
+
const preset = summary.drawn
|
|
382
|
+
? 'Drawn slab'
|
|
383
|
+
: Math.abs(summary.lift) > 0.005
|
|
384
|
+
? `${summary.lift > 0 ? 'Raised' : 'Lowered'} ${formatLinearMeasurement(Math.abs(summary.lift), unit, metricNotation)}`
|
|
385
|
+
: summary.supported
|
|
386
|
+
? 'On the walls below'
|
|
387
|
+
: 'On the ground'
|
|
388
|
+
if (noFloor)
|
|
389
|
+
return (
|
|
390
|
+
<span className="mb-2 ml-[42px] text-muted-foreground text-xs" data-sits-on="none">
|
|
391
|
+
No floor
|
|
392
|
+
</span>
|
|
393
|
+
)
|
|
394
|
+
if (!summary.id) return null
|
|
395
|
+
return (
|
|
396
|
+
<button
|
|
397
|
+
className="mb-2 ml-[42px] flex items-center gap-1 text-left text-muted-foreground text-xs transition-colors hover:text-foreground"
|
|
398
|
+
data-sits-on={summary.id}
|
|
399
|
+
onClick={() => openFloorFoundation(summary.id)}
|
|
400
|
+
type="button"
|
|
401
|
+
>
|
|
402
|
+
<span>
|
|
403
|
+
Sits on: <span className="text-foreground">{summary.name}</span> · {preset}
|
|
404
|
+
</span>
|
|
405
|
+
<ChevronRight className="size-3 shrink-0" />
|
|
406
|
+
</button>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The room's floor construction, edited from the room: its thickness, through
|
|
412
|
+
* core's `setRoomFloorConstruction` (the command MCP uses too), on whatever the
|
|
413
|
+
* floor is built as. A drawn slab takes thickness and finishes only — it has
|
|
414
|
+
* no footprint height or foundation, and says so.
|
|
415
|
+
*/
|
|
416
|
+
export function RoomFloorConstructionField({ zoneId }: { zoneId: string }) {
|
|
417
|
+
const owner = useScene(
|
|
418
|
+
useShallow((scene) => {
|
|
419
|
+
// Several drawn slabs show in the room: no one thickness to edit here.
|
|
420
|
+
if (roomDrawnFloor(scene.nodes, zoneId)?.ambiguous) return null
|
|
421
|
+
const plate = roomFloorOwner(scene.nodes, zoneId)
|
|
422
|
+
return plate
|
|
423
|
+
? {
|
|
424
|
+
id: plate.id as string,
|
|
425
|
+
thickness: plate.thickness,
|
|
426
|
+
drawn: plate.plateRole !== 'base',
|
|
427
|
+
// A slab on the ground has nothing under it to z-fight; it may be thinner.
|
|
428
|
+
min:
|
|
429
|
+
plate.plateRole === 'base' && floorPlateAtGroundContact(scene.nodes, plate)
|
|
430
|
+
? MIN_GROUND_FLOOR_THICKNESS
|
|
431
|
+
: MIN_SLAB_THICKNESS,
|
|
432
|
+
}
|
|
433
|
+
: null
|
|
434
|
+
}),
|
|
435
|
+
)
|
|
436
|
+
const [draft, setDraft] = useState<number | null>(null)
|
|
437
|
+
const [error, setError] = useState<string | null>(null)
|
|
438
|
+
if (!owner) return null
|
|
439
|
+
return (
|
|
440
|
+
<div className="flex flex-col gap-1" data-room-floor-construction={owner.id}>
|
|
441
|
+
<MetricControl
|
|
442
|
+
className="h-8 text-xs"
|
|
443
|
+
label="Floor thickness"
|
|
444
|
+
max={1}
|
|
445
|
+
min={owner.min}
|
|
446
|
+
onChange={setDraft}
|
|
447
|
+
onCommit={(value) => {
|
|
448
|
+
setDraft(null)
|
|
449
|
+
if (Math.abs(value - owner.thickness) <= 1e-6) return
|
|
450
|
+
setError(applyRoomFloorConstruction(zoneId, { thickness: value }, owner.id))
|
|
451
|
+
}}
|
|
452
|
+
precision={2}
|
|
453
|
+
step={0.01}
|
|
454
|
+
unit="m"
|
|
455
|
+
value={draft ?? owner.thickness}
|
|
456
|
+
/>
|
|
457
|
+
{owner.drawn && (
|
|
458
|
+
<p data-drawn-slab-note>
|
|
459
|
+
A drawn slab: set its thickness and finishes here. It has no footprint height or
|
|
460
|
+
foundation.
|
|
461
|
+
</p>
|
|
462
|
+
)}
|
|
463
|
+
{error && (
|
|
464
|
+
<p className="text-destructive text-xs" data-floor-construction-error role="status">
|
|
465
|
+
{error}
|
|
466
|
+
</p>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* How a footprint stands: on the ground, its foundation height ("Raised 0.30 m"
|
|
474
|
+
* or on the ground); upstairs, its lift above resting on the walls below.
|
|
475
|
+
*/
|
|
476
|
+
function footprintStanding(
|
|
477
|
+
nodes: Record<string, import('@pascal-app/core').AnyNode>,
|
|
478
|
+
plate: import('@pascal-app/core').SlabNode,
|
|
479
|
+
) {
|
|
480
|
+
const supported = floorFootprintSupportClass(nodes, plate) === 'supported'
|
|
481
|
+
return {
|
|
482
|
+
supported,
|
|
483
|
+
lift: supported ? floorLift(nodes, plate) : foundationHeight(nodes, plate),
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Rooms sharing any of `wallIds` with `zoneId`, by name. */
|
|
488
|
+
function sharingRoomNames(
|
|
489
|
+
nodes: Record<string, import('@pascal-app/core').AnyNode>,
|
|
490
|
+
zoneId: string,
|
|
491
|
+
wallIds: readonly string[],
|
|
492
|
+
) {
|
|
493
|
+
return Object.values(nodes).flatMap((node) =>
|
|
494
|
+
node.type === 'zone' &&
|
|
495
|
+
node.id !== zoneId &&
|
|
496
|
+
node.spaceRole === 'room' &&
|
|
497
|
+
node.boundaryWallIds.some((id) => wallIds.includes(id))
|
|
498
|
+
? [node.name?.trim() || 'the next room']
|
|
499
|
+
: [],
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* "Built with": the room's floor, walls and ceiling, each included or not
|
|
505
|
+
* with its switch, and its settings a chevron away. Then Add mezzanine.
|
|
506
|
+
*/
|
|
507
|
+
export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
|
|
508
|
+
const zoneId = room.zoneId
|
|
509
|
+
const state = useScene((scene) => roomConstructionState(scene.nodes, zoneId))
|
|
510
|
+
const nodes = useScene((scene) => scene.nodes)
|
|
511
|
+
const unit = useViewer((s) => s.unit)
|
|
512
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
513
|
+
const [expanded, setExpanded] = useState<RoomConstructionPart | null>(null)
|
|
514
|
+
const [message, setMessage] = useState<{ text: string; conflict: boolean } | null>(null)
|
|
515
|
+
if (!state) return null
|
|
516
|
+
const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
|
|
517
|
+
const report = (result: RoomConstructionResult, unchanged = 'Nothing to change.') =>
|
|
518
|
+
setMessage(
|
|
519
|
+
result.status === 'conflict'
|
|
520
|
+
? { text: result.message, conflict: true }
|
|
521
|
+
: result.status === 'unchanged'
|
|
522
|
+
? { text: unchanged, conflict: false }
|
|
523
|
+
: null,
|
|
524
|
+
)
|
|
525
|
+
const toggle = (part: RoomConstructionPart) => () =>
|
|
526
|
+
setExpanded((current) => (current === part ? null : part))
|
|
527
|
+
const drawnFloor = roomDrawnFloor(nodes, zoneId)
|
|
528
|
+
const partSwitch = (part: RoomConstructionPart, label: string) => {
|
|
529
|
+
const { state: presence, actions } = state[part]
|
|
530
|
+
if (part === 'walls') {
|
|
531
|
+
const walls = wallsSwitch(state)
|
|
532
|
+
return (
|
|
533
|
+
<Switch
|
|
534
|
+
aria-checked={walls.mixed ? 'mixed' : walls.checked}
|
|
535
|
+
aria-label="Include walls"
|
|
536
|
+
checked={walls.checked}
|
|
537
|
+
className={cn(
|
|
538
|
+
walls.mixed && 'data-[state=unchecked]:bg-primary/45 [&>span]:translate-x-2!',
|
|
539
|
+
)}
|
|
540
|
+
data-walls-switch={presence}
|
|
541
|
+
disabled={walls.disabled}
|
|
542
|
+
onCheckedChange={() =>
|
|
543
|
+
report(
|
|
544
|
+
walls.command === 'remove'
|
|
545
|
+
? removeRoomConstruction(zoneId, 'walls')
|
|
546
|
+
: addRoomConstruction(zoneId, 'walls'),
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
/>
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
if (part === 'floor' && drawnFloor) {
|
|
553
|
+
// A drawn-slab floor is the slab: the switch only brings back a floor
|
|
554
|
+
// switched off before; the slab itself is edited or deleted on its own.
|
|
555
|
+
return (
|
|
556
|
+
<Tooltip>
|
|
557
|
+
<TooltipTrigger asChild>
|
|
558
|
+
{/* The span takes the hover: a disabled switch gets no pointer events. */}
|
|
559
|
+
<span
|
|
560
|
+
className={cn('inline-flex', !actions.add && 'cursor-not-allowed')}
|
|
561
|
+
data-drawn-floor-switch
|
|
562
|
+
>
|
|
563
|
+
<Switch
|
|
564
|
+
aria-label="Include floor"
|
|
565
|
+
checked={state.floor.intent}
|
|
566
|
+
className="disabled:pointer-events-none"
|
|
567
|
+
disabled={!actions.add}
|
|
568
|
+
onCheckedChange={() => report(addRoomConstruction(zoneId, 'floor'))}
|
|
569
|
+
/>
|
|
570
|
+
</span>
|
|
571
|
+
</TooltipTrigger>
|
|
572
|
+
{!actions.add && (
|
|
573
|
+
<TooltipContent side="top" sideOffset={6}>
|
|
574
|
+
{DRAWN_FLOOR_SWITCH_HINT}
|
|
575
|
+
</TooltipContent>
|
|
576
|
+
)}
|
|
577
|
+
</Tooltip>
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
const on = presence !== 'absent'
|
|
581
|
+
return (
|
|
582
|
+
<Switch
|
|
583
|
+
aria-label={`Include ${label.toLowerCase()}`}
|
|
584
|
+
checked={on}
|
|
585
|
+
disabled={on ? !actions.remove : !actions.add}
|
|
586
|
+
onCheckedChange={(next) =>
|
|
587
|
+
report(next ? addRoomConstruction(zoneId, part) : removeRoomConstruction(zoneId, part))
|
|
588
|
+
}
|
|
589
|
+
/>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const floorElevation = roomRelativeFloorHeight(nodes, zoneId)
|
|
594
|
+
const drawnSlab = drawnFloor ? nodes[drawnFloor.slabId as AnyNodeId] : undefined
|
|
595
|
+
const builtOn = roomBuiltOn(nodes, zoneId)
|
|
596
|
+
const floorMeta =
|
|
597
|
+
state.floor.state === 'partial'
|
|
598
|
+
? `Partial · ${Math.round(state.floor.coverage * 100)}% covered`
|
|
599
|
+
: drawnSlab?.type === 'slab'
|
|
600
|
+
? `Drawn slab at ${length(drawnSlab.elevation)}`
|
|
601
|
+
: builtOn && builtOn.current.key !== null
|
|
602
|
+
? separateFloorSummary(
|
|
603
|
+
{ ...builtOn, offset: separateFloorOffset(nodes, zoneId) },
|
|
604
|
+
length,
|
|
605
|
+
)
|
|
606
|
+
: Math.abs(floorElevation) < 0.005
|
|
607
|
+
? 'At floor level'
|
|
608
|
+
: `${length(Math.abs(floorElevation))} ${floorElevation > 0 ? 'up' : 'down'}`
|
|
609
|
+
// A room on its own floor moves within its plate's range (on the ground, its foundation's).
|
|
610
|
+
const ownBounds = ownFloorElevationBounds(nodes, zoneId)
|
|
611
|
+
const floorBase = ownBounds ? roomFloorBase(nodes, zoneId) : 0
|
|
612
|
+
const heightBounds = ownBounds && {
|
|
613
|
+
min: ownBounds.min - floorBase,
|
|
614
|
+
max: ownBounds.max - floorBase,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const wallIds = room.boundaryWallIds
|
|
618
|
+
const openSides = state.walls.separators.length
|
|
619
|
+
const shared = sharingRoomNames(nodes, zoneId, state.walls.sharedWallIds)
|
|
620
|
+
const wallsMeta =
|
|
621
|
+
state.walls.state === 'partial'
|
|
622
|
+
? `Partial · ${count(wallIds.length, 'wall')}, ${count(openSides, 'open side')}`
|
|
623
|
+
: count(wallIds.length, 'wall')
|
|
624
|
+
const locked = roomOutsideFacesLocked(nodes, wallIds)
|
|
625
|
+
|
|
626
|
+
const ceilingHeight = state.ceiling.ceilingId ? roomInsideHeight(room, nodes).ceiling : null
|
|
627
|
+
const ceilingMeta =
|
|
628
|
+
state.ceiling.state === 'partial'
|
|
629
|
+
? 'Hand-drawn ceiling'
|
|
630
|
+
: ceilingHeight !== null
|
|
631
|
+
? `At ${length(ceilingHeight)}`
|
|
632
|
+
: 'Flat, at wall height'
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<div className="flex flex-col" data-room-construction={zoneId}>
|
|
636
|
+
<h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">Built with</h3>
|
|
637
|
+
<div className="flex flex-col gap-1">
|
|
638
|
+
<ConstructionPart
|
|
639
|
+
control={partSwitch('floor', 'Floor')}
|
|
640
|
+
expanded={expanded === 'floor'}
|
|
641
|
+
icon="/icons/floor.webp"
|
|
642
|
+
label="Floor"
|
|
643
|
+
meta={floorMeta}
|
|
644
|
+
on={state.floor.state !== 'absent'}
|
|
645
|
+
footer={<SitsOn noFloor={state.floor.state === 'absent'} zoneId={zoneId} />}
|
|
646
|
+
onToggleExpanded={toggle('floor')}
|
|
647
|
+
part="floor"
|
|
648
|
+
>
|
|
649
|
+
<BuiltOnField zoneId={zoneId} />
|
|
650
|
+
{drawnFloor ? (
|
|
651
|
+
<DrawnFloorRow zoneId={zoneId} />
|
|
652
|
+
) : (
|
|
653
|
+
<FloorHeightField bounds={heightBounds} zoneId={zoneId} />
|
|
654
|
+
)}
|
|
655
|
+
<RoomFloorConstructionField zoneId={zoneId} />
|
|
656
|
+
<RoomOpeningsField surface="floor" zoneId={zoneId} />
|
|
657
|
+
</ConstructionPart>
|
|
658
|
+
<ConstructionPart
|
|
659
|
+
control={partSwitch('walls', 'Walls')}
|
|
660
|
+
expanded={expanded === 'walls'}
|
|
661
|
+
icon="/icons/wall.webp"
|
|
662
|
+
label="Walls"
|
|
663
|
+
meta={wallsMeta}
|
|
664
|
+
on={state.walls.state !== 'absent'}
|
|
665
|
+
onToggleExpanded={toggle('walls')}
|
|
666
|
+
part="walls"
|
|
667
|
+
>
|
|
668
|
+
<PartField label="Walls">
|
|
669
|
+
<span className="font-mono text-foreground tabular-nums">{wallIds.length}</span>
|
|
670
|
+
</PartField>
|
|
671
|
+
{state.walls.state === 'partial' && state.walls.actions.add && (
|
|
672
|
+
<PartField label={`${count(openSides, 'open side')}`}>
|
|
673
|
+
<button
|
|
674
|
+
className={pill}
|
|
675
|
+
onClick={() => report(addRoomConstruction(zoneId, 'walls'))}
|
|
676
|
+
type="button"
|
|
677
|
+
>
|
|
678
|
+
Close with walls
|
|
679
|
+
</button>
|
|
680
|
+
</PartField>
|
|
681
|
+
)}
|
|
682
|
+
{shared.length > 0 && (
|
|
683
|
+
<p data-shared-walls-note>
|
|
684
|
+
Turning walls off keeps the ones shared with {[...new Set(shared)].join(', ')}.
|
|
685
|
+
</p>
|
|
686
|
+
)}
|
|
687
|
+
{locked !== null && (
|
|
688
|
+
<div className="mt-1 flex items-start gap-3" data-lock-outside-faces>
|
|
689
|
+
<span className="flex flex-1 flex-col gap-0.5">
|
|
690
|
+
<span className="font-medium text-[13px] text-foreground">
|
|
691
|
+
Keep outside dimensions
|
|
692
|
+
</span>
|
|
693
|
+
<span>Walls thicken inward, so the building's outer size stays the same.</span>
|
|
694
|
+
</span>
|
|
695
|
+
<Switch
|
|
696
|
+
aria-label="Keep outside dimensions"
|
|
697
|
+
checked={locked}
|
|
698
|
+
onCheckedChange={(next) =>
|
|
699
|
+
report(next ? lockRoomOutsideFaces(zoneId) : unlockRoomOutsideFaces(wallIds))
|
|
700
|
+
}
|
|
701
|
+
/>
|
|
702
|
+
</div>
|
|
703
|
+
)}
|
|
704
|
+
</ConstructionPart>
|
|
705
|
+
<ConstructionPart
|
|
706
|
+
control={partSwitch('ceiling', 'Ceiling')}
|
|
707
|
+
expanded={expanded === 'ceiling'}
|
|
708
|
+
icon="/icons/ceiling.webp"
|
|
709
|
+
label="Ceiling"
|
|
710
|
+
meta={ceilingMeta}
|
|
711
|
+
on={state.ceiling.state !== 'absent'}
|
|
712
|
+
onToggleExpanded={toggle('ceiling')}
|
|
713
|
+
part="ceiling"
|
|
714
|
+
>
|
|
715
|
+
{ceilingHeight !== null && (
|
|
716
|
+
<PartField label="Height">
|
|
717
|
+
<span className="font-mono text-foreground tabular-nums">
|
|
718
|
+
{length(ceilingHeight)}
|
|
719
|
+
</span>
|
|
720
|
+
</PartField>
|
|
721
|
+
)}
|
|
722
|
+
{state.ceiling.ceilingId && (
|
|
723
|
+
<PartField label="Shape">
|
|
724
|
+
<button className={pill} onClick={() => startCeilingEdit(zoneId)} type="button">
|
|
725
|
+
Edit shape
|
|
726
|
+
</button>
|
|
727
|
+
</PartField>
|
|
728
|
+
)}
|
|
729
|
+
{state.ceiling.ceilingId && <RoomOpeningsField surface="ceiling" zoneId={zoneId} />}
|
|
730
|
+
{(state.ceiling.actions.useExisting || state.ceiling.actions.replace) && (
|
|
731
|
+
<div className="flex items-center gap-2" data-construction-manual-ceiling>
|
|
732
|
+
<span className="flex-1">Hand-drawn ceiling</span>
|
|
733
|
+
{state.ceiling.actions.useExisting && (
|
|
734
|
+
<button
|
|
735
|
+
className={pill}
|
|
736
|
+
onClick={() => report(adoptExistingCeiling(zoneId))}
|
|
737
|
+
type="button"
|
|
738
|
+
>
|
|
739
|
+
Use existing
|
|
740
|
+
</button>
|
|
741
|
+
)}
|
|
742
|
+
{state.ceiling.actions.replace && (
|
|
743
|
+
<button
|
|
744
|
+
className={pill}
|
|
745
|
+
onClick={() => report(replaceRoomCeiling(zoneId))}
|
|
746
|
+
type="button"
|
|
747
|
+
>
|
|
748
|
+
Replace
|
|
749
|
+
</button>
|
|
750
|
+
)}
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</ConstructionPart>
|
|
754
|
+
</div>
|
|
755
|
+
{message && (
|
|
756
|
+
<span
|
|
757
|
+
className={cn(
|
|
758
|
+
'mt-2 text-xs',
|
|
759
|
+
message.conflict ? 'text-destructive' : 'text-muted-foreground',
|
|
760
|
+
)}
|
|
761
|
+
role="status"
|
|
762
|
+
>
|
|
763
|
+
{message.text}
|
|
764
|
+
</span>
|
|
765
|
+
)}
|
|
766
|
+
<ShapeChoice
|
|
767
|
+
className="mt-3 rounded-full border border-border border-dashed py-1.5 pr-1.5 pl-3.5 font-medium text-[13px] text-foreground"
|
|
768
|
+
data="add-mezzanine"
|
|
769
|
+
label="+ Add mezzanine"
|
|
770
|
+
onPick={(shape) => startMezzanineDraft(zoneId, shape)}
|
|
771
|
+
/>
|
|
772
|
+
</div>
|
|
773
|
+
)
|
|
774
|
+
}
|