@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type { AnyNodeId,
|
|
3
|
+
import type { AnyNodeId, CollectionId } from '@pascal-app/core'
|
|
4
4
|
import { useScene } from '@pascal-app/core'
|
|
5
5
|
import {
|
|
6
6
|
Check,
|
|
7
7
|
ChevronDown,
|
|
8
8
|
ChevronRight,
|
|
9
9
|
Layers,
|
|
10
|
+
Minus,
|
|
10
11
|
MoreHorizontal,
|
|
11
12
|
Pencil,
|
|
12
13
|
Plus,
|
|
@@ -29,12 +30,12 @@ import {
|
|
|
29
30
|
import { cn } from '../../../../lib/utils'
|
|
30
31
|
|
|
31
32
|
interface CollectionsPopoverProps {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/** The elements the popover adds to and removes from collections, together. */
|
|
34
|
+
nodeIds: AnyNodeId[]
|
|
34
35
|
children: React.ReactNode
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export function CollectionsPopover({
|
|
38
|
+
export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProps) {
|
|
38
39
|
const collections = useScene((s) => s.collections)
|
|
39
40
|
const nodes = useScene((s) => s.nodes)
|
|
40
41
|
const createCollection = useScene((s) => s.createCollection)
|
|
@@ -54,12 +55,11 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
|
|
|
54
55
|
const [deletingId, setDeletingId] = useState<CollectionId | null>(null)
|
|
55
56
|
const [expandedIds, setExpandedIds] = useState<Set<CollectionId>>(new Set())
|
|
56
57
|
|
|
57
|
-
const memberIds = collectionIds ?? []
|
|
58
58
|
const allCollections = Object.values(collections)
|
|
59
59
|
|
|
60
60
|
const handleCreate = () => {
|
|
61
61
|
if (!createName.trim()) return
|
|
62
|
-
createCollection(createName.trim(),
|
|
62
|
+
createCollection(createName.trim(), nodeIds)
|
|
63
63
|
setCreateName('')
|
|
64
64
|
setShowCreateInput(false)
|
|
65
65
|
}
|
|
@@ -71,10 +71,11 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const toggleMembership = (collectionId: CollectionId) => {
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const memberIds = collections[collectionId]?.nodeIds ?? []
|
|
75
|
+
if (nodeIds.every((id) => memberIds.includes(id))) {
|
|
76
|
+
removeFromCollection(collectionId, nodeIds)
|
|
76
77
|
} else {
|
|
77
|
-
addToCollection(collectionId,
|
|
78
|
+
addToCollection(collectionId, nodeIds)
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -161,13 +162,15 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
|
|
|
161
162
|
<div className="flex flex-col items-center justify-center gap-2 px-4 py-8 text-center">
|
|
162
163
|
<Layers className="h-6 w-6 text-muted-foreground/40" />
|
|
163
164
|
<p className="text-muted-foreground text-xs">
|
|
164
|
-
No collections yet. Create one to group
|
|
165
|
+
No collections yet. Create one to group elements together.
|
|
165
166
|
</p>
|
|
166
167
|
</div>
|
|
167
168
|
) : (
|
|
168
169
|
<ul className="divide-y divide-border/30">
|
|
169
170
|
{allCollections.map((collection) => {
|
|
170
|
-
const
|
|
171
|
+
const memberCount = nodeIds.filter((id) => collection.nodeIds.includes(id)).length
|
|
172
|
+
const isIn = memberCount === nodeIds.length
|
|
173
|
+
const isPartly = memberCount > 0 && !isIn
|
|
171
174
|
const isExpanded = expandedIds.has(collection.id)
|
|
172
175
|
const isRenaming = renamingId === collection.id
|
|
173
176
|
const isDeleting = deletingId === collection.id
|
|
@@ -268,10 +271,13 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
|
|
|
268
271
|
<div
|
|
269
272
|
className={cn(
|
|
270
273
|
'pointer-events-none flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors',
|
|
271
|
-
isIn
|
|
274
|
+
isIn || isPartly
|
|
275
|
+
? 'border-primary bg-primary/20 text-primary'
|
|
276
|
+
: 'border-border/50',
|
|
272
277
|
)}
|
|
273
278
|
>
|
|
274
279
|
{isIn && <Check className="h-2.5 w-2.5" />}
|
|
280
|
+
{isPartly && <Minus className="h-2.5 w-2.5" />}
|
|
275
281
|
</div>
|
|
276
282
|
|
|
277
283
|
{/* Expand toggle (only if has members) */}
|
|
@@ -290,7 +296,7 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
|
|
|
290
296
|
)}
|
|
291
297
|
|
|
292
298
|
{/* More dropdown */}
|
|
293
|
-
<DropdownMenu>
|
|
299
|
+
<DropdownMenu modal={false}>
|
|
294
300
|
<DropdownMenuTrigger asChild>
|
|
295
301
|
<button
|
|
296
302
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-colors hover:bg-white/10 hover:text-foreground group-hover:opacity-100"
|
|
@@ -332,7 +338,7 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
|
|
|
332
338
|
<span
|
|
333
339
|
className={cn(
|
|
334
340
|
'truncate text-[11px]',
|
|
335
|
-
nid
|
|
341
|
+
nodeIds.includes(nid)
|
|
336
342
|
? 'font-medium text-foreground'
|
|
337
343
|
: 'text-muted-foreground',
|
|
338
344
|
)}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type FloorFoundationPatch,
|
|
7
|
+
floorFootprintName,
|
|
8
|
+
floorFootprintSupportClass,
|
|
9
|
+
upperFloorHeightControl,
|
|
10
|
+
getLevelDisplayName,
|
|
11
|
+
getLevelElevations,
|
|
12
|
+
getStoredLevelHeight,
|
|
13
|
+
type LevelNode,
|
|
14
|
+
MIN_GROUND_FLOOR_THICKNESS,
|
|
15
|
+
MIN_SLAB_THICKNESS,
|
|
16
|
+
type SlabNode,
|
|
17
|
+
useScene,
|
|
18
|
+
} from '@pascal-app/core'
|
|
19
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
20
|
+
import { useRef, useState } from 'react'
|
|
21
|
+
import {
|
|
22
|
+
applyFloorFoundation,
|
|
23
|
+
beginFootprintHeightPreview,
|
|
24
|
+
beginFootprintThicknessPreview,
|
|
25
|
+
type FootprintPreset,
|
|
26
|
+
footprintHeightMinimum,
|
|
27
|
+
footprintHeightPatch,
|
|
28
|
+
footprintHeightValue,
|
|
29
|
+
footprintPreset,
|
|
30
|
+
footprintsToMoveTogether,
|
|
31
|
+
floorTopAboveGround,
|
|
32
|
+
presetPatch,
|
|
33
|
+
THICK_FLOOR_HINT,
|
|
34
|
+
thickFloorAdvice,
|
|
35
|
+
} from '../../../lib/floor-footprints'
|
|
36
|
+
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
37
|
+
import { FinishSwatch } from '../controls/finish-swatch'
|
|
38
|
+
import { MetricControl } from '../controls/metric-control'
|
|
39
|
+
import { SegmentedControl } from '../controls/segmented-control'
|
|
40
|
+
import { Button } from '../primitives/button'
|
|
41
|
+
import { PanelWrapper } from './panel-wrapper'
|
|
42
|
+
|
|
43
|
+
/** Unpainted finishes, as the plate renders them (nodes' slab slot defaults). */
|
|
44
|
+
const EDGE_DEFAULT = '#cccccc'
|
|
45
|
+
const FOUNDATION_DEFAULT = 'library:concrete-raw'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A side section through the footprint's edge, top to bottom in the order it
|
|
49
|
+
* is built. On the ground: the floor top, the slab, the foundation when the
|
|
50
|
+
* footprint is raised on one, the ground — the slab always sits on the
|
|
51
|
+
* foundation (or the ground), so the top is their sum. Upstairs: the floor
|
|
52
|
+
* plate resting on the walls of the storey below. Drawn to a readable scale,
|
|
53
|
+
* not to size.
|
|
54
|
+
*/
|
|
55
|
+
export function FloorSection({
|
|
56
|
+
supported,
|
|
57
|
+
raised,
|
|
58
|
+
lift = 0,
|
|
59
|
+
liftLabel = '',
|
|
60
|
+
foundationHeight = 0,
|
|
61
|
+
foundationLabel = '',
|
|
62
|
+
topLabel,
|
|
63
|
+
thickness,
|
|
64
|
+
thicknessLabel,
|
|
65
|
+
}: {
|
|
66
|
+
/** Upstairs (or over a basement): it rests on the walls below, never on a foundation. */
|
|
67
|
+
supported: boolean
|
|
68
|
+
/** On a foundation (ground-bearing footprints only). */
|
|
69
|
+
raised: boolean
|
|
70
|
+
/** Upstairs: meters the top sits above resting on the walls below. */
|
|
71
|
+
lift?: number
|
|
72
|
+
liftLabel?: string
|
|
73
|
+
/** On the ground: the foundation's height, meters. */
|
|
74
|
+
foundationHeight?: number
|
|
75
|
+
foundationLabel?: string
|
|
76
|
+
/** On the ground: the floor top above the ground (foundation + slab). */
|
|
77
|
+
topLabel?: string
|
|
78
|
+
thickness: number
|
|
79
|
+
thicknessLabel: string
|
|
80
|
+
}) {
|
|
81
|
+
const base = 100
|
|
82
|
+
const foundation = !supported && raised ? Math.min(40, 14 + foundationHeight * 30) : 0
|
|
83
|
+
const band = Math.max(7, Math.min(16, thickness * 60))
|
|
84
|
+
// Upstairs a lift moves the plate off its default position (on the walls below).
|
|
85
|
+
const upstairsLift =
|
|
86
|
+
supported && Math.abs(lift) > 0.005
|
|
87
|
+
? lift > 0
|
|
88
|
+
? Math.min(18, 6 + lift * 30)
|
|
89
|
+
: -Math.min(10, 4 + -lift * 30)
|
|
90
|
+
: 0
|
|
91
|
+
const bandBottom = base - foundation - upstairsLift
|
|
92
|
+
const bandTop = bandBottom - band
|
|
93
|
+
// Upstairs the lift is measured from resting on the walls below; on the
|
|
94
|
+
// ground the top is measured from the ground.
|
|
95
|
+
const measureFrom = supported ? base - band : base
|
|
96
|
+
const measured = supported ? Math.abs(lift) > 0.005 : true
|
|
97
|
+
const measureLabel = supported ? liftLabel : (topLabel ?? '')
|
|
98
|
+
const left = 64
|
|
99
|
+
const right = 164
|
|
100
|
+
const labels: { y: number; text: string; key: string }[] = [
|
|
101
|
+
{ key: 'top', y: bandTop - 9, text: 'Floor top' },
|
|
102
|
+
{
|
|
103
|
+
key: 'plate',
|
|
104
|
+
y: bandTop + band / 2,
|
|
105
|
+
text: supported ? `Floor plate · ${thicknessLabel}` : `Slab · ${thicknessLabel}`,
|
|
106
|
+
},
|
|
107
|
+
...(foundation
|
|
108
|
+
? [
|
|
109
|
+
{
|
|
110
|
+
key: 'foundation',
|
|
111
|
+
y: bandBottom + foundation / 2,
|
|
112
|
+
text: foundationLabel ? `Foundation · ${foundationLabel}` : 'Foundation',
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
: []),
|
|
116
|
+
{ key: 'base', y: base + 9, text: supported ? 'Walls below' : 'Ground' },
|
|
117
|
+
]
|
|
118
|
+
return (
|
|
119
|
+
<figure className="flex flex-col gap-1" data-floor-section>
|
|
120
|
+
<svg
|
|
121
|
+
aria-label={
|
|
122
|
+
supported
|
|
123
|
+
? `Floor plate ${thicknessLabel} thick, resting on the walls below`
|
|
124
|
+
: raised
|
|
125
|
+
? `Slab ${thicknessLabel} thick on a ${foundationLabel} foundation, floor top ${topLabel ?? ''} above the ground`
|
|
126
|
+
: `Slab ${thicknessLabel} thick on the ground, floor top ${topLabel ?? ''} above the ground`
|
|
127
|
+
}
|
|
128
|
+
className="h-auto w-full text-muted-foreground"
|
|
129
|
+
role="img"
|
|
130
|
+
viewBox="0 0 280 118"
|
|
131
|
+
>
|
|
132
|
+
{supported ? (
|
|
133
|
+
// The walls of the storey below, under the plate's edge.
|
|
134
|
+
<g data-section-walls-below>
|
|
135
|
+
<rect
|
|
136
|
+
className="fill-muted"
|
|
137
|
+
height={118 - bandBottom}
|
|
138
|
+
stroke="currentColor"
|
|
139
|
+
strokeOpacity="0.4"
|
|
140
|
+
width="14"
|
|
141
|
+
x={left + 6}
|
|
142
|
+
y={bandBottom}
|
|
143
|
+
/>
|
|
144
|
+
<line
|
|
145
|
+
opacity="0.6"
|
|
146
|
+
stroke="currentColor"
|
|
147
|
+
strokeDasharray="3 3"
|
|
148
|
+
x1="8"
|
|
149
|
+
x2={right}
|
|
150
|
+
y1={base}
|
|
151
|
+
y2={base}
|
|
152
|
+
/>
|
|
153
|
+
</g>
|
|
154
|
+
) : (
|
|
155
|
+
<g data-section-ground>
|
|
156
|
+
<line stroke="currentColor" strokeWidth="1" x1="8" x2="272" y1={base} y2={base} />
|
|
157
|
+
{Array.from({ length: 22 }, (_, i) => (
|
|
158
|
+
<line
|
|
159
|
+
key={i}
|
|
160
|
+
opacity="0.35"
|
|
161
|
+
stroke="currentColor"
|
|
162
|
+
strokeWidth="1"
|
|
163
|
+
x1={12 + i * 12}
|
|
164
|
+
x2={6 + i * 12}
|
|
165
|
+
y1={base + 1}
|
|
166
|
+
y2={base + 7}
|
|
167
|
+
/>
|
|
168
|
+
))}
|
|
169
|
+
</g>
|
|
170
|
+
)}
|
|
171
|
+
{/* A wall standing on the floor. */}
|
|
172
|
+
<rect
|
|
173
|
+
className="fill-muted"
|
|
174
|
+
height={Math.min(26, bandTop - 12)}
|
|
175
|
+
stroke="currentColor"
|
|
176
|
+
strokeOpacity="0.4"
|
|
177
|
+
width="14"
|
|
178
|
+
x={left + 6}
|
|
179
|
+
y={bandTop - Math.min(26, bandTop - 12)}
|
|
180
|
+
/>
|
|
181
|
+
{foundation > 0 && (
|
|
182
|
+
<rect
|
|
183
|
+
data-section-foundation
|
|
184
|
+
fill="#8a8a8a"
|
|
185
|
+
height={foundation}
|
|
186
|
+
stroke="currentColor"
|
|
187
|
+
strokeOpacity="0.5"
|
|
188
|
+
width={right - left - 8}
|
|
189
|
+
x={left + 4}
|
|
190
|
+
y={bandBottom}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
<rect
|
|
194
|
+
className="fill-foreground"
|
|
195
|
+
data-section-band
|
|
196
|
+
height={band}
|
|
197
|
+
width={right - left}
|
|
198
|
+
x={left}
|
|
199
|
+
y={bandTop}
|
|
200
|
+
/>
|
|
201
|
+
{/* The floor top. */}
|
|
202
|
+
<line
|
|
203
|
+
className="text-foreground"
|
|
204
|
+
stroke="currentColor"
|
|
205
|
+
strokeWidth="1.5"
|
|
206
|
+
x1={left - 4}
|
|
207
|
+
x2={right + 4}
|
|
208
|
+
y1={bandTop}
|
|
209
|
+
y2={bandTop}
|
|
210
|
+
/>
|
|
211
|
+
{measured && measureLabel && (
|
|
212
|
+
<g className="text-foreground" data-section-height>
|
|
213
|
+
<line stroke="currentColor" strokeWidth="1" x1="50" x2="50" y1={bandTop} y2={measureFrom} />
|
|
214
|
+
<line stroke="currentColor" strokeWidth="1" x1="45" x2="55" y1={bandTop} y2={bandTop} />
|
|
215
|
+
<line stroke="currentColor" strokeWidth="1" x1="45" x2="55" y1={measureFrom} y2={measureFrom} />
|
|
216
|
+
{supported && (
|
|
217
|
+
<line
|
|
218
|
+
opacity="0.6"
|
|
219
|
+
stroke="currentColor"
|
|
220
|
+
strokeDasharray="2 3"
|
|
221
|
+
x1="55"
|
|
222
|
+
x2={left}
|
|
223
|
+
y1={measureFrom}
|
|
224
|
+
y2={measureFrom}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
<text
|
|
228
|
+
className="fill-foreground font-mono"
|
|
229
|
+
dominantBaseline="middle"
|
|
230
|
+
fontSize="10"
|
|
231
|
+
textAnchor="end"
|
|
232
|
+
x="42"
|
|
233
|
+
y={(bandTop + measureFrom) / 2}
|
|
234
|
+
>
|
|
235
|
+
{measureLabel}
|
|
236
|
+
</text>
|
|
237
|
+
</g>
|
|
238
|
+
)}
|
|
239
|
+
{labels.map((label) => (
|
|
240
|
+
<g key={label.key}>
|
|
241
|
+
<line
|
|
242
|
+
opacity="0.6"
|
|
243
|
+
stroke="currentColor"
|
|
244
|
+
x1={right + 4}
|
|
245
|
+
x2={right + 12}
|
|
246
|
+
y1={label.y}
|
|
247
|
+
y2={label.y}
|
|
248
|
+
/>
|
|
249
|
+
<text
|
|
250
|
+
className="fill-foreground"
|
|
251
|
+
dominantBaseline="middle"
|
|
252
|
+
fontSize="10"
|
|
253
|
+
x={right + 16}
|
|
254
|
+
y={label.y}
|
|
255
|
+
>
|
|
256
|
+
{label.text}
|
|
257
|
+
</text>
|
|
258
|
+
</g>
|
|
259
|
+
))}
|
|
260
|
+
</svg>
|
|
261
|
+
<figcaption className="text-right text-[10px] text-muted-foreground/70">
|
|
262
|
+
Not to scale
|
|
263
|
+
</figcaption>
|
|
264
|
+
</figure>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
type Notice = {
|
|
269
|
+
message: string
|
|
270
|
+
/** The footprints to move together, and the change, when that would be accepted. */
|
|
271
|
+
together: { ids: string[]; patch: FloorFoundationPatch; lower: boolean } | null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function moveTogetherLabel(count: number, lower: boolean) {
|
|
275
|
+
const verb = lower ? 'Lower' : 'Raise'
|
|
276
|
+
return count === 2 ? `${verb} both` : `${verb} all ${count}`
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** "Floor plate · Ground floor", upstairs "Floor plate · Floor 1 · rests on Ground floor". */
|
|
280
|
+
export function floorPlateKicker(nodes: Record<string, AnyNode>, plate: SlabNode): string {
|
|
281
|
+
const level = plate.parentId ? nodes[plate.parentId] : undefined
|
|
282
|
+
const levelName = level?.type === 'level' ? ` · ${getLevelDisplayName(level)}` : ''
|
|
283
|
+
// A supported floor rests on the storey right below it.
|
|
284
|
+
let below: LevelNode | null = null
|
|
285
|
+
if (level?.type === 'level' && floorFootprintSupportClass(nodes, plate) === 'supported')
|
|
286
|
+
for (const node of Object.values(nodes))
|
|
287
|
+
if (
|
|
288
|
+
node.type === 'level' &&
|
|
289
|
+
node.parentId === level.parentId &&
|
|
290
|
+
node.level < level.level &&
|
|
291
|
+
(!below || node.level > below.level)
|
|
292
|
+
)
|
|
293
|
+
below = node
|
|
294
|
+
const restsOn = below ? ` · rests on ${getLevelDisplayName(below)}` : ''
|
|
295
|
+
return `Floor plate${levelName}${restsOn}`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* What the panel shows for a footprint. An upper floor rests on the walls
|
|
300
|
+
* below: its height is its floor top, and raising it thickens the plate (the
|
|
301
|
+
* two stay in sync); it has no ground preset and no foundation. A footprint on
|
|
302
|
+
* the ground has two inputs, the slab thickness and the foundation height (0 on
|
|
303
|
+
* the ground); its floor top above the ground is their sum, read-only.
|
|
304
|
+
*/
|
|
305
|
+
export function floorFoundationModel(nodes: Record<string, AnyNode>, plate: SlabNode) {
|
|
306
|
+
const upper = upperFloorHeightControl(nodes, plate) !== null
|
|
307
|
+
const height = footprintHeightValue(nodes, plate)
|
|
308
|
+
const levelBase = plate.parentId
|
|
309
|
+
? (getLevelElevations(nodes as Record<AnyNodeId, AnyNode>).get(plate.parentId as AnyNodeId)
|
|
310
|
+
?.baseY ?? 0)
|
|
311
|
+
: 0
|
|
312
|
+
return {
|
|
313
|
+
upper,
|
|
314
|
+
kicker: floorPlateKicker(nodes, plate),
|
|
315
|
+
preset: upper ? ('ground' as const) : footprintPreset(nodes, plate),
|
|
316
|
+
/** Upstairs: the floor top (level-local). On the ground: the foundation height. */
|
|
317
|
+
height,
|
|
318
|
+
/** Upstairs: the floor top above the ground. On the ground: above the ground (foundation + slab). */
|
|
319
|
+
floorTop: upper ? levelBase + height : floorTopAboveGround(nodes, plate),
|
|
320
|
+
minHeight: footprintHeightMinimum(nodes, plate),
|
|
321
|
+
minThickness: upper ? MIN_SLAB_THICKNESS : MIN_GROUND_FLOOR_THICKNESS,
|
|
322
|
+
underFloor: !upper,
|
|
323
|
+
thickFloor: thickFloorAdvice(nodes, plate),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* "Floor & foundation" for one footprint (the house, a shed, an upper storey),
|
|
329
|
+
* in one column in the order it is built: the section, the slab (thickness,
|
|
330
|
+
* edge finish), then what is under the floor — on a ground-bearing footprint
|
|
331
|
+
* only — and the floor top that follows. Scrubbing the foundation height moves
|
|
332
|
+
* the building live and lands as one undo step.
|
|
333
|
+
*/
|
|
334
|
+
export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClose: () => void }) {
|
|
335
|
+
const nodes = useScene((state) => state.nodes)
|
|
336
|
+
const unit = useViewer((s) => s.unit)
|
|
337
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
338
|
+
const [notice, setNotice] = useState<Notice | null>(null)
|
|
339
|
+
const scrub = useRef<ReturnType<typeof beginFootprintHeightPreview> | null>(null)
|
|
340
|
+
const thicknessScrub = useRef<ReturnType<typeof beginFootprintThicknessPreview> | null>(null)
|
|
341
|
+
const plate = nodes[node.id as AnyNodeId]
|
|
342
|
+
if (plate?.type !== 'slab' || plate.plateRole !== 'base') return null
|
|
343
|
+
const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
|
|
344
|
+
const { upper, kicker, preset, height, floorTop, minHeight, minThickness, underFloor, thickFloor } =
|
|
345
|
+
floorFoundationModel(nodes, plate)
|
|
346
|
+
const level = plate.parentId ? nodes[plate.parentId as AnyNodeId] : undefined
|
|
347
|
+
const maxHeight = level?.type === 'level' ? getStoredLevelHeight(level) - 0.5 : 3
|
|
348
|
+
// A refused change says why, in the panel's words, never as an error; when
|
|
349
|
+
// only the shared-storey rule refused it, it offers to move every footprint
|
|
350
|
+
// under that storey together (checked once the change settles, not per frame).
|
|
351
|
+
const report = (message: string | null, patch?: FloorFoundationPatch) => {
|
|
352
|
+
if (!message) return setNotice(null)
|
|
353
|
+
const current = useScene.getState().nodes
|
|
354
|
+
const together = patch ? footprintsToMoveTogether(current, plate.id, patch) : null
|
|
355
|
+
const latest = current[plate.id as AnyNodeId]
|
|
356
|
+
const lower =
|
|
357
|
+
latest?.type !== 'slab'
|
|
358
|
+
? false
|
|
359
|
+
: patch?.thickness !== undefined
|
|
360
|
+
? patch.thickness < latest.thickness
|
|
361
|
+
: patch?.foundationHeight !== undefined
|
|
362
|
+
? patch.foundationHeight < footprintHeightValue(current, latest)
|
|
363
|
+
: patch?.floorHeight == null ||
|
|
364
|
+
patch.floorHeight < (latest.floorHeight ?? latest.elevation)
|
|
365
|
+
setNotice({ message, together: together && patch ? { ids: together, patch, lower } : null })
|
|
366
|
+
}
|
|
367
|
+
const heightPatchFor = (value: number) => {
|
|
368
|
+
const current = useScene.getState().nodes
|
|
369
|
+
const latest = current[plate.id as AnyNodeId]
|
|
370
|
+
return latest?.type === 'slab' ? footprintHeightPatch(current, latest, value) : undefined
|
|
371
|
+
}
|
|
372
|
+
// A scrub previews live and lands once on release; a typed value lands directly.
|
|
373
|
+
const previewHeight = (value: number) => {
|
|
374
|
+
scrub.current ??= beginFootprintHeightPreview(plate.id)
|
|
375
|
+
report(scrub.current.preview(value))
|
|
376
|
+
}
|
|
377
|
+
const commitHeight = (value: number) => {
|
|
378
|
+
const session = scrub.current ?? beginFootprintHeightPreview(plate.id)
|
|
379
|
+
scrub.current = null
|
|
380
|
+
const message = session.commit(value)
|
|
381
|
+
report(message, message ? heightPatchFor(value) : undefined)
|
|
382
|
+
}
|
|
383
|
+
// Upstairs the underside stays on the walls below: the thickness is the one
|
|
384
|
+
// control, and it moves the floor top (and the storeys above) with it.
|
|
385
|
+
const underside = height - plate.thickness
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<PanelWrapper
|
|
389
|
+
icon="/icons/floor.webp"
|
|
390
|
+
kicker={kicker}
|
|
391
|
+
onClose={onClose}
|
|
392
|
+
title={floorFootprintName(nodes, plate)}
|
|
393
|
+
>
|
|
394
|
+
<div
|
|
395
|
+
className="flex flex-col gap-3 px-4 pt-3 pb-4 text-sm"
|
|
396
|
+
data-floor-foundation={plate.id}
|
|
397
|
+
data-support={upper ? 'supported' : 'ground-bearing'}
|
|
398
|
+
>
|
|
399
|
+
<FloorSection
|
|
400
|
+
foundationHeight={upper ? 0 : height}
|
|
401
|
+
foundationLabel={length(height)}
|
|
402
|
+
raised={preset === 'raised'}
|
|
403
|
+
supported={upper}
|
|
404
|
+
thickness={plate.thickness}
|
|
405
|
+
thicknessLabel={length(plate.thickness)}
|
|
406
|
+
topLabel={upper ? undefined : length(floorTop)}
|
|
407
|
+
/>
|
|
408
|
+
<section className="flex flex-col gap-1.5" data-floor-plate-group>
|
|
409
|
+
<h3 className="font-medium text-muted-foreground text-xs">
|
|
410
|
+
{upper ? 'Floor plate' : 'Slab'}
|
|
411
|
+
</h3>
|
|
412
|
+
{upper ? (
|
|
413
|
+
<div title="A thicker floor grows upward: the rooms below keep their height.">
|
|
414
|
+
<MetricControl
|
|
415
|
+
className="h-9 text-xs"
|
|
416
|
+
label="Floor thickness"
|
|
417
|
+
max={Math.max(MIN_SLAB_THICKNESS, maxHeight - underside)}
|
|
418
|
+
min={Math.max(MIN_SLAB_THICKNESS, minHeight - underside)}
|
|
419
|
+
onChange={(next) => previewHeight(underside + next)}
|
|
420
|
+
onCommit={(next) => commitHeight(underside + next)}
|
|
421
|
+
precision={2}
|
|
422
|
+
restoreOnCommit={false}
|
|
423
|
+
step={0.01}
|
|
424
|
+
unit="m"
|
|
425
|
+
value={plate.thickness}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
) : (
|
|
429
|
+
<div title="The slab sits on the foundation (or the ground): a thicker slab raises the floor top and everything on it.">
|
|
430
|
+
<MetricControl
|
|
431
|
+
className="h-9 text-xs"
|
|
432
|
+
label="Slab thickness"
|
|
433
|
+
max={1}
|
|
434
|
+
min={minThickness}
|
|
435
|
+
onChange={(next) => {
|
|
436
|
+
thicknessScrub.current ??= beginFootprintThicknessPreview(plate.id)
|
|
437
|
+
report(thicknessScrub.current.preview(Math.max(minThickness, next)))
|
|
438
|
+
}}
|
|
439
|
+
onCommit={(value) => {
|
|
440
|
+
const session =
|
|
441
|
+
thicknessScrub.current ?? beginFootprintThicknessPreview(plate.id)
|
|
442
|
+
thicknessScrub.current = null
|
|
443
|
+
const patch = { thickness: Math.max(minThickness, value) }
|
|
444
|
+
const message = session.commit(patch.thickness)
|
|
445
|
+
report(message, message ? patch : undefined)
|
|
446
|
+
}}
|
|
447
|
+
precision={2}
|
|
448
|
+
restoreOnCommit={false}
|
|
449
|
+
step={0.01}
|
|
450
|
+
unit="m"
|
|
451
|
+
value={plate.thickness}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
{upper && (
|
|
456
|
+
<p className="px-0.5 text-muted-foreground text-xs" data-floor-top>
|
|
457
|
+
Floor top +{length(floorTop)}
|
|
458
|
+
</p>
|
|
459
|
+
)}
|
|
460
|
+
<FinishSwatch
|
|
461
|
+
fallback={EDGE_DEFAULT}
|
|
462
|
+
hint="The floor's outside edge, all around the footprint."
|
|
463
|
+
label="Edge finish"
|
|
464
|
+
node={plate}
|
|
465
|
+
role="edge"
|
|
466
|
+
value={plate.slots?.edge}
|
|
467
|
+
/>
|
|
468
|
+
{thickFloor && (
|
|
469
|
+
<p className="text-muted-foreground text-xs" data-thick-floor-hint>
|
|
470
|
+
{THICK_FLOOR_HINT}
|
|
471
|
+
</p>
|
|
472
|
+
)}
|
|
473
|
+
</section>
|
|
474
|
+
{underFloor && (
|
|
475
|
+
<section className="flex flex-col gap-1.5" data-under-floor-group>
|
|
476
|
+
<h3 className="font-medium text-muted-foreground text-xs">Under the floor</h3>
|
|
477
|
+
<SegmentedControl
|
|
478
|
+
onChange={(next: FootprintPreset) => {
|
|
479
|
+
if (next === preset) return
|
|
480
|
+
const patch = presetPatch(nodes, plate, next)
|
|
481
|
+
report(applyFloorFoundation(plate.id, patch), patch)
|
|
482
|
+
}}
|
|
483
|
+
options={[
|
|
484
|
+
{ label: 'On the ground', value: 'ground' },
|
|
485
|
+
{ label: 'Raised on a foundation', value: 'raised' },
|
|
486
|
+
]}
|
|
487
|
+
value={preset}
|
|
488
|
+
/>
|
|
489
|
+
{preset === 'raised' && (
|
|
490
|
+
<>
|
|
491
|
+
<div title="The foundation stands on the ground; the slab sits on it.">
|
|
492
|
+
<MetricControl
|
|
493
|
+
className="h-9 text-xs"
|
|
494
|
+
label="Foundation height"
|
|
495
|
+
max={Math.max(0.05, maxHeight)}
|
|
496
|
+
min={minHeight}
|
|
497
|
+
onChange={previewHeight}
|
|
498
|
+
onCommit={commitHeight}
|
|
499
|
+
precision={2}
|
|
500
|
+
restoreOnCommit={false}
|
|
501
|
+
step={0.05}
|
|
502
|
+
unit="m"
|
|
503
|
+
value={height}
|
|
504
|
+
/>
|
|
505
|
+
</div>
|
|
506
|
+
<FinishSwatch
|
|
507
|
+
fallback={FOUNDATION_DEFAULT}
|
|
508
|
+
label="Foundation finish"
|
|
509
|
+
node={plate}
|
|
510
|
+
role="foundation"
|
|
511
|
+
value={
|
|
512
|
+
typeof plate.foundation?.material === 'string'
|
|
513
|
+
? plate.foundation.material
|
|
514
|
+
: undefined
|
|
515
|
+
}
|
|
516
|
+
/>
|
|
517
|
+
</>
|
|
518
|
+
)}
|
|
519
|
+
<p className="px-0.5 text-muted-foreground text-xs" data-floor-top>
|
|
520
|
+
Floor top {length(floorTop)}
|
|
521
|
+
</p>
|
|
522
|
+
</section>
|
|
523
|
+
)}
|
|
524
|
+
{notice && (
|
|
525
|
+
<div
|
|
526
|
+
className="flex flex-col items-start gap-2 rounded-lg bg-accent/30 px-2.5 py-2"
|
|
527
|
+
data-floor-foundation-notice
|
|
528
|
+
>
|
|
529
|
+
<p className="text-muted-foreground text-xs" role="status">
|
|
530
|
+
{notice.message}
|
|
531
|
+
</p>
|
|
532
|
+
{notice.together && (
|
|
533
|
+
<Button
|
|
534
|
+
className="rounded-full"
|
|
535
|
+
onClick={() => {
|
|
536
|
+
const { ids, patch } = notice.together!
|
|
537
|
+
report(applyFloorFoundation(ids, patch))
|
|
538
|
+
}}
|
|
539
|
+
size="sm"
|
|
540
|
+
type="button"
|
|
541
|
+
variant="outline"
|
|
542
|
+
>
|
|
543
|
+
{moveTogetherLabel(notice.together.ids.length, notice.together.lower)}
|
|
544
|
+
</Button>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</PanelWrapper>
|
|
550
|
+
)
|
|
551
|
+
}
|