@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
area,
|
|
5
|
+
type CeilingNode,
|
|
6
|
+
ceilingPaintRegions,
|
|
7
|
+
ceilingRegionsOwner,
|
|
8
|
+
type MultiPolygon,
|
|
9
|
+
resolveCeilingHeight,
|
|
10
|
+
union,
|
|
11
|
+
useScene,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { create } from 'zustand'
|
|
14
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../store/use-editor'
|
|
15
|
+
import {
|
|
16
|
+
CROSSING_MESSAGE,
|
|
17
|
+
clipFloorRegion,
|
|
18
|
+
floorRegionAxisAngle,
|
|
19
|
+
floorRegionEdgeCrosses,
|
|
20
|
+
floorRegionRectangle,
|
|
21
|
+
MIN_FLOOR_REGION_AREA,
|
|
22
|
+
OUTSIDE_MESSAGE,
|
|
23
|
+
} from './floor-region-geometry'
|
|
24
|
+
import {
|
|
25
|
+
type FloorRegionPoint,
|
|
26
|
+
type FloorRegionSnapSettings,
|
|
27
|
+
type FloorRegionSnapTargets,
|
|
28
|
+
floorRegionSnapTargets,
|
|
29
|
+
snapFloorRegionPoint,
|
|
30
|
+
} from './floor-region-snap'
|
|
31
|
+
import { beginGesture, type GestureHandle } from './gesture-lifecycle'
|
|
32
|
+
import { hasActivePaintMaterial } from './material-paint'
|
|
33
|
+
import { isPaintErasing, usePaintRegionMode } from './paint-region-mode'
|
|
34
|
+
import { addCeilingRegion, addFloorRegion } from './paint-regions'
|
|
35
|
+
import { roomFloorElevation } from './room-handle-drag'
|
|
36
|
+
|
|
37
|
+
// An outline drawn on one room's floor: a rectangle (press, drag, release; a
|
|
38
|
+
// persistent tool also takes two clicks) or a polygon (click points, close on
|
|
39
|
+
// the first point or Enter). What the outline becomes is the draft's purpose:
|
|
40
|
+
// "Paint part of the floor" (the default) clips it to the room and paints it
|
|
41
|
+
// as one `zone.floor.regions` entry; on a ceiling (`ceilingId`) the outline is
|
|
42
|
+
// drawn on its underside and paints one ceiling region; "Add mezzanine" (`mezzanine-draft`)
|
|
43
|
+
// builds a mezzanine from it. Either way one commit is one undo step. Surfaces
|
|
44
|
+
// (3D canvas, 2D plan) resolve pointers into plan points and call the actions
|
|
45
|
+
// below.
|
|
46
|
+
|
|
47
|
+
export const PICK_MATERIAL_MESSAGE = 'Pick a material first'
|
|
48
|
+
|
|
49
|
+
export type FloorDraftCommit = { ok: true } | { ok: false; message: string }
|
|
50
|
+
|
|
51
|
+
/** What a floor outline is drawn for, and how it commits. */
|
|
52
|
+
export type FloorDraftPurpose = {
|
|
53
|
+
/** The lifecycle handle a draft runs under; `onCancel` drops the draft. */
|
|
54
|
+
own: (room: FloorRegionRoom, onCancel: () => void) => GestureHandle
|
|
55
|
+
/**
|
|
56
|
+
* The tool outlives its drafts: a draft that ends uncommitted keeps the
|
|
57
|
+
* handle, and a refused commit stays on the draft, red and labelled. Paint
|
|
58
|
+
* drafts are one gesture each and refusals go to the HUD notice.
|
|
59
|
+
*/
|
|
60
|
+
persistent?: boolean
|
|
61
|
+
/** Whether a new draft may start; says why not itself. */
|
|
62
|
+
canStart?: () => boolean
|
|
63
|
+
commit: (room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) => FloorDraftCommit
|
|
64
|
+
/** Why a polygon point cannot be placed at `point`, or null. */
|
|
65
|
+
pointMessage?: (room: FloorRegionRoom, point: FloorRegionPoint) => string | null
|
|
66
|
+
/** Preview fill; the paint tint when unset. */
|
|
67
|
+
fill?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type FloorRegionRoom = {
|
|
71
|
+
zoneId: string
|
|
72
|
+
levelId: string
|
|
73
|
+
/** Where the outline may go, level XZ: the room's clear floor for paint. */
|
|
74
|
+
clear: MultiPolygon
|
|
75
|
+
/** Elevation the 3D pointer plane and the preview use. */
|
|
76
|
+
elevation: number
|
|
77
|
+
/** Rectangle frame: the room's longest edge direction. */
|
|
78
|
+
angle: number
|
|
79
|
+
targets: FloorRegionSnapTargets
|
|
80
|
+
/** Painting part of the floor when unset. */
|
|
81
|
+
purpose?: FloorDraftPurpose
|
|
82
|
+
/** The ceiling whose underside the outline is drawn on, when it is not a floor. */
|
|
83
|
+
ceilingId?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type FloorRegionDraft =
|
|
87
|
+
| {
|
|
88
|
+
kind: 'rectangle'
|
|
89
|
+
room: FloorRegionRoom
|
|
90
|
+
start: FloorRegionPoint
|
|
91
|
+
end: FloorRegionPoint
|
|
92
|
+
/** Why the released box was refused; the box stays put until the next press. */
|
|
93
|
+
refusal?: string | null
|
|
94
|
+
/**
|
|
95
|
+
* A persistent tool's click without a drag placed the first corner: the
|
|
96
|
+
* box follows the pointer and the next click sets the opposite corner.
|
|
97
|
+
*/
|
|
98
|
+
anchored?: boolean
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
kind: 'polygon'
|
|
102
|
+
room: FloorRegionRoom
|
|
103
|
+
points: FloorRegionPoint[]
|
|
104
|
+
cursor: FloorRegionPoint | null
|
|
105
|
+
/** Why the live edge cannot be placed (red preview), or null. */
|
|
106
|
+
message: string | null
|
|
107
|
+
/** Why closing was refused; kept until the points change. */
|
|
108
|
+
refusal?: string | null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type FloorRegionHover = {
|
|
112
|
+
levelId: string
|
|
113
|
+
elevation: number
|
|
114
|
+
point: FloorRegionPoint
|
|
115
|
+
/** Over a ceiling's underside rather than a floor. */
|
|
116
|
+
ceiling?: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const useFloorRegionDraft = create<{
|
|
120
|
+
draft: FloorRegionDraft | null
|
|
121
|
+
hover: FloorRegionHover | null
|
|
122
|
+
}>(() => ({ draft: null, hover: null }))
|
|
123
|
+
|
|
124
|
+
/** The room a floor region is drawn in, read from its zone and room geometry. */
|
|
125
|
+
export function describeFloorRegionRoom(
|
|
126
|
+
nodes: Record<string, AnyNode>,
|
|
127
|
+
room: { zoneId: string; key: { levelId: string }; clearPolygon: MultiPolygon },
|
|
128
|
+
): FloorRegionRoom {
|
|
129
|
+
const zone = nodes[room.zoneId]
|
|
130
|
+
const regions = zone?.type === 'zone' ? (zone.floor?.regions ?? []) : []
|
|
131
|
+
return {
|
|
132
|
+
zoneId: room.zoneId,
|
|
133
|
+
levelId: room.key.levelId,
|
|
134
|
+
clear: room.clearPolygon,
|
|
135
|
+
elevation: roomFloorElevation(nodes, room.zoneId),
|
|
136
|
+
angle: floorRegionAxisAngle(room.clearPolygon),
|
|
137
|
+
targets: floorRegionSnapTargets(
|
|
138
|
+
room.clearPolygon,
|
|
139
|
+
regions.map((region) => region.polygon),
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The ceilings a ceiling region covers: an automatic ceiling's room may be cut into several. */
|
|
145
|
+
function regionCeilings(nodes: Record<string, AnyNode>, ceiling: CeilingNode): CeilingNode[] {
|
|
146
|
+
const owner = ceilingRegionsOwner(ceiling)
|
|
147
|
+
if (owner.kind === 'ceiling') return [ceiling]
|
|
148
|
+
return Object.values(nodes).filter(
|
|
149
|
+
(node): node is CeilingNode =>
|
|
150
|
+
node.type === 'ceiling' &&
|
|
151
|
+
node.parentId === ceiling.parentId &&
|
|
152
|
+
node.boundary === 'auto' &&
|
|
153
|
+
node.zoneId === owner.id,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A ceiling's underside as a draft room: its outline minus holes (every part of
|
|
159
|
+
* an automatic ceiling's room), at the plane the ceiling draws at.
|
|
160
|
+
*/
|
|
161
|
+
export function describeCeilingRegionRoom(
|
|
162
|
+
nodes: Record<string, AnyNode>,
|
|
163
|
+
ceiling: CeilingNode,
|
|
164
|
+
): FloorRegionRoom {
|
|
165
|
+
const clear = union(
|
|
166
|
+
regionCeilings(nodes, ceiling).map((part) => ({ outer: part.polygon, holes: part.holes })),
|
|
167
|
+
)
|
|
168
|
+
const owner = ceilingRegionsOwner(ceiling)
|
|
169
|
+
return {
|
|
170
|
+
zoneId: owner.id,
|
|
171
|
+
levelId: ceiling.parentId ?? '',
|
|
172
|
+
clear,
|
|
173
|
+
elevation: resolveCeilingHeight(ceiling, nodes) - 0.01,
|
|
174
|
+
angle: floorRegionAxisAngle(clear),
|
|
175
|
+
targets: floorRegionSnapTargets(
|
|
176
|
+
clear,
|
|
177
|
+
ceilingPaintRegions(ceiling, nodes).map((region) => region.polygon),
|
|
178
|
+
),
|
|
179
|
+
purpose: PAINT_CEILING_REGION,
|
|
180
|
+
ceilingId: ceiling.id,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The snapping the active context asks for: `lines` (magnetic), `grid`, or `off`. */
|
|
185
|
+
export function currentFloorRegionSnap(free: boolean): FloorRegionSnapSettings {
|
|
186
|
+
return {
|
|
187
|
+
mode: isMagneticSnapActive() ? 'lines' : isGridSnapActive() ? 'grid' : 'off',
|
|
188
|
+
step: useEditor.getState().gridSnapStep,
|
|
189
|
+
free,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function snap(room: FloorRegionRoom, raw: FloorRegionPoint, settings: FloorRegionSnapSettings) {
|
|
194
|
+
return snapFloorRegionPoint(raw, settings, room.targets).point
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function samePoint(a: FloorRegionPoint, b: FloorRegionPoint) {
|
|
198
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-6
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The outline the draft currently describes (the live pointer included). */
|
|
202
|
+
export function floorRegionDraftOutline(draft: FloorRegionDraft): FloorRegionPoint[] {
|
|
203
|
+
if (draft.kind === 'rectangle')
|
|
204
|
+
return floorRegionRectangle(draft.start, draft.end, draft.room.angle)
|
|
205
|
+
const { points, cursor } = draft
|
|
206
|
+
return cursor && !(points.length && samePoint(points.at(-1)!, cursor))
|
|
207
|
+
? [...points, cursor]
|
|
208
|
+
: [...points]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Whether a polygon click at `point` closes the ring on its first point. */
|
|
212
|
+
export function closesOnFirst(
|
|
213
|
+
points: readonly FloorRegionPoint[],
|
|
214
|
+
point: FloorRegionPoint,
|
|
215
|
+
closeRadius: number,
|
|
216
|
+
) {
|
|
217
|
+
const first = points[0]
|
|
218
|
+
return (
|
|
219
|
+
points.length >= 3 &&
|
|
220
|
+
!!first &&
|
|
221
|
+
Math.hypot(first[0] - point[0], first[1] - point[1]) <= closeRadius
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Why the live polygon edge to `cursor` cannot be placed, or null. */
|
|
226
|
+
export function polygonLiveMessage(
|
|
227
|
+
points: readonly FloorRegionPoint[],
|
|
228
|
+
cursor: FloorRegionPoint | null,
|
|
229
|
+
closeRadius: number,
|
|
230
|
+
room?: FloorRegionRoom,
|
|
231
|
+
): string | null {
|
|
232
|
+
if (!cursor) return null
|
|
233
|
+
const closing = closesOnFirst(points, cursor, closeRadius)
|
|
234
|
+
if (points.length && floorRegionEdgeCrosses(points, closing ? points[0]! : cursor, closing))
|
|
235
|
+
return CROSSING_MESSAGE
|
|
236
|
+
return (!closing && room && purposeOf(room).pointMessage?.(room, cursor)) || null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Why the draft is red: the live edge, or the refused commit. */
|
|
240
|
+
export function floorRegionDraftMessage(draft: FloorRegionDraft): string | null {
|
|
241
|
+
return (draft.kind === 'polygon' ? draft.message : null) ?? draft.refusal ?? null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function setNotice(notice: string | null) {
|
|
245
|
+
if (usePaintRegionMode.getState().notice !== notice)
|
|
246
|
+
usePaintRegionMode.getState().setNotice(notice)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The paint the region takes, or null (no material, or the eraser). */
|
|
250
|
+
function activePaint() {
|
|
251
|
+
const editor = useEditor.getState()
|
|
252
|
+
if (isPaintErasing() || !hasActivePaintMaterial(editor.activePaintMaterial)) return null
|
|
253
|
+
return {
|
|
254
|
+
material: editor.activePaintMaterial.material,
|
|
255
|
+
materialPreset: editor.activePaintMaterial.materialPreset,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Commits the draft's outline and ends the draft; a refused commit keeps it. A
|
|
261
|
+
* throwing write cancels the draft, so it can never stay half-alive.
|
|
262
|
+
*/
|
|
263
|
+
function commitDraft(room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
|
|
264
|
+
let settled = false
|
|
265
|
+
try {
|
|
266
|
+
const result = purposeOf(room).commit(room, polygon)
|
|
267
|
+
settled = true
|
|
268
|
+
if (result.ok) endDraft(room)
|
|
269
|
+
return result
|
|
270
|
+
} finally {
|
|
271
|
+
if (!settled) cancelFloorRegion()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Clips `polygon` to the room and paints it; the HUD notice says why when it cannot. */
|
|
276
|
+
export function commitFloorRegion(room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
|
|
277
|
+
const paint = activePaint()
|
|
278
|
+
if (!paint) {
|
|
279
|
+
setNotice(PICK_MATERIAL_MESSAGE)
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
const clip = clipFloorRegion(polygon, room.clear)
|
|
283
|
+
if (!clip) {
|
|
284
|
+
setNotice(OUTSIDE_MESSAGE)
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
const result = addFloorRegion(room.zoneId, clip.polygon, paint)
|
|
288
|
+
setNotice(result.ok ? null : result.message)
|
|
289
|
+
return result.ok
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const FLOOR_PAINT_REGION_HANDLE = 'paint-floor-region'
|
|
293
|
+
|
|
294
|
+
/** "Paint part of the floor": each draft is its own gesture in the paint sub-mode. */
|
|
295
|
+
const PAINT_FLOOR_REGION: FloorDraftPurpose = {
|
|
296
|
+
own: (room, onCancel) =>
|
|
297
|
+
beginGesture({
|
|
298
|
+
kind: 'floor-paint-region',
|
|
299
|
+
scope: { kind: 'handle-drag', nodeId: room.zoneId, handle: FLOOR_PAINT_REGION_HANDLE },
|
|
300
|
+
paintSubMode: true,
|
|
301
|
+
stale: () => floorRegionRoomGone(useScene.getState().nodes) || !floorRegionEditable(),
|
|
302
|
+
onCancel,
|
|
303
|
+
}),
|
|
304
|
+
canStart: () => {
|
|
305
|
+
if (activePaint()) return true
|
|
306
|
+
setNotice(PICK_MATERIAL_MESSAGE)
|
|
307
|
+
return false
|
|
308
|
+
},
|
|
309
|
+
commit: (room, polygon) =>
|
|
310
|
+
commitFloorRegion(room, polygon)
|
|
311
|
+
? { ok: true }
|
|
312
|
+
: { ok: false, message: usePaintRegionMode.getState().notice ?? '' },
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Clips `polygon` to the ceiling and paints it as one region of its owner. */
|
|
316
|
+
export function commitCeilingRegion(room: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
|
|
317
|
+
const paint = activePaint()
|
|
318
|
+
if (!paint) {
|
|
319
|
+
setNotice(PICK_MATERIAL_MESSAGE)
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
const clip = clipFloorRegion(polygon, room.clear)
|
|
323
|
+
if (!(clip && room.ceilingId)) {
|
|
324
|
+
setNotice(OUTSIDE_MESSAGE)
|
|
325
|
+
return false
|
|
326
|
+
}
|
|
327
|
+
const result = addCeilingRegion(room.ceilingId, clip.polygon, paint)
|
|
328
|
+
setNotice(result.ok ? null : result.message)
|
|
329
|
+
return result.ok
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const CEILING_PAINT_REGION_HANDLE = 'paint-ceiling-region'
|
|
333
|
+
|
|
334
|
+
/** "Paint part of the ceiling": the floor paint gesture, drawn on a ceiling's underside. */
|
|
335
|
+
const PAINT_CEILING_REGION: FloorDraftPurpose = {
|
|
336
|
+
own: (room, onCancel) =>
|
|
337
|
+
beginGesture({
|
|
338
|
+
kind: 'ceiling-paint-region',
|
|
339
|
+
scope: {
|
|
340
|
+
kind: 'handle-drag',
|
|
341
|
+
nodeId: room.ceilingId ?? room.zoneId,
|
|
342
|
+
handle: CEILING_PAINT_REGION_HANDLE,
|
|
343
|
+
},
|
|
344
|
+
paintSubMode: true,
|
|
345
|
+
stale: () => floorRegionRoomGone(useScene.getState().nodes) || !floorRegionEditable(),
|
|
346
|
+
onCancel,
|
|
347
|
+
}),
|
|
348
|
+
canStart: PAINT_FLOOR_REGION.canStart,
|
|
349
|
+
commit: (room, polygon) =>
|
|
350
|
+
commitCeilingRegion(room, polygon)
|
|
351
|
+
? { ok: true }
|
|
352
|
+
: { ok: false, message: usePaintRegionMode.getState().notice ?? '' },
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function purposeOf(room: FloorRegionRoom) {
|
|
356
|
+
return room.purpose ?? PAINT_FLOOR_REGION
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// The draft's lifecycle handle: a press, or a whole polygon across clicks.
|
|
360
|
+
let owner: GestureHandle | null = null
|
|
361
|
+
|
|
362
|
+
function clearDraft() {
|
|
363
|
+
if (useFloorRegionDraft.getState().draft) useFloorRegionDraft.setState({ draft: null })
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function ownDraft(room: FloorRegionRoom) {
|
|
367
|
+
if (owner?.active) return owner
|
|
368
|
+
const handle = purposeOf(room).own(room, () => {
|
|
369
|
+
if (owner === handle) owner = null
|
|
370
|
+
clearDraft()
|
|
371
|
+
})
|
|
372
|
+
owner = handle
|
|
373
|
+
return handle
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Ends the draft normally (committed, or nothing left to draw); a persistent tool stays. */
|
|
377
|
+
function endDraft(room: FloorRegionRoom) {
|
|
378
|
+
const current = owner
|
|
379
|
+
owner = null
|
|
380
|
+
clearDraft()
|
|
381
|
+
if (!purposeOf(room).persistent) current?.end()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** The draft's lifecycle handle, for the surface's pointer capture. */
|
|
385
|
+
export function floorRegionGesture(): GestureHandle | null {
|
|
386
|
+
return owner?.active ? owner : null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function cancelFloorRegion() {
|
|
390
|
+
const current = owner
|
|
391
|
+
owner = null
|
|
392
|
+
if (current?.active) current.cancel()
|
|
393
|
+
else clearDraft()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Drops the draft in hand: a paint draft is cancelled, a persistent tool stays armed. */
|
|
397
|
+
export function dropFloorRegionDraft() {
|
|
398
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
399
|
+
if (!(draft && purposeOf(draft.room).persistent)) return cancelFloorRegion()
|
|
400
|
+
owner = null
|
|
401
|
+
clearDraft()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function setFloorRegionHover(hover: FloorRegionHover | null) {
|
|
405
|
+
const current = useFloorRegionDraft.getState().hover
|
|
406
|
+
if (
|
|
407
|
+
current === hover ||
|
|
408
|
+
(current &&
|
|
409
|
+
hover &&
|
|
410
|
+
current.levelId === hover.levelId &&
|
|
411
|
+
current.elevation === hover.elevation &&
|
|
412
|
+
samePoint(current.point, hover.point))
|
|
413
|
+
)
|
|
414
|
+
return
|
|
415
|
+
useFloorRegionDraft.setState({ hover })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* A press on a room's floor. Rectangle: the first corner (the opposite one
|
|
420
|
+
* once a click anchored the first). Polygon: previews the point the release
|
|
421
|
+
* will place (starting a draft in this room if none).
|
|
422
|
+
* Returns whether the press belongs to the region draft.
|
|
423
|
+
*/
|
|
424
|
+
export function pressFloorRegion(
|
|
425
|
+
mode: 'rectangle' | 'polygon',
|
|
426
|
+
room: FloorRegionRoom,
|
|
427
|
+
raw: FloorRegionPoint,
|
|
428
|
+
settings: FloorRegionSnapSettings,
|
|
429
|
+
closeRadius: number,
|
|
430
|
+
): boolean {
|
|
431
|
+
const current = useFloorRegionDraft.getState().draft
|
|
432
|
+
if (!(current || (purposeOf(room).canStart?.() ?? true))) return true
|
|
433
|
+
const draftRoom = mode === 'polygon' && current?.kind === 'polygon' ? current.room : room
|
|
434
|
+
if (current && current.room !== draftRoom) cancelFloorRegion()
|
|
435
|
+
ownDraft(draftRoom)
|
|
436
|
+
const point = snap(draftRoom, raw, settings)
|
|
437
|
+
if (mode === 'rectangle') {
|
|
438
|
+
if (current?.kind === 'rectangle' && current.anchored && !current.refusal) {
|
|
439
|
+
useFloorRegionDraft.setState({ draft: { ...current, end: point } })
|
|
440
|
+
return true
|
|
441
|
+
}
|
|
442
|
+
useFloorRegionDraft.setState({
|
|
443
|
+
draft: { kind: 'rectangle', room: draftRoom, start: point, end: point },
|
|
444
|
+
})
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
const points = current?.kind === 'polygon' ? current.points : []
|
|
448
|
+
useFloorRegionDraft.setState({
|
|
449
|
+
draft: {
|
|
450
|
+
kind: 'polygon',
|
|
451
|
+
room: draftRoom,
|
|
452
|
+
points,
|
|
453
|
+
cursor: point,
|
|
454
|
+
message: polygonLiveMessage(points, point, closeRadius, draftRoom),
|
|
455
|
+
refusal: current?.kind === 'polygon' ? current.refusal : null,
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** The pointer moved: the rectangle's opposite corner, or the polygon's live point. */
|
|
462
|
+
export function moveFloorRegion(
|
|
463
|
+
raw: FloorRegionPoint,
|
|
464
|
+
settings: FloorRegionSnapSettings,
|
|
465
|
+
closeRadius: number,
|
|
466
|
+
) {
|
|
467
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
468
|
+
if (!draft) return
|
|
469
|
+
const point = snap(draft.room, raw, settings)
|
|
470
|
+
if (draft.kind === 'rectangle') {
|
|
471
|
+
if (!(draft.refusal || samePoint(point, draft.end)))
|
|
472
|
+
useFloorRegionDraft.setState({ draft: { ...draft, end: point } })
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
if (draft.cursor && samePoint(point, draft.cursor)) return
|
|
476
|
+
useFloorRegionDraft.setState({
|
|
477
|
+
draft: {
|
|
478
|
+
...draft,
|
|
479
|
+
cursor: point,
|
|
480
|
+
message: polygonLiveMessage(draft.points, point, closeRadius, draft.room),
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function finishPolygon(
|
|
486
|
+
draft: Extract<FloorRegionDraft, { kind: 'polygon' }>,
|
|
487
|
+
points: FloorRegionPoint[],
|
|
488
|
+
) {
|
|
489
|
+
const result = commitDraft(draft.room, points)
|
|
490
|
+
if (!result.ok)
|
|
491
|
+
useFloorRegionDraft.setState({
|
|
492
|
+
draft: {
|
|
493
|
+
...draft,
|
|
494
|
+
points,
|
|
495
|
+
cursor: null,
|
|
496
|
+
message: null,
|
|
497
|
+
refusal: purposeOf(draft.room).persistent ? result.message : null,
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The press was released. Rectangle: commits the box (a click without a drag
|
|
504
|
+
* commits nothing, or anchors the first corner in a persistent tool). Polygon: places the point, or closes and commits the ring
|
|
505
|
+
* on the first point. A crossing edge is refused and stays red.
|
|
506
|
+
*/
|
|
507
|
+
export function releaseFloorRegion(closeRadius: number) {
|
|
508
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
509
|
+
if (!draft) return
|
|
510
|
+
if (draft.kind === 'rectangle') {
|
|
511
|
+
if (draft.refusal) return
|
|
512
|
+
const box = floorRegionDraftOutline(draft)
|
|
513
|
+
const persistent = purposeOf(draft.room).persistent
|
|
514
|
+
// A click without a drag paints nothing; in a persistent tool it places
|
|
515
|
+
// the first corner and the next click the opposite one, like a Rectangle
|
|
516
|
+
// room. A persistent tool refuses a real box outside the room with a
|
|
517
|
+
// label; paint just drops it.
|
|
518
|
+
const nothing = persistent
|
|
519
|
+
? area([{ outer: box, holes: [] }]) < MIN_FLOOR_REGION_AREA
|
|
520
|
+
: !clipFloorRegion(box, draft.room.clear)
|
|
521
|
+
if (nothing && persistent && !draft.anchored) {
|
|
522
|
+
useFloorRegionDraft.setState({ draft: { ...draft, anchored: true } })
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
if (nothing) return endDraft(draft.room)
|
|
526
|
+
const result = commitDraft(draft.room, box)
|
|
527
|
+
if (result.ok) return
|
|
528
|
+
if (persistent) useFloorRegionDraft.setState({ draft: { ...draft, refusal: result.message } })
|
|
529
|
+
else endDraft(draft.room)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
const { points, cursor } = draft
|
|
533
|
+
if (!cursor) return
|
|
534
|
+
const message = polygonLiveMessage(points, cursor, closeRadius, draft.room)
|
|
535
|
+
if (message) {
|
|
536
|
+
useFloorRegionDraft.setState({ draft: { ...draft, message } })
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
if (closesOnFirst(points, cursor, closeRadius)) return finishPolygon(draft, points)
|
|
540
|
+
if (points.length && samePoint(points.at(-1)!, cursor)) return
|
|
541
|
+
useFloorRegionDraft.setState({
|
|
542
|
+
draft: { ...draft, points: [...points, cursor], message: null, refusal: null },
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Enter: closes the polygon on its first point (three points or more). */
|
|
547
|
+
export function finishFloorRegion() {
|
|
548
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
549
|
+
if (draft?.kind !== 'polygon' || draft.points.length < 3) return false
|
|
550
|
+
const { points } = draft
|
|
551
|
+
if (floorRegionEdgeCrosses(points, points[0]!, true)) {
|
|
552
|
+
useFloorRegionDraft.setState({ draft: { ...draft, message: CROSSING_MESSAGE } })
|
|
553
|
+
return true
|
|
554
|
+
}
|
|
555
|
+
finishPolygon(draft, points)
|
|
556
|
+
return true
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Backspace: drops the polygon's last point; the draft ends with its first point. */
|
|
560
|
+
export function removeLastFloorRegionPoint() {
|
|
561
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
562
|
+
if (draft?.kind !== 'polygon') return false
|
|
563
|
+
const points = draft.points.slice(0, -1)
|
|
564
|
+
if (!points.length) {
|
|
565
|
+
endDraft(draft.room)
|
|
566
|
+
return true
|
|
567
|
+
}
|
|
568
|
+
useFloorRegionDraft.setState({
|
|
569
|
+
draft: { ...draft, points, message: null, refusal: null },
|
|
570
|
+
})
|
|
571
|
+
return true
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Whether the draft's room is gone (undo, delete, another level). */
|
|
575
|
+
export function floorRegionRoomGone(nodes: Record<string, AnyNode>) {
|
|
576
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
577
|
+
if (!draft) return false
|
|
578
|
+
if (draft.room.ceilingId) {
|
|
579
|
+
const ceiling = nodes[draft.room.ceilingId as AnyNodeId]
|
|
580
|
+
return ceiling?.type !== 'ceiling' || ceiling.parentId !== draft.room.levelId
|
|
581
|
+
}
|
|
582
|
+
const zone = nodes[draft.room.zoneId as AnyNodeId]
|
|
583
|
+
return zone?.type !== 'zone' || zone.parentId !== draft.room.levelId
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Whether the scene can take a region now. */
|
|
587
|
+
export function floorRegionEditable() {
|
|
588
|
+
return !useScene.getState().readOnly
|
|
589
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { MultiPolygon } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
// Snapping for a floor region's points ("Paint part of the floor"): the
|
|
4
|
+
// polygon snap context's three modes. `grid` rounds to the grid step, `lines`
|
|
5
|
+
// pulls onto the room's corners and edges and the room's existing region
|
|
6
|
+
// vertices, `off` keeps the raw point. Alt (free) always keeps the raw point.
|
|
7
|
+
|
|
8
|
+
export type FloorRegionPoint = [number, number]
|
|
9
|
+
export type FloorRegionSnapMode = 'grid' | 'lines' | 'off'
|
|
10
|
+
|
|
11
|
+
export type FloorRegionSnapSettings = {
|
|
12
|
+
mode: FloorRegionSnapMode
|
|
13
|
+
/** Grid step in metres (`gridSnapStep`). */
|
|
14
|
+
step: number
|
|
15
|
+
/** Alt held: no snapping at all. */
|
|
16
|
+
free?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FloorRegionSnapTargets = {
|
|
20
|
+
vertices: FloorRegionPoint[]
|
|
21
|
+
edges: [FloorRegionPoint, FloorRegionPoint][]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type FloorRegionSnapResult = {
|
|
25
|
+
point: FloorRegionPoint
|
|
26
|
+
snap: 'grid' | 'vertex' | 'edge' | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const FLOOR_REGION_SNAP_RADIUS = 0.1
|
|
30
|
+
|
|
31
|
+
/** Corners and edges of the room's clear floor, plus the vertices and edges of its regions. */
|
|
32
|
+
export function floorRegionSnapTargets(
|
|
33
|
+
clear: MultiPolygon,
|
|
34
|
+
regions: readonly (readonly FloorRegionPoint[])[] = [],
|
|
35
|
+
): FloorRegionSnapTargets {
|
|
36
|
+
const vertices: FloorRegionPoint[] = []
|
|
37
|
+
const edges: [FloorRegionPoint, FloorRegionPoint][] = []
|
|
38
|
+
const rings = [...clear.flatMap(({ outer, holes }) => [outer, ...holes]), ...regions]
|
|
39
|
+
for (const ring of rings) {
|
|
40
|
+
for (let i = 0; i < ring.length; i++) {
|
|
41
|
+
const a = ring[i]!
|
|
42
|
+
const b = ring[(i + 1) % ring.length]!
|
|
43
|
+
vertices.push([a[0], a[1]])
|
|
44
|
+
edges.push([
|
|
45
|
+
[a[0], a[1]],
|
|
46
|
+
[b[0], b[1]],
|
|
47
|
+
])
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { vertices, edges }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function closestOnSegment(
|
|
54
|
+
p: FloorRegionPoint,
|
|
55
|
+
a: FloorRegionPoint,
|
|
56
|
+
b: FloorRegionPoint,
|
|
57
|
+
): FloorRegionPoint {
|
|
58
|
+
const dx = b[0] - a[0]
|
|
59
|
+
const dz = b[1] - a[1]
|
|
60
|
+
const lengthSq = dx * dx + dz * dz
|
|
61
|
+
const t = lengthSq
|
|
62
|
+
? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dz) / lengthSq))
|
|
63
|
+
: 0
|
|
64
|
+
return [a[0] + t * dx, a[1] + t * dz]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function snapFloorRegionPoint(
|
|
68
|
+
raw: FloorRegionPoint,
|
|
69
|
+
settings: FloorRegionSnapSettings,
|
|
70
|
+
targets: FloorRegionSnapTargets,
|
|
71
|
+
radius = FLOOR_REGION_SNAP_RADIUS,
|
|
72
|
+
): FloorRegionSnapResult {
|
|
73
|
+
if (settings.free || settings.mode === 'off') return { point: [raw[0], raw[1]], snap: null }
|
|
74
|
+
if (settings.mode === 'grid') {
|
|
75
|
+
const step = settings.step
|
|
76
|
+
if (!(step > 0)) return { point: [raw[0], raw[1]], snap: null }
|
|
77
|
+
const round = (value: number) => Number((Math.round(value / step) * step).toFixed(6))
|
|
78
|
+
return { point: [round(raw[0]), round(raw[1])], snap: 'grid' }
|
|
79
|
+
}
|
|
80
|
+
// Corners win over edges: a pointer near a corner lands on the corner.
|
|
81
|
+
let best: FloorRegionPoint | null = null
|
|
82
|
+
let bestDistance = radius
|
|
83
|
+
for (const vertex of targets.vertices) {
|
|
84
|
+
const distance = Math.hypot(vertex[0] - raw[0], vertex[1] - raw[1])
|
|
85
|
+
if (distance <= bestDistance) {
|
|
86
|
+
best = vertex
|
|
87
|
+
bestDistance = distance
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (best) return { point: [best[0], best[1]], snap: 'vertex' }
|
|
91
|
+
bestDistance = radius
|
|
92
|
+
for (const [a, b] of targets.edges) {
|
|
93
|
+
const candidate = closestOnSegment(raw, a, b)
|
|
94
|
+
const distance = Math.hypot(candidate[0] - raw[0], candidate[1] - raw[1])
|
|
95
|
+
if (distance <= bestDistance) {
|
|
96
|
+
best = candidate
|
|
97
|
+
bestDistance = distance
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return best ? { point: best, snap: 'edge' } : { point: [raw[0], raw[1]], snap: null }
|
|
101
|
+
}
|