@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,7 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type DeleteZonePayload,
|
|
6
|
+
type StructureNodes,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
10
|
+
import { cn } from '../../lib/utils'
|
|
11
|
+
import useDeleteConfirmation, {
|
|
12
|
+
type DeleteConfirmationRequest,
|
|
13
|
+
type RoomConstructionChange,
|
|
14
|
+
} from '../../store/use-delete-confirmation'
|
|
5
15
|
import {
|
|
6
16
|
Dialog,
|
|
7
17
|
DialogContent,
|
|
@@ -11,10 +21,268 @@ import {
|
|
|
11
21
|
DialogTitle,
|
|
12
22
|
} from '../ui/primitives/dialog'
|
|
13
23
|
|
|
24
|
+
const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
|
|
25
|
+
|
|
26
|
+
function sentenceList(parts: string[]) {
|
|
27
|
+
if (parts.length <= 1) return parts.join('')
|
|
28
|
+
return `${parts.slice(0, -1).join(', ')} and ${parts.at(-1)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A mezzanine takes its own plate (with its railing) and the ceiling core
|
|
33
|
+
* derived over it; kept items come down to the floor below.
|
|
34
|
+
*/
|
|
35
|
+
function mezzanineDeletionDescription(room: DeleteZonePayload, nodes: StructureNodes) {
|
|
36
|
+
const hasCeiling = Object.values(nodes).some(
|
|
37
|
+
(node) => node.type === 'ceiling' && node.zoneId === room.zoneId,
|
|
38
|
+
)
|
|
39
|
+
const sentences = [
|
|
40
|
+
hasCeiling
|
|
41
|
+
? 'Removes the mezzanine floor, its railing and the ceiling over it.'
|
|
42
|
+
: 'Removes the mezzanine floor and its railing.',
|
|
43
|
+
]
|
|
44
|
+
const items = room.itemIds.length
|
|
45
|
+
if (items)
|
|
46
|
+
sentences.push(
|
|
47
|
+
`${count(items, 'item')} ${items === 1 ? 'is' : 'are'} on it. Keep ${items === 1 ? 'it' : 'them'} on the floor below, or delete ${items === 1 ? 'it' : 'them'} too.`,
|
|
48
|
+
)
|
|
49
|
+
return sentences.join(' ')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An area Divide made goes back into the room across its dividing line: the
|
|
54
|
+
* line and the area's own finish go, the walls and anything standing in it stay.
|
|
55
|
+
*/
|
|
56
|
+
function mergeDescription(room: DeleteZonePayload, nodes: StructureNodes) {
|
|
57
|
+
const into = nodes[room.mergedIntoZoneId ?? '']?.name?.trim()
|
|
58
|
+
const lines = room.separatorIds.length === 1 ? 'dividing line' : 'dividing lines'
|
|
59
|
+
const sentences = [
|
|
60
|
+
`Merges it back into ${into || 'the room next to it'}: removes the ${lines} and its floor finish. The walls stay.`,
|
|
61
|
+
]
|
|
62
|
+
const items = room.itemIds.length
|
|
63
|
+
if (items)
|
|
64
|
+
sentences.push(
|
|
65
|
+
`${count(items, 'item')} inside ${items === 1 ? 'stays where it is' : 'stay where they are'}.`,
|
|
66
|
+
)
|
|
67
|
+
return sentences.join(' ')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function roomDeletionDescription(room: DeleteZonePayload, nodes: StructureNodes) {
|
|
71
|
+
if (isMezzanine(room, nodes)) return mezzanineDeletionDescription(room, nodes)
|
|
72
|
+
if (room.mode === 'merge') return mergeDescription(room, nodes)
|
|
73
|
+
const sentences: string[] = []
|
|
74
|
+
const typeCount = (type: string) =>
|
|
75
|
+
room.openingIds.filter((id) => nodes[id]?.type === type).length
|
|
76
|
+
const removed = [
|
|
77
|
+
room.wallIds.length ? count(room.wallIds.length, 'wall') : '',
|
|
78
|
+
room.separatorIds.length ? count(room.separatorIds.length, 'separator') : '',
|
|
79
|
+
typeCount('door') ? count(typeCount('door'), 'door') : '',
|
|
80
|
+
typeCount('window') ? count(typeCount('window'), 'window') : '',
|
|
81
|
+
].filter(Boolean)
|
|
82
|
+
sentences.push(
|
|
83
|
+
removed.length
|
|
84
|
+
? `Removes ${sentenceList(removed)}, with the floor finish and ceiling.`
|
|
85
|
+
: 'Removes the floor finish and ceiling.',
|
|
86
|
+
)
|
|
87
|
+
if (room.keptSharedWallIds.length + room.keptSharedSeparatorIds.length > 0)
|
|
88
|
+
sentences.push('Walls shared with other rooms stay.')
|
|
89
|
+
if (room.opensZoneIds.length) {
|
|
90
|
+
const names = room.opensZoneIds.map((id) => nodes[id]?.name?.trim() || 'the next room')
|
|
91
|
+
sentences.push(`Opens into ${sentenceList(names)}.`)
|
|
92
|
+
}
|
|
93
|
+
const items = room.itemIds.length
|
|
94
|
+
if (items)
|
|
95
|
+
sentences.push(
|
|
96
|
+
`${count(items, 'item')} ${items === 1 ? 'is' : 'are'} inside. Keep ${items === 1 ? 'it' : 'them'} where ${items === 1 ? 'it is' : 'they are'}, or delete ${items === 1 ? 'it' : 'them'} too.`,
|
|
97
|
+
)
|
|
98
|
+
return sentences.join(' ')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A hosted node's kind from its type and structure — never from its name. */
|
|
102
|
+
function hostedKind(node: AnyNode | undefined) {
|
|
103
|
+
if (node?.type === 'door' || node?.type === 'window') return node.type
|
|
104
|
+
if (node?.type === 'item' && node.asset.interactive?.effects.some((e) => e.kind === 'light'))
|
|
105
|
+
return 'light'
|
|
106
|
+
return 'item'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hostedName(node: AnyNode | undefined) {
|
|
110
|
+
return (node?.name || (node?.type === 'item' ? node.asset.name : '') || '').trim()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function shownNames(names: string[]) {
|
|
114
|
+
const named = [...new Set(names.filter(Boolean))]
|
|
115
|
+
return named.slice(0, 3).join(', ') + (named.length > 3 ? ', …' : '')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** "1 door (Front door), 2 windows and 1 light": counts per kind, a few names, no ids. */
|
|
119
|
+
export function hostedSummary(ids: readonly string[], nodes: StructureNodes) {
|
|
120
|
+
const groups = new Map<string, string[]>()
|
|
121
|
+
for (const id of ids) {
|
|
122
|
+
const kind = hostedKind(nodes[id])
|
|
123
|
+
groups.set(kind, [...(groups.get(kind) ?? []), hostedName(nodes[id])])
|
|
124
|
+
}
|
|
125
|
+
return sentenceList(
|
|
126
|
+
[...groups].map(([kind, names]) => {
|
|
127
|
+
const shown = shownNames(names)
|
|
128
|
+
return `${count(names.length, kind)}${shown ? ` (${shown})` : ''}`
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pronouns = (one: boolean) =>
|
|
134
|
+
one
|
|
135
|
+
? { it: 'it', is: 'is', its: 'its', verb: (v: string) => `${v}s` }
|
|
136
|
+
: { it: 'them', is: 'are', its: 'their', verb: (v: string) => v }
|
|
137
|
+
|
|
138
|
+
function ceilingContent(
|
|
139
|
+
change: RoomConstructionChange,
|
|
140
|
+
nodes: StructureNodes,
|
|
141
|
+
room: string,
|
|
142
|
+
canKeep: boolean,
|
|
143
|
+
): DeleteConfirmationContent {
|
|
144
|
+
const ids = change.hostedIds
|
|
145
|
+
const p = pronouns(ids.length === 1)
|
|
146
|
+
const manual = change.manualCeilings ?? []
|
|
147
|
+
const sentences: string[] = []
|
|
148
|
+
if (manual.length) {
|
|
149
|
+
const names = shownNames(manual)
|
|
150
|
+
sentences.push(
|
|
151
|
+
manual.length === 1
|
|
152
|
+
? `Removes the hand-drawn ceiling${names ? ` (${names})` : ''}.`
|
|
153
|
+
: `Removes ${manual.length} hand-drawn ceilings${names ? ` (${names})` : ''}.`,
|
|
154
|
+
)
|
|
155
|
+
if (change.alsoCovers?.length)
|
|
156
|
+
sentences.push(
|
|
157
|
+
`${manual.length === 1 ? 'It also covers' : 'They also cover'} ${sentenceList(change.alsoCovers)}.`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
// Lights are known from their light effect; anything else reads as an item.
|
|
161
|
+
const lights = ids.length > 0 && ids.every((id) => hostedKind(nodes[id]) === 'light')
|
|
162
|
+
if (ids.length) {
|
|
163
|
+
const names = shownNames(ids.map((id) => hostedName(nodes[id])))
|
|
164
|
+
sentences.push(
|
|
165
|
+
lights
|
|
166
|
+
? `${hostedSummary(ids, nodes)} ${p.verb('hang')} from it.`
|
|
167
|
+
: `${count(ids.length, 'item')} ${p.is} on the ceiling${names ? ` (${names})` : ''}.`,
|
|
168
|
+
`Keep ${p.it} in place, or delete ${p.it} too.`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
title: `Remove the ceiling from ${room}?`,
|
|
173
|
+
description: sentences.length ? sentences.join(' ') : 'Removes the ceiling.',
|
|
174
|
+
keepLabel: ids.length && canKeep ? (lights ? 'Keep lights' : 'Keep items') : null,
|
|
175
|
+
confirmLabel: ids.length ? 'Remove all' : 'Remove',
|
|
176
|
+
destructive: true,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function constructionContent(
|
|
181
|
+
change: RoomConstructionChange,
|
|
182
|
+
nodes: StructureNodes,
|
|
183
|
+
canKeep: boolean,
|
|
184
|
+
): DeleteConfirmationContent {
|
|
185
|
+
const room = change.roomName.trim() || 'this room'
|
|
186
|
+
const hosted = hostedSummary(change.hostedIds, nodes)
|
|
187
|
+
const p = pronouns(change.hostedIds.length === 1)
|
|
188
|
+
if (change.part === 'walls' && change.action === 'add') {
|
|
189
|
+
return {
|
|
190
|
+
title: `Add walls to ${room}?`,
|
|
191
|
+
description: `Adds ${count(change.wallCount ?? 0, 'wall')} · the room loses ${(change.areaLoss ?? 0).toFixed(2)} m² of floor.`,
|
|
192
|
+
keepLabel: null,
|
|
193
|
+
confirmLabel: 'Add walls',
|
|
194
|
+
destructive: false,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (change.part === 'walls') {
|
|
198
|
+
const walls = count(change.wallCount ?? 0, 'wall')
|
|
199
|
+
return {
|
|
200
|
+
title: `Remove walls from ${room}?`,
|
|
201
|
+
description: [
|
|
202
|
+
hosted
|
|
203
|
+
? `Turns ${walls} into separators and removes ${hosted}.`
|
|
204
|
+
: `Turns ${walls} into separators.`,
|
|
205
|
+
change.sharedWalls ? 'Shared walls remain.' : '',
|
|
206
|
+
]
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.join(' '),
|
|
209
|
+
keepLabel: null,
|
|
210
|
+
confirmLabel: 'Remove walls',
|
|
211
|
+
destructive: true,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (change.part === 'floor') {
|
|
215
|
+
return {
|
|
216
|
+
title: `Remove the floor from ${room}?`,
|
|
217
|
+
description: hosted
|
|
218
|
+
? `${hosted} ${p.verb('stand')} on it. Keep ${p.it} at ${p.its} height, or delete ${p.it} too.`
|
|
219
|
+
: 'Removes the floor.',
|
|
220
|
+
keepLabel: hosted && canKeep ? 'Keep items' : null,
|
|
221
|
+
confirmLabel: hosted ? 'Delete items' : 'Remove',
|
|
222
|
+
destructive: true,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return ceilingContent(change, nodes, room, canKeep)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type DeleteConfirmationContent = {
|
|
229
|
+
title: string
|
|
230
|
+
description: string
|
|
231
|
+
/** Delete the room but leave its items in place — offered only when it has items. */
|
|
232
|
+
keepLabel: string | null
|
|
233
|
+
confirmLabel: string
|
|
234
|
+
/** Red confirm for removals; adding walls confirms in the neutral style. */
|
|
235
|
+
destructive: boolean
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The words and choices the confirmation offers, from the request alone. */
|
|
239
|
+
function isMezzanine(room: DeleteZonePayload, nodes: StructureNodes) {
|
|
240
|
+
const zone = nodes[room.zoneId]
|
|
241
|
+
return zone?.type === 'zone' && zone.floor?.support === 'open'
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function deleteConfirmationContent(
|
|
245
|
+
request: DeleteConfirmationRequest,
|
|
246
|
+
nodes: StructureNodes,
|
|
247
|
+
): DeleteConfirmationContent {
|
|
248
|
+
if (request.construction)
|
|
249
|
+
return constructionContent(request.construction, nodes, !!request.onKeepContents)
|
|
250
|
+
const room = request.room
|
|
251
|
+
if (!room)
|
|
252
|
+
return {
|
|
253
|
+
title: `Delete ${count(request.count, 'element')}?`,
|
|
254
|
+
description:
|
|
255
|
+
'This removes every selected element. You can undo the deletion while it remains in the editor history.',
|
|
256
|
+
keepLabel: null,
|
|
257
|
+
confirmLabel: 'Delete',
|
|
258
|
+
destructive: true,
|
|
259
|
+
}
|
|
260
|
+
const merge = room.mode === 'merge'
|
|
261
|
+
// A merged area's items always stay, so there is nothing to choose.
|
|
262
|
+
const hasItems = room.itemIds.length > 0 && !merge
|
|
263
|
+
return {
|
|
264
|
+
title: `Delete ${room.name.trim() || (isMezzanine(room, nodes) ? 'this mezzanine' : merge ? 'this area' : 'this room')}?`,
|
|
265
|
+
description: roomDeletionDescription(room, nodes),
|
|
266
|
+
keepLabel: hasItems && request.onKeepContents ? 'Keep items' : null,
|
|
267
|
+
confirmLabel: hasItems ? 'Delete all' : 'Delete',
|
|
268
|
+
destructive: true,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
14
272
|
export function DeleteConfirmationDialog() {
|
|
15
273
|
const request = useDeleteConfirmation((state) => state.request)
|
|
16
274
|
const cancel = useDeleteConfirmation((state) => state.cancel)
|
|
17
275
|
const confirm = useDeleteConfirmation((state) => state.confirm)
|
|
276
|
+
const nodes = useScene((state) => (request ? state.nodes : null))
|
|
277
|
+
const live = useMemo(
|
|
278
|
+
() => (request && nodes ? deleteConfirmationContent(request, nodes) : null),
|
|
279
|
+
[request, nodes],
|
|
280
|
+
)
|
|
281
|
+
// Hold the last words through the close animation so the dialog never empties.
|
|
282
|
+
const shown = useRef(live)
|
|
283
|
+
if (live) shown.current = live
|
|
284
|
+
const content = shown.current
|
|
285
|
+
const blocked = !!request?.conflict
|
|
18
286
|
|
|
19
287
|
useEffect(() => cancel, [cancel])
|
|
20
288
|
|
|
@@ -26,10 +294,12 @@ export function DeleteConfirmationDialog() {
|
|
|
26
294
|
showCloseButton={false}
|
|
27
295
|
>
|
|
28
296
|
<DialogHeader>
|
|
29
|
-
<DialogTitle>
|
|
297
|
+
<DialogTitle>{content?.title}</DialogTitle>
|
|
30
298
|
<DialogDescription>
|
|
31
|
-
|
|
32
|
-
|
|
299
|
+
{content?.description}
|
|
300
|
+
{request?.conflict && (
|
|
301
|
+
<span className="mt-2 block text-destructive">{request.conflict}</span>
|
|
302
|
+
)}
|
|
33
303
|
</DialogDescription>
|
|
34
304
|
</DialogHeader>
|
|
35
305
|
<DialogFooter>
|
|
@@ -40,12 +310,32 @@ export function DeleteConfirmationDialog() {
|
|
|
40
310
|
>
|
|
41
311
|
Cancel
|
|
42
312
|
</button>
|
|
313
|
+
{content?.keepLabel && (
|
|
314
|
+
<button
|
|
315
|
+
className="rounded-full border border-border px-4 py-2 text-sm transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
316
|
+
disabled={blocked}
|
|
317
|
+
onClick={() => {
|
|
318
|
+
const keep = request?.onKeepContents
|
|
319
|
+
cancel()
|
|
320
|
+
keep?.()
|
|
321
|
+
}}
|
|
322
|
+
type="button"
|
|
323
|
+
>
|
|
324
|
+
{content.keepLabel}
|
|
325
|
+
</button>
|
|
326
|
+
)}
|
|
43
327
|
<button
|
|
44
|
-
className=
|
|
328
|
+
className={cn(
|
|
329
|
+
'rounded-full px-4 py-2 text-sm transition-colors disabled:pointer-events-none disabled:opacity-50',
|
|
330
|
+
content?.destructive === false
|
|
331
|
+
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
332
|
+
: 'bg-red-600 text-white hover:bg-red-700',
|
|
333
|
+
)}
|
|
334
|
+
disabled={blocked}
|
|
45
335
|
onClick={confirm}
|
|
46
336
|
type="button"
|
|
47
337
|
>
|
|
48
|
-
|
|
338
|
+
{content?.confirmLabel}
|
|
49
339
|
</button>
|
|
50
340
|
</DialogFooter>
|
|
51
341
|
</DialogContent>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type CollectionId,
|
|
5
|
+
ColumnNode,
|
|
6
|
+
collectionIdsOf,
|
|
7
|
+
DoorNode,
|
|
8
|
+
ElevatorNode,
|
|
9
|
+
FenceNode,
|
|
10
|
+
generateId,
|
|
11
|
+
ItemNode,
|
|
12
|
+
nodeRegistry,
|
|
13
|
+
RoofSegmentNode,
|
|
14
|
+
runSceneHistoryDraftWrite,
|
|
15
|
+
SpawnNode,
|
|
16
|
+
StairSegmentNode,
|
|
17
|
+
useScene,
|
|
18
|
+
WallNode,
|
|
19
|
+
WindowNode,
|
|
20
|
+
} from '@pascal-app/core'
|
|
21
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
+
import {
|
|
23
|
+
createFreshPlacementSubtree,
|
|
24
|
+
duplicatesAsFreshSubtree,
|
|
25
|
+
prepareFreshPlacementRootDuplicate,
|
|
26
|
+
} from '../../lib/fresh-planar-placement'
|
|
27
|
+
import { duplicateWithoutMove, registryMoveDisabled } from '../../lib/node-action-movement'
|
|
28
|
+
import { duplicateRoofSubtree } from '../../lib/roof-duplication'
|
|
29
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
30
|
+
import useEditor from '../../store/use-editor'
|
|
31
|
+
import { startZoneRoomTransform } from './room-controls'
|
|
32
|
+
|
|
33
|
+
let carriedCopy: { id: AnyNodeId; sourceId: AnyNodeId } | null = null
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The collections a copy being placed joins when its move tool creates it:
|
|
37
|
+
* its draft's when the copy is in the scene, else those of the node it copies.
|
|
38
|
+
*/
|
|
39
|
+
export function copyCollectionIds(copy: AnyNode): CollectionId[] {
|
|
40
|
+
const { collections, nodes } = useScene.getState()
|
|
41
|
+
if (nodes[copy.id as AnyNodeId]) return collectionIdsOf(collections, copy.id as AnyNodeId)
|
|
42
|
+
if (carriedCopy?.id !== copy.id) return []
|
|
43
|
+
return collectionIdsOf(collections, carriedCopy.sourceId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Duplicate one node and hand the copy to its move tool: the toolbar's and
|
|
48
|
+
* every inspector's Duplicate. The copy is in the collections its source is in.
|
|
49
|
+
*/
|
|
50
|
+
export function duplicateNodeAndPickUp(node: AnyNode) {
|
|
51
|
+
if (!node.parentId) return
|
|
52
|
+
if (startZoneRoomTransform(node, 'duplicate')) return
|
|
53
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
54
|
+
const { setMovingNode } = useEditor.getState()
|
|
55
|
+
const { setSelection } = useViewer.getState()
|
|
56
|
+
|
|
57
|
+
if (registryMoveDisabled(node)) {
|
|
58
|
+
try {
|
|
59
|
+
const id = duplicateWithoutMove(node)
|
|
60
|
+
if (id) setSelection({ selectedIds: [id] })
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Failed to duplicate node', error)
|
|
63
|
+
}
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (node.type === 'roof') {
|
|
68
|
+
try {
|
|
69
|
+
duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Failed to duplicate roof', error)
|
|
72
|
+
}
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
runSceneHistoryDraftWrite(() => {
|
|
77
|
+
if (duplicatesAsFreshSubtree(node)) {
|
|
78
|
+
let draftId: AnyNodeId | null = null
|
|
79
|
+
try {
|
|
80
|
+
draftId = createFreshPlacementSubtree(node.id as AnyNodeId)
|
|
81
|
+
const draft = draftId ? useScene.getState().nodes[draftId] : null
|
|
82
|
+
if (draft) {
|
|
83
|
+
setMovingNode(draft)
|
|
84
|
+
setSelection({ selectedIds: [] })
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (draftId && useScene.getState().nodes[draftId]) {
|
|
89
|
+
useScene.getState().deleteNode(draftId)
|
|
90
|
+
}
|
|
91
|
+
console.error('Failed to duplicate node subtree', error)
|
|
92
|
+
}
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const duplicateInfo = prepareFreshPlacementRootDuplicate(node) as any
|
|
97
|
+
|
|
98
|
+
let duplicate: AnyNode | null = null
|
|
99
|
+
try {
|
|
100
|
+
if (node.type === 'door') {
|
|
101
|
+
duplicate = DoorNode.parse(duplicateInfo)
|
|
102
|
+
} else if (node.type === 'window') {
|
|
103
|
+
duplicate = WindowNode.parse(duplicateInfo)
|
|
104
|
+
} else if (node.type === 'item') {
|
|
105
|
+
duplicate = ItemNode.parse(duplicateInfo)
|
|
106
|
+
} else if (node.type === 'elevator') {
|
|
107
|
+
duplicate = ElevatorNode.parse(duplicateInfo)
|
|
108
|
+
} else if (node.type === 'column') {
|
|
109
|
+
duplicate = ColumnNode.parse(duplicateInfo)
|
|
110
|
+
} else if (node.type === 'wall') {
|
|
111
|
+
duplicate = WallNode.parse(duplicateInfo)
|
|
112
|
+
} else if (node.type === 'fence') {
|
|
113
|
+
duplicate = FenceNode.parse(duplicateInfo)
|
|
114
|
+
duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1]
|
|
115
|
+
duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1]
|
|
116
|
+
} else if (node.type === 'roof-segment') {
|
|
117
|
+
duplicateInfo.id = generateId('rseg')
|
|
118
|
+
duplicate = RoofSegmentNode.parse(duplicateInfo)
|
|
119
|
+
} else if (node.type === 'stair-segment') {
|
|
120
|
+
duplicate = StairSegmentNode.parse(duplicateInfo)
|
|
121
|
+
} else if (node.type === 'spawn') {
|
|
122
|
+
duplicate = SpawnNode.parse(duplicateInfo)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Registry-driven fallback: any kind with a NodeDefinition can be
|
|
126
|
+
// duplicated through its schema's parse(). Future built-in kinds
|
|
127
|
+
// get duplicate for free.
|
|
128
|
+
if (!duplicate) {
|
|
129
|
+
const def = nodeRegistry.get(node.type)
|
|
130
|
+
if (def) {
|
|
131
|
+
duplicate = def.schema.parse(duplicateInfo) as AnyNode
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('Failed to parse duplicate', error)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!duplicate) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const copy = duplicate
|
|
144
|
+
// The copy is in the collections its source is in, from its draft on.
|
|
145
|
+
const createCopy = () =>
|
|
146
|
+
useScene.getState().createNodes([
|
|
147
|
+
{
|
|
148
|
+
node: copy,
|
|
149
|
+
parentId: copy.parentId as AnyNodeId,
|
|
150
|
+
collectionIds: collectionIdsOf(useScene.getState().collections, node.id),
|
|
151
|
+
},
|
|
152
|
+
])
|
|
153
|
+
if (
|
|
154
|
+
duplicate.type === 'door' ||
|
|
155
|
+
duplicate.type === 'window' ||
|
|
156
|
+
duplicate.type === 'elevator' ||
|
|
157
|
+
duplicate.type === 'wall' ||
|
|
158
|
+
duplicate.type === 'fence'
|
|
159
|
+
) {
|
|
160
|
+
createCopy()
|
|
161
|
+
} else if (duplicate.type === 'roof-segment' || duplicate.type === 'stair-segment') {
|
|
162
|
+
// Add small offset to make it visible
|
|
163
|
+
if ('position' in duplicate) {
|
|
164
|
+
duplicate.position = [
|
|
165
|
+
duplicate.position[0] + 1,
|
|
166
|
+
duplicate.position[1],
|
|
167
|
+
duplicate.position[2] + 1,
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
createCopy()
|
|
171
|
+
} else if (
|
|
172
|
+
duplicate.type === 'item' ||
|
|
173
|
+
duplicate.type === 'chimney' ||
|
|
174
|
+
duplicate.type === 'dormer'
|
|
175
|
+
) {
|
|
176
|
+
// Items, chimneys & dormers use pure drag-to-place: NO node is
|
|
177
|
+
// inserted into the scene until the user clicks to commit. The
|
|
178
|
+
// `setMovingNode` call below hands the clone (with
|
|
179
|
+
// `metadata.isNew = true` + no id) to its move tool —
|
|
180
|
+
// `MoveItemTool` / `MoveChimneyTool` / `MoveDormerTool` — which
|
|
181
|
+
// create a draft and call `createNode` on the commit click.
|
|
182
|
+
// Pre-creating here would drop a second copy into the scene
|
|
183
|
+
// before any click — the furnish-tab "duplicate auto-places an
|
|
184
|
+
// item without clicking" bug. (Item has its own
|
|
185
|
+
// draft-committing move tool, so it must skip the generic
|
|
186
|
+
// registry auto-create branch below.)
|
|
187
|
+
carriedCopy = { id: duplicate.id as AnyNodeId, sourceId: node.id as AnyNodeId }
|
|
188
|
+
} else if (
|
|
189
|
+
duplicate.type === 'duct-segment' ||
|
|
190
|
+
duplicate.type === 'duct-fitting' ||
|
|
191
|
+
duplicate.type === 'pipe-segment' ||
|
|
192
|
+
duplicate.type === 'lineset' ||
|
|
193
|
+
duplicate.type === 'liquid-line'
|
|
194
|
+
) {
|
|
195
|
+
// Duct runs & fittings, DWV pipe runs, and refrigerant linesets use
|
|
196
|
+
// pure drag-to-place: NO node is inserted into the scene until the
|
|
197
|
+
// commit click. `setMovingNode` below hands the clone (with
|
|
198
|
+
// `metadata.isNew`) to its ghost tool (`MoveDuctSegmentTool` /
|
|
199
|
+
// `MoveDuctFittingTool` / `MovePipeSegmentTool` / `MoveLinesetTool`),
|
|
200
|
+
// which previews a translucent copy inside a footprint bounding box
|
|
201
|
+
// on the cursor and calls `createNode` on the drop click.
|
|
202
|
+
// Pre-creating here would drop a copy before any click — the
|
|
203
|
+
// "auto-places it" bug.
|
|
204
|
+
carriedCopy = { id: duplicate.id as AnyNodeId, sourceId: node.id as AnyNodeId }
|
|
205
|
+
} else if (nodeRegistry.has(duplicate.type)) {
|
|
206
|
+
// Registry-driven kinds: offset slightly so the duplicate doesn't
|
|
207
|
+
// overlap exactly, then create + hand to the move tool. Mirrors the
|
|
208
|
+
// roof-segment / stair-segment behavior.
|
|
209
|
+
if ('position' in duplicate && Array.isArray((duplicate as any).position)) {
|
|
210
|
+
const pos = (duplicate as { position: [number, number, number] }).position
|
|
211
|
+
;(duplicate as { position: [number, number, number] }).position = [
|
|
212
|
+
pos[0] + 1,
|
|
213
|
+
pos[1],
|
|
214
|
+
pos[2] + 1,
|
|
215
|
+
]
|
|
216
|
+
} else if ('path' in duplicate && Array.isArray((duplicate as any).path)) {
|
|
217
|
+
// Other polyline kinds (pipe / lineset) carry a `path`, not a
|
|
218
|
+
// `position`. Create the copy HIDDEN so nothing is auto-placed:
|
|
219
|
+
// their shared path mover reveals it as a cursor-following preview
|
|
220
|
+
// on the first mouse move and commits on the next click.
|
|
221
|
+
;(duplicate as { visible?: boolean }).visible = false
|
|
222
|
+
}
|
|
223
|
+
createCopy()
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
duplicate.type === 'item' ||
|
|
227
|
+
duplicate.type === 'elevator' ||
|
|
228
|
+
duplicate.type === 'column' ||
|
|
229
|
+
duplicate.type === 'wall' ||
|
|
230
|
+
duplicate.type === 'fence' ||
|
|
231
|
+
duplicate.type === 'window' ||
|
|
232
|
+
duplicate.type === 'door' ||
|
|
233
|
+
duplicate.type === 'roof-segment' ||
|
|
234
|
+
duplicate.type === 'spawn' ||
|
|
235
|
+
duplicate.type === 'stair-segment' ||
|
|
236
|
+
// Registry-driven kinds get picked up by MoveTool's generic
|
|
237
|
+
// fallback (MoveRegistryNodeTool) so the user can reposition.
|
|
238
|
+
nodeRegistry.has(duplicate.type)
|
|
239
|
+
) {
|
|
240
|
+
setMovingNode(duplicate)
|
|
241
|
+
}
|
|
242
|
+
setSelection({ selectedIds: [] })
|
|
243
|
+
})
|
|
244
|
+
}
|