@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,846 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
containsPoint,
|
|
5
|
+
emitter,
|
|
6
|
+
type MultiPolygon,
|
|
7
|
+
resolveCeilingHeight,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { getSceneTheme, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
13
|
+
import { Pentagon, Square } from 'lucide-react'
|
|
14
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
15
|
+
import {
|
|
16
|
+
BufferGeometry,
|
|
17
|
+
type Camera,
|
|
18
|
+
DoubleSide,
|
|
19
|
+
Float32BufferAttribute,
|
|
20
|
+
type Group,
|
|
21
|
+
Matrix4,
|
|
22
|
+
type Object3D,
|
|
23
|
+
type OrthographicCamera,
|
|
24
|
+
type PerspectiveCamera,
|
|
25
|
+
Plane,
|
|
26
|
+
Raycaster,
|
|
27
|
+
ShapeUtils,
|
|
28
|
+
Vector2,
|
|
29
|
+
Vector3,
|
|
30
|
+
} from 'three'
|
|
31
|
+
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
32
|
+
import { markToolCancelConsumed } from '../../hooks/use-keyboard'
|
|
33
|
+
import { getRoomSelectionIndex } from '../../hooks/use-selected-room'
|
|
34
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
35
|
+
import {
|
|
36
|
+
cancelOpeningDraft,
|
|
37
|
+
openingDraftActive,
|
|
38
|
+
useOpeningDraft,
|
|
39
|
+
} from '../../lib/floor-opening-draft'
|
|
40
|
+
import { clipFloorRegion, floorRegionRoomAt } from '../../lib/floor-region-geometry'
|
|
41
|
+
import {
|
|
42
|
+
cancelFloorRegion,
|
|
43
|
+
currentFloorRegionSnap,
|
|
44
|
+
describeCeilingRegionRoom,
|
|
45
|
+
describeFloorRegionRoom,
|
|
46
|
+
dropFloorRegionDraft,
|
|
47
|
+
type FloorRegionDraft,
|
|
48
|
+
type FloorRegionRoom,
|
|
49
|
+
finishFloorRegion,
|
|
50
|
+
floorRegionDraftMessage,
|
|
51
|
+
floorRegionDraftOutline,
|
|
52
|
+
floorRegionEditable,
|
|
53
|
+
floorRegionGesture,
|
|
54
|
+
moveFloorRegion,
|
|
55
|
+
pressFloorRegion,
|
|
56
|
+
releaseFloorRegion,
|
|
57
|
+
removeLastFloorRegionPoint,
|
|
58
|
+
setFloorRegionHover,
|
|
59
|
+
useFloorRegionDraft,
|
|
60
|
+
} from '../../lib/floor-region-session'
|
|
61
|
+
import { type FloorRegionPoint, snapFloorRegionPoint } from '../../lib/floor-region-snap'
|
|
62
|
+
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
63
|
+
import { formatAreaLabel } from '../../lib/measurements'
|
|
64
|
+
import {
|
|
65
|
+
cancelMezzanineDraft,
|
|
66
|
+
mezzanineDraftActive,
|
|
67
|
+
useMezzanineDraft,
|
|
68
|
+
} from '../../lib/mezzanine-draft'
|
|
69
|
+
import { usePaintRegionMode } from '../../lib/paint-region-mode'
|
|
70
|
+
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
71
|
+
import {
|
|
72
|
+
cancelTerraceDraft,
|
|
73
|
+
terraceDraftActive,
|
|
74
|
+
toggleTerraceShape,
|
|
75
|
+
useTerraceDraft,
|
|
76
|
+
} from '../../lib/terrace-draft'
|
|
77
|
+
import { isTypingTarget } from '../../lib/typing-target'
|
|
78
|
+
import useEditor from '../../store/use-editor'
|
|
79
|
+
import { useFloorplanRender } from '../editor-2d/floorplan-render-context'
|
|
80
|
+
import { usePaintTint } from '../tools/paint-region/use-paint-tint'
|
|
81
|
+
import { CursorSphere } from '../tools/shared/cursor-sphere'
|
|
82
|
+
import {
|
|
83
|
+
DRAFT_LABEL_Y_OFFSET,
|
|
84
|
+
DraftMeasurementLabel,
|
|
85
|
+
} from '../tools/shared/draft-measurement-label'
|
|
86
|
+
import { swallowNextClick } from './handles/use-handle-drag'
|
|
87
|
+
import { RoomFloorHighlight3D } from './room-floor-highlight'
|
|
88
|
+
|
|
89
|
+
// An outline on a room's floor, in 3D and on the plan: the paint tool's
|
|
90
|
+
// rectangle and polygon sub-modes ("Paint part of the floor"), and the armed
|
|
91
|
+
// "Add mezzanine" tool, which draws in its host room on the mezzanine's plane.
|
|
92
|
+
// The session (`floor-region-session`) owns the draft; this file turns
|
|
93
|
+
// pointers into plan points on a room's floor and draws the draft in the wall
|
|
94
|
+
// draft's colours.
|
|
95
|
+
|
|
96
|
+
const DRAFT_COLOR = '#818cf8'
|
|
97
|
+
const DRAFT_COLOR_LIGHT_PLAN = '#6366f1'
|
|
98
|
+
const INVALID_COLOR = '#ef4444'
|
|
99
|
+
/** How close (screen pixels) a click lands to the first point to close the polygon, in 3D and on the plan. */
|
|
100
|
+
const CLOSE_RADIUS_PX = 10
|
|
101
|
+
/** The 3D close radius (m) when the camera cannot say how big a pixel is. */
|
|
102
|
+
const CLOSE_RADIUS_3D_FALLBACK = 0.2
|
|
103
|
+
const PREVIEW_LIFT = 0.012
|
|
104
|
+
|
|
105
|
+
type FloorRegionMode = 'rectangle' | 'polygon'
|
|
106
|
+
type FloorRegionHit = { room: FloorRegionRoom; point: FloorRegionPoint }
|
|
107
|
+
|
|
108
|
+
function activeMode(): FloorRegionMode | null {
|
|
109
|
+
if (!floorRegionEditable()) return null
|
|
110
|
+
const opening = useOpeningDraft.getState()
|
|
111
|
+
if (opening.host) return opening.shape
|
|
112
|
+
const mezzanine = useMezzanineDraft.getState()
|
|
113
|
+
if (mezzanine.host) return mezzanine.shape
|
|
114
|
+
const terrace = useTerraceDraft.getState()
|
|
115
|
+
if (terrace.host) return terrace.shape
|
|
116
|
+
if (useEditor.getState().mode !== 'material-paint') return null
|
|
117
|
+
const mode = usePaintRegionMode.getState().mode
|
|
118
|
+
return mode === 'rectangle' || mode === 'polygon' ? mode : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function useFloorRegionMode(): FloorRegionMode | null {
|
|
122
|
+
const editorMode = useEditor((s) => s.mode)
|
|
123
|
+
const regionMode = usePaintRegionMode((s) => s.mode)
|
|
124
|
+
const mezzanine = useMezzanineDraft((s) => (s.host ? s.shape : null))
|
|
125
|
+
const terrace = useTerraceDraft((s) => (s.host ? s.shape : null))
|
|
126
|
+
const opening = useOpeningDraft((s) => (s.host ? s.shape : null))
|
|
127
|
+
const readOnly = useScene((s) => s.readOnly)
|
|
128
|
+
if (readOnly) return null
|
|
129
|
+
if (opening) return opening
|
|
130
|
+
if (mezzanine) return mezzanine
|
|
131
|
+
if (terrace) return terrace
|
|
132
|
+
if (editorMode !== 'material-paint') return null
|
|
133
|
+
return regionMode === 'rectangle' || regionMode === 'polygon' ? regionMode : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function useSceneIsDark() {
|
|
137
|
+
return useViewer((s) => getSceneTheme(s.sceneTheme).appearance === 'dark')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The mezzanine tool owns Escape, Backspace, Enter (and the terrace tool T) while
|
|
141
|
+
// armed, draft or not, so none of them reaches the selected room (Backspace
|
|
142
|
+
// would delete it).
|
|
143
|
+
function floorRegionKey(event: KeyboardEvent) {
|
|
144
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
145
|
+
const terrace = terraceDraftActive()
|
|
146
|
+
const opening = openingDraftActive()
|
|
147
|
+
const mezzanine = mezzanineDraftActive() || terrace || opening
|
|
148
|
+
if (!(draft || mezzanine) || event.defaultPrevented || isTypingTarget(event.target)) return
|
|
149
|
+
if (event.key === 'Escape') {
|
|
150
|
+
if (opening) cancelOpeningDraft()
|
|
151
|
+
else if (terrace) cancelTerraceDraft()
|
|
152
|
+
else if (mezzanine) cancelMezzanineDraft()
|
|
153
|
+
else cancelFloorRegion()
|
|
154
|
+
} else if (event.key === 'Backspace') {
|
|
155
|
+
if (!(removeLastFloorRegionPoint() || mezzanine)) return
|
|
156
|
+
} else if (event.key === 'Enter') {
|
|
157
|
+
if (!(finishFloorRegion() || mezzanine)) return
|
|
158
|
+
} else if (
|
|
159
|
+
terrace &&
|
|
160
|
+
(event.key === 't' || event.key === 'T') &&
|
|
161
|
+
!(event.metaKey || event.ctrlKey || event.altKey)
|
|
162
|
+
)
|
|
163
|
+
toggleTerraceShape()
|
|
164
|
+
else return
|
|
165
|
+
event.preventDefault()
|
|
166
|
+
event.stopPropagation()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// One keyboard listener per target however many surfaces bind to it (split
|
|
170
|
+
// view mounts the plan and the 3D controller together).
|
|
171
|
+
const keyboardOwners = new WeakMap<EventTarget, number>()
|
|
172
|
+
function acquireKeyboard(target: EventTarget) {
|
|
173
|
+
const owners = keyboardOwners.get(target) ?? 0
|
|
174
|
+
if (owners === 0) target.addEventListener('keydown', floorRegionKey as EventListener, true)
|
|
175
|
+
keyboardOwners.set(target, owners + 1)
|
|
176
|
+
let released = false
|
|
177
|
+
return () => {
|
|
178
|
+
if (released) return
|
|
179
|
+
released = true
|
|
180
|
+
const left = (keyboardOwners.get(target) ?? 1) - 1
|
|
181
|
+
keyboardOwners.set(target, left)
|
|
182
|
+
if (left === 0) target.removeEventListener('keydown', floorRegionKey as EventListener, true)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Binds a surface to the floor region draft. `resolve` answers where the
|
|
188
|
+
* pointer is on a room floor: while a draft is open, on that draft's floor;
|
|
189
|
+
* otherwise the room under the pointer, or null when the floor is not what
|
|
190
|
+
* the pointer is on (another agent's surface keeps the event).
|
|
191
|
+
*/
|
|
192
|
+
export function bindFloorRegionPointer(
|
|
193
|
+
surface: EventTarget,
|
|
194
|
+
resolve: (event: PointerEvent) => FloorRegionHit | null,
|
|
195
|
+
closeRadius: () => number,
|
|
196
|
+
keyboard: EventTarget = window,
|
|
197
|
+
) {
|
|
198
|
+
let pressed: number | null = null
|
|
199
|
+
const capture = surface as Element
|
|
200
|
+
// Moves are coalesced to one update per animation frame; a press drops the
|
|
201
|
+
// pending move and acts on its own position at once.
|
|
202
|
+
let frame = 0
|
|
203
|
+
let pending: PointerEvent | null = null
|
|
204
|
+
const flush = () => {
|
|
205
|
+
frame = 0
|
|
206
|
+
const event = pending
|
|
207
|
+
pending = null
|
|
208
|
+
if (!(event && activeMode())) return
|
|
209
|
+
const hit = resolve(event)
|
|
210
|
+
const settings = currentFloorRegionSnap(event.altKey)
|
|
211
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
212
|
+
// A refused box stays put until the next press; the marker shows meanwhile.
|
|
213
|
+
if (draft && !(draft.kind === 'rectangle' && draft.refusal)) {
|
|
214
|
+
setFloorRegionHover(null)
|
|
215
|
+
if (hit) moveFloorRegion(hit.point, settings, closeRadius())
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
setFloorRegionHover(
|
|
219
|
+
hit
|
|
220
|
+
? {
|
|
221
|
+
levelId: hit.room.levelId,
|
|
222
|
+
elevation: hit.room.elevation,
|
|
223
|
+
point: snapFloorRegionPoint(hit.point, settings, hit.room.targets).point,
|
|
224
|
+
...(hit.room.ceilingId ? { ceiling: true } : {}),
|
|
225
|
+
}
|
|
226
|
+
: null,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
const dropPending = () => {
|
|
230
|
+
if (frame) cancelAnimationFrame(frame)
|
|
231
|
+
frame = 0
|
|
232
|
+
pending = null
|
|
233
|
+
}
|
|
234
|
+
const move = (event: PointerEvent) => {
|
|
235
|
+
if (!activeMode()) return
|
|
236
|
+
pending = event
|
|
237
|
+
if (!frame) frame = requestAnimationFrame(flush) || 0
|
|
238
|
+
}
|
|
239
|
+
const down = (event: PointerEvent) => {
|
|
240
|
+
const mode = activeMode()
|
|
241
|
+
if (event.button !== 0 || !mode || useViewer.getState().cameraDragging) return
|
|
242
|
+
const hit = resolve(event)
|
|
243
|
+
if (!hit) return
|
|
244
|
+
dropPending()
|
|
245
|
+
event.preventDefault()
|
|
246
|
+
event.stopPropagation()
|
|
247
|
+
setFloorRegionHover(null)
|
|
248
|
+
pressFloorRegion(mode, hit.room, hit.point, currentFloorRegionSnap(event.altKey), closeRadius())
|
|
249
|
+
pressed = event.pointerId
|
|
250
|
+
// A box dragged off the surface keeps following the pointer. The capture is
|
|
251
|
+
// the gesture owner's: released on release, cancel or any lifecycle end.
|
|
252
|
+
if ('setPointerCapture' in capture)
|
|
253
|
+
floorRegionGesture()?.capturePointer(capture, event.pointerId)
|
|
254
|
+
}
|
|
255
|
+
const up = (event: PointerEvent) => {
|
|
256
|
+
if (pressed !== event.pointerId) return
|
|
257
|
+
pressed = null
|
|
258
|
+
floorRegionGesture()?.releasePointer(event.pointerId)
|
|
259
|
+
// The last move lands before the release, so the box ends where the pointer let go.
|
|
260
|
+
if (frame) cancelAnimationFrame(frame)
|
|
261
|
+
flush()
|
|
262
|
+
event.preventDefault()
|
|
263
|
+
event.stopPropagation()
|
|
264
|
+
// The click the browser synthesizes from this release belongs to the draft:
|
|
265
|
+
// it must not deselect what the commit just selected.
|
|
266
|
+
swallowNextClick()
|
|
267
|
+
useViewer.getState().setInputDragging(true)
|
|
268
|
+
releaseFloorRegion(closeRadius())
|
|
269
|
+
setTimeout(() => useViewer.getState().setInputDragging(false), 0)
|
|
270
|
+
}
|
|
271
|
+
// The system took the pointer (a touch turned into a scroll, a lost window):
|
|
272
|
+
// a box ends uncommitted; a polygon keeps its placed points.
|
|
273
|
+
const cancel = (event: PointerEvent) => {
|
|
274
|
+
if (pressed !== event.pointerId) return
|
|
275
|
+
pressed = null
|
|
276
|
+
dropPending()
|
|
277
|
+
floorRegionGesture()?.releasePointer(event.pointerId)
|
|
278
|
+
if (useFloorRegionDraft.getState().draft?.kind === 'rectangle') dropFloorRegionDraft()
|
|
279
|
+
}
|
|
280
|
+
const leave = () => {
|
|
281
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
282
|
+
if (!draft || (draft.kind === 'rectangle' && draft.refusal)) setFloorRegionHover(null)
|
|
283
|
+
}
|
|
284
|
+
const onMove = (event: Event) => move(event as PointerEvent)
|
|
285
|
+
const onDown = (event: Event) => down(event as PointerEvent)
|
|
286
|
+
surface.addEventListener('pointermove', onMove, true)
|
|
287
|
+
surface.addEventListener('pointerdown', onDown, true)
|
|
288
|
+
surface.addEventListener('pointerleave', leave)
|
|
289
|
+
keyboard.addEventListener('pointerup', up as EventListener, true)
|
|
290
|
+
keyboard.addEventListener('pointercancel', cancel as EventListener, true)
|
|
291
|
+
const releaseKeyboard = acquireKeyboard(keyboard)
|
|
292
|
+
return () => {
|
|
293
|
+
dropPending()
|
|
294
|
+
if (pressed !== null) floorRegionGesture()?.releasePointer(pressed)
|
|
295
|
+
surface.removeEventListener('pointermove', onMove, true)
|
|
296
|
+
surface.removeEventListener('pointerdown', onDown, true)
|
|
297
|
+
surface.removeEventListener('pointerleave', leave)
|
|
298
|
+
keyboard.removeEventListener('pointerup', up as EventListener, true)
|
|
299
|
+
keyboard.removeEventListener('pointercancel', cancel as EventListener, true)
|
|
300
|
+
releaseKeyboard()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The draft's own lifetime lives with the gesture lifecycle owner (sub-mode,
|
|
306
|
+
* mode, level or selection change, its room gone, its scope replaced, any
|
|
307
|
+
* history command). Here: Escape (`tool:cancel`, consumed so the paint mode
|
|
308
|
+
* stays), the hover marker on a level switch, and the surface unmounting.
|
|
309
|
+
*/
|
|
310
|
+
function useFloorRegionLifetime(mode: FloorRegionMode | null) {
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!mode) setFloorRegionHover(null)
|
|
313
|
+
}, [mode])
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const stopViewer = useViewer.subscribe((state, previous) => {
|
|
316
|
+
if (state.selection.levelId !== previous.selection.levelId) setFloorRegionHover(null)
|
|
317
|
+
})
|
|
318
|
+
const onToolCancel = () => {
|
|
319
|
+
if (terraceDraftActive()) cancelTerraceDraft()
|
|
320
|
+
else if (mezzanineDraftActive()) cancelMezzanineDraft()
|
|
321
|
+
else if (useFloorRegionDraft.getState().draft) cancelFloorRegion()
|
|
322
|
+
else return
|
|
323
|
+
markToolCancelConsumed()
|
|
324
|
+
}
|
|
325
|
+
emitter.on('tool:cancel', onToolCancel)
|
|
326
|
+
editingSurfaces++
|
|
327
|
+
return () => {
|
|
328
|
+
stopViewer()
|
|
329
|
+
emitter.off('tool:cancel', onToolCancel)
|
|
330
|
+
cancelFloorRegion()
|
|
331
|
+
setFloorRegionHover(null)
|
|
332
|
+
editingSurfaces--
|
|
333
|
+
// Before its first press the mezzanine tool owns no draft, only its armed
|
|
334
|
+
// state and scope; the last surface going away (capture mode, the editor
|
|
335
|
+
// closing) must not leave them behind.
|
|
336
|
+
if (editingSurfaces === 0 && mezzanineDraftActive()) cancelMezzanineDraft()
|
|
337
|
+
if (editingSurfaces === 0 && terraceDraftActive()) cancelTerraceDraft()
|
|
338
|
+
}
|
|
339
|
+
}, [])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The mounted 3D / 2D drawing surfaces; the mezzanine tool lives only while one is. */
|
|
343
|
+
let editingSurfaces = 0
|
|
344
|
+
|
|
345
|
+
/** The room whose clear floor holds `point` on `levelId`, as a draft room. */
|
|
346
|
+
export function resolveFloorRegionRoom(levelId: string, point: FloorRegionPoint) {
|
|
347
|
+
const nodes = useScene.getState().nodes
|
|
348
|
+
const record = floorRegionRoomAt(getRoomSelectionIndex(levelId).update(nodes), point, (room) =>
|
|
349
|
+
roomFloorElevation(nodes, room.zoneId),
|
|
350
|
+
)
|
|
351
|
+
return record ? describeFloorRegionRoom(nodes, record) : null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// A cutaway hides walls without unregistering them, and an invisible wall must
|
|
355
|
+
// not answer a ray the user aimed at the floor behind it.
|
|
356
|
+
function visibleSurface(object: Object3D) {
|
|
357
|
+
for (let node: Object3D | null = object; node; node = node.parent)
|
|
358
|
+
if (!node.visible || node.userData.wallHidden === true) return false
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Distance (level space) from `origin` to the nearest visible wall of the level along the ray. */
|
|
363
|
+
function nearestWallDistance(
|
|
364
|
+
raycaster: Raycaster,
|
|
365
|
+
level: Object3D,
|
|
366
|
+
levelId: string,
|
|
367
|
+
origin: Vector3,
|
|
368
|
+
) {
|
|
369
|
+
const nodes = useScene.getState().nodes
|
|
370
|
+
let nearest = Number.POSITIVE_INFINITY
|
|
371
|
+
for (const id of sceneRegistry.byType.wall ?? []) {
|
|
372
|
+
if (nodes[id as keyof typeof nodes]?.parentId !== levelId) continue
|
|
373
|
+
const object = sceneRegistry.nodes.get(id)
|
|
374
|
+
const hit = object
|
|
375
|
+
? raycaster
|
|
376
|
+
.intersectObject(object, true)
|
|
377
|
+
.find((candidate) => visibleSurface(candidate.object))
|
|
378
|
+
: undefined
|
|
379
|
+
if (!hit) continue
|
|
380
|
+
nearest = Math.min(nearest, origin.distanceTo(level.worldToLocal(hit.point.clone())))
|
|
381
|
+
}
|
|
382
|
+
return nearest
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const UP = new Vector3(0, 1, 0)
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The 3D pointer on a room floor. With a draft open (or the mezzanine tool
|
|
389
|
+
* armed), the ray meets that room's plane whatever stands in front of it.
|
|
390
|
+
* Otherwise it meets each room's floor plane (at its own elevation); the
|
|
391
|
+
* nearest landing inside a room's clear floor wins, unless a visible wall of
|
|
392
|
+
* the level is nearer along the ray — then the wall's paint gesture owns the
|
|
393
|
+
* press.
|
|
394
|
+
*/
|
|
395
|
+
function resolveFloorRegion3D(
|
|
396
|
+
event: PointerEvent,
|
|
397
|
+
element: HTMLElement,
|
|
398
|
+
camera: Camera,
|
|
399
|
+
): FloorRegionHit | null {
|
|
400
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
401
|
+
const fixed =
|
|
402
|
+
draft?.room ??
|
|
403
|
+
useOpeningDraft.getState().host ??
|
|
404
|
+
useMezzanineDraft.getState().host ??
|
|
405
|
+
useTerraceDraft.getState().host
|
|
406
|
+
const levelId = fixed?.levelId ?? useViewer.getState().selection.levelId
|
|
407
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
408
|
+
if (!(levelId && level)) return null
|
|
409
|
+
level.updateWorldMatrix(true, false)
|
|
410
|
+
const bounds = element.getBoundingClientRect()
|
|
411
|
+
const raycaster = new Raycaster()
|
|
412
|
+
raycaster.setFromCamera(
|
|
413
|
+
new Vector2(
|
|
414
|
+
((event.clientX - bounds.left) / bounds.width) * 2 - 1,
|
|
415
|
+
1 - ((event.clientY - bounds.top) / bounds.height) * 2,
|
|
416
|
+
),
|
|
417
|
+
camera,
|
|
418
|
+
)
|
|
419
|
+
setSurfaceRaycastLayers(raycaster.layers)
|
|
420
|
+
const ray = raycaster.ray.clone().applyMatrix4(new Matrix4().copy(level.matrixWorld).invert())
|
|
421
|
+
const onFloor = (elevation: number) =>
|
|
422
|
+
ray.intersectPlane(new Plane(UP, -elevation), new Vector3())
|
|
423
|
+
if (fixed) {
|
|
424
|
+
const hit = onFloor(fixed.elevation)
|
|
425
|
+
return hit ? { room: fixed, point: [hit.x, hit.z] } : null
|
|
426
|
+
}
|
|
427
|
+
const nodes = useScene.getState().nodes
|
|
428
|
+
let best: {
|
|
429
|
+
room: () => FloorRegionRoom
|
|
430
|
+
point: FloorRegionPoint
|
|
431
|
+
distance: number
|
|
432
|
+
} | null = null
|
|
433
|
+
for (const record of getRoomSelectionIndex(levelId).update(nodes)) {
|
|
434
|
+
const hit = onFloor(roomFloorElevation(nodes, record.zoneId))
|
|
435
|
+
if (!hit) continue
|
|
436
|
+
const point: FloorRegionPoint = [hit.x, hit.z]
|
|
437
|
+
if (!containsPoint(record.clearPolygon, point)) continue
|
|
438
|
+
const distance = ray.origin.distanceTo(hit)
|
|
439
|
+
if (!best || distance < best.distance)
|
|
440
|
+
best = { room: () => describeFloorRegionRoom(nodes, record), point, distance }
|
|
441
|
+
}
|
|
442
|
+
// Painting also draws on a ceiling seen from below: its underside, where it draws.
|
|
443
|
+
if (useEditor.getState().mode === 'material-paint' && ray.direction.y > 1e-6)
|
|
444
|
+
for (const id of sceneRegistry.byType.ceiling ?? []) {
|
|
445
|
+
const ceiling = nodes[id as keyof typeof nodes]
|
|
446
|
+
if (ceiling?.type !== 'ceiling' || ceiling.parentId !== levelId) continue
|
|
447
|
+
const object = sceneRegistry.nodes.get(id)
|
|
448
|
+
if (!(object && visibleSurface(object))) continue
|
|
449
|
+
const hit = onFloor(resolveCeilingHeight(ceiling, nodes) - 0.01)
|
|
450
|
+
if (!hit) continue
|
|
451
|
+
const point: FloorRegionPoint = [hit.x, hit.z]
|
|
452
|
+
if (!containsPoint([{ outer: ceiling.polygon, holes: ceiling.holes }], point)) continue
|
|
453
|
+
const distance = ray.origin.distanceTo(hit)
|
|
454
|
+
if (!best || distance < best.distance)
|
|
455
|
+
best = { room: () => describeCeilingRegionRoom(nodes, ceiling), point, distance }
|
|
456
|
+
}
|
|
457
|
+
if (!best || nearestWallDistance(raycaster, level, levelId, ray.origin) < best.distance)
|
|
458
|
+
return null
|
|
459
|
+
return { room: best.room(), point: best.point }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function ringPairs(ring: readonly FloorRegionPoint[], closed: boolean, y: number) {
|
|
463
|
+
const out: number[] = []
|
|
464
|
+
const count = closed ? ring.length : ring.length - 1
|
|
465
|
+
for (let i = 0; i < count; i++) {
|
|
466
|
+
const a = ring[i]!
|
|
467
|
+
const b = ring[(i + 1) % ring.length]!
|
|
468
|
+
out.push(a[0], y, a[1], b[0], y, b[1])
|
|
469
|
+
}
|
|
470
|
+
return out
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function fillPositions(pieces: MultiPolygon, y: number) {
|
|
474
|
+
const out: number[] = []
|
|
475
|
+
for (const { outer, holes } of pieces) {
|
|
476
|
+
const contour = outer.map(([x, z]) => new Vector2(x, z))
|
|
477
|
+
const holeRings = holes.map((hole) => hole.map(([x, z]) => new Vector2(x, z)))
|
|
478
|
+
const all = [...contour, ...holeRings.flat()]
|
|
479
|
+
for (const triangle of ShapeUtils.triangulateShape(contour, holeRings))
|
|
480
|
+
for (const index of triangle) out.push(all[index]!.x, y, all[index]!.y)
|
|
481
|
+
}
|
|
482
|
+
return out
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function geometryOf(positions: number[]) {
|
|
486
|
+
const geometry = new BufferGeometry()
|
|
487
|
+
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
|
|
488
|
+
return geometry
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const noRaycast = () => {}
|
|
492
|
+
|
|
493
|
+
/** What the draft shows: its outline, the clipped pieces, the label and its colour. */
|
|
494
|
+
function useDraftView(draft: FloorRegionDraft | null) {
|
|
495
|
+
const unit = useViewer((s) => s.unit)
|
|
496
|
+
return useMemo(() => {
|
|
497
|
+
if (!draft) return null
|
|
498
|
+
const outline = floorRegionDraftOutline(draft)
|
|
499
|
+
const clip = outline.length >= 3 ? clipFloorRegion(outline, draft.room.clear) : null
|
|
500
|
+
const message = floorRegionDraftMessage(draft)
|
|
501
|
+
const xs = outline.map((point) => point[0])
|
|
502
|
+
const zs = outline.map((point) => point[1])
|
|
503
|
+
const anchor: FloorRegionPoint | null = outline.length
|
|
504
|
+
? [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...zs) + Math.max(...zs)) / 2]
|
|
505
|
+
: null
|
|
506
|
+
return {
|
|
507
|
+
outline,
|
|
508
|
+
closed: draft.kind === 'rectangle' || outline.length >= 3,
|
|
509
|
+
clip,
|
|
510
|
+
message,
|
|
511
|
+
label: message ?? (clip ? formatAreaLabel(clip.area, unit) : null),
|
|
512
|
+
anchor: message && draft.kind === 'polygon' ? (draft.cursor ?? anchor) : anchor,
|
|
513
|
+
points: draft.kind === 'polygon' ? draft.points : [],
|
|
514
|
+
cursor: draft.kind === 'polygon' ? draft.cursor : draft.end,
|
|
515
|
+
fill: draft.room.purpose?.fill ?? null,
|
|
516
|
+
}
|
|
517
|
+
}, [draft, unit])
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function FloorRegionPreview3D({ mode }: { mode: FloorRegionMode }) {
|
|
521
|
+
const draft = useFloorRegionDraft((s) => s.draft)
|
|
522
|
+
const hover = useFloorRegionDraft((s) => s.hover)
|
|
523
|
+
const mezzanineHost = useMezzanineDraft((s) => s.host)
|
|
524
|
+
const openingHost = useOpeningDraft((s) => s.host)
|
|
525
|
+
const host = openingHost ?? mezzanineHost
|
|
526
|
+
const view = useDraftView(draft)
|
|
527
|
+
const isDark = useSceneIsDark()
|
|
528
|
+
const tint = usePaintTint(DRAFT_COLOR)
|
|
529
|
+
const color = view?.fill ?? tint
|
|
530
|
+
const levelId = draft?.room.levelId ?? hover?.levelId ?? host?.levelId ?? null
|
|
531
|
+
const elevation = draft?.room.elevation ?? hover?.elevation ?? host?.elevation ?? 0
|
|
532
|
+
// A ceiling's outline hangs just under it, a floor's just over it.
|
|
533
|
+
const onCeiling = !!(draft?.room.ceilingId ?? hover?.ceiling)
|
|
534
|
+
const lift = elevation + (onCeiling ? -PREVIEW_LIFT : PREVIEW_LIFT)
|
|
535
|
+
const root = useRef<Group>(null)
|
|
536
|
+
const materials = useMemo(
|
|
537
|
+
() => ({
|
|
538
|
+
fill: new MeshBasicNodeMaterial({
|
|
539
|
+
depthTest: false,
|
|
540
|
+
depthWrite: false,
|
|
541
|
+
opacity: 0.3,
|
|
542
|
+
side: DoubleSide,
|
|
543
|
+
transparent: true,
|
|
544
|
+
}),
|
|
545
|
+
line: new LineBasicNodeMaterial({ depthTest: false, depthWrite: false }),
|
|
546
|
+
}),
|
|
547
|
+
[],
|
|
548
|
+
)
|
|
549
|
+
useEffect(
|
|
550
|
+
() => () => {
|
|
551
|
+
materials.fill.dispose()
|
|
552
|
+
materials.line.dispose()
|
|
553
|
+
},
|
|
554
|
+
[materials],
|
|
555
|
+
)
|
|
556
|
+
const invalid = !!view?.message
|
|
557
|
+
useEffect(() => {
|
|
558
|
+
materials.fill.color.set(color)
|
|
559
|
+
materials.line.color.set(invalid ? INVALID_COLOR : DRAFT_COLOR)
|
|
560
|
+
}, [materials, color, invalid])
|
|
561
|
+
const geometry = useMemo(
|
|
562
|
+
() =>
|
|
563
|
+
view
|
|
564
|
+
? {
|
|
565
|
+
fill: view.clip ? geometryOf(fillPositions(view.clip.pieces, lift)) : null,
|
|
566
|
+
line: geometryOf(ringPairs(view.outline, view.closed, lift)),
|
|
567
|
+
}
|
|
568
|
+
: null,
|
|
569
|
+
[view, lift],
|
|
570
|
+
)
|
|
571
|
+
useEffect(
|
|
572
|
+
() => () => {
|
|
573
|
+
geometry?.fill?.dispose()
|
|
574
|
+
geometry?.line.dispose()
|
|
575
|
+
},
|
|
576
|
+
[geometry],
|
|
577
|
+
)
|
|
578
|
+
useFrame(() => {
|
|
579
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
580
|
+
if (!root.current) return
|
|
581
|
+
root.current.visible = !!level
|
|
582
|
+
if (level) {
|
|
583
|
+
level.updateWorldMatrix(true, false)
|
|
584
|
+
root.current.matrix.copy(level.matrixWorld)
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
if (!levelId) return null
|
|
588
|
+
const Icon = mode === 'polygon' ? Pentagon : Square
|
|
589
|
+
const cursor = view?.cursor ?? (draft ? null : (hover?.point ?? null))
|
|
590
|
+
return (
|
|
591
|
+
<group matrixAutoUpdate={false} ref={root}>
|
|
592
|
+
{/* Where a mezzanine can go: the host's clear floor on the mezzanine's
|
|
593
|
+
plane, drawn over its walls like Divide's room, under the draft. */}
|
|
594
|
+
{host && (
|
|
595
|
+
<RoomFloorHighlight3D
|
|
596
|
+
elevation={host.elevation}
|
|
597
|
+
levelId={host.levelId}
|
|
598
|
+
renderOrder={98}
|
|
599
|
+
zoneId={host.zoneId}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
602
|
+
{geometry?.fill && (
|
|
603
|
+
<mesh
|
|
604
|
+
geometry={geometry.fill}
|
|
605
|
+
layers={EDITOR_LAYER}
|
|
606
|
+
material={materials.fill}
|
|
607
|
+
raycast={noRaycast}
|
|
608
|
+
renderOrder={100}
|
|
609
|
+
/>
|
|
610
|
+
)}
|
|
611
|
+
{geometry && (
|
|
612
|
+
<lineSegments
|
|
613
|
+
geometry={geometry.line}
|
|
614
|
+
layers={EDITOR_LAYER}
|
|
615
|
+
material={materials.line}
|
|
616
|
+
raycast={noRaycast}
|
|
617
|
+
renderOrder={101}
|
|
618
|
+
/>
|
|
619
|
+
)}
|
|
620
|
+
{view?.points.map((point, index) => (
|
|
621
|
+
<CursorSphere
|
|
622
|
+
color={DRAFT_COLOR}
|
|
623
|
+
height={0}
|
|
624
|
+
key={index}
|
|
625
|
+
position={[point[0], elevation, point[1]]}
|
|
626
|
+
showTooltip={false}
|
|
627
|
+
/>
|
|
628
|
+
))}
|
|
629
|
+
{cursor && (
|
|
630
|
+
<CursorSphere
|
|
631
|
+
color={invalid ? INVALID_COLOR : DRAFT_COLOR}
|
|
632
|
+
height={0}
|
|
633
|
+
position={[cursor[0], elevation, cursor[1]]}
|
|
634
|
+
tooltipContent={<Icon className="h-5 w-5 text-white" />}
|
|
635
|
+
/>
|
|
636
|
+
)}
|
|
637
|
+
{view?.label && view.anchor && (
|
|
638
|
+
<DraftMeasurementLabel
|
|
639
|
+
color={invalid ? INVALID_COLOR : isDark ? '#ffffff' : '#111111'}
|
|
640
|
+
label={view.label}
|
|
641
|
+
position={[view.anchor[0], elevation + DRAFT_LABEL_Y_OFFSET, view.anchor[1]]}
|
|
642
|
+
shadowColor={isDark ? '#111111' : '#ffffff'}
|
|
643
|
+
/>
|
|
644
|
+
)}
|
|
645
|
+
</group>
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The 3D close radius: CLOSE_RADIUS_PX at the polygon's first point's depth,
|
|
651
|
+
* so closing feels the same zoomed in or out.
|
|
652
|
+
*/
|
|
653
|
+
export function floorRegionCloseRadius3D(
|
|
654
|
+
camera: Camera,
|
|
655
|
+
viewportHeight: number,
|
|
656
|
+
pointWorld: Vector3 | null,
|
|
657
|
+
): number {
|
|
658
|
+
if (!(pointWorld && viewportHeight > 0)) return CLOSE_RADIUS_3D_FALLBACK
|
|
659
|
+
let perPixel: number
|
|
660
|
+
if ((camera as OrthographicCamera).isOrthographicCamera) {
|
|
661
|
+
const ortho = camera as OrthographicCamera
|
|
662
|
+
perPixel = (ortho.top - ortho.bottom) / ortho.zoom / viewportHeight
|
|
663
|
+
} else {
|
|
664
|
+
const perspective = camera as PerspectiveCamera
|
|
665
|
+
const distance = perspective.getWorldPosition(new Vector3()).distanceTo(pointWorld)
|
|
666
|
+
perPixel = (2 * distance * Math.tan((perspective.fov * Math.PI) / 360)) / viewportHeight
|
|
667
|
+
}
|
|
668
|
+
return Number.isFinite(perPixel) && perPixel > 0
|
|
669
|
+
? CLOSE_RADIUS_PX * perPixel
|
|
670
|
+
: CLOSE_RADIUS_3D_FALLBACK
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function firstPointWorld(): Vector3 | null {
|
|
674
|
+
const draft = useFloorRegionDraft.getState().draft
|
|
675
|
+
const first = draft?.kind === 'polygon' ? draft.points[0] : undefined
|
|
676
|
+
const level = draft ? sceneRegistry.nodes.get(draft.room.levelId) : undefined
|
|
677
|
+
if (!(draft && first && level)) return null
|
|
678
|
+
level.updateWorldMatrix(true, false)
|
|
679
|
+
return level.localToWorld(new Vector3(first[0], draft.room.elevation, first[1]))
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function FloorRegionControls3D() {
|
|
683
|
+
const mode = useFloorRegionMode()
|
|
684
|
+
const { gl, camera } = useThree()
|
|
685
|
+
useFloorRegionLifetime(mode)
|
|
686
|
+
useEffect(() => {
|
|
687
|
+
if (!mode) return
|
|
688
|
+
return bindFloorRegionPointer(
|
|
689
|
+
gl.domElement,
|
|
690
|
+
(event) =>
|
|
691
|
+
event.target === gl.domElement ? resolveFloorRegion3D(event, gl.domElement, camera) : null,
|
|
692
|
+
() =>
|
|
693
|
+
floorRegionCloseRadius3D(
|
|
694
|
+
camera,
|
|
695
|
+
gl.domElement.getBoundingClientRect().height,
|
|
696
|
+
firstPointWorld(),
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
}, [gl, camera, mode])
|
|
700
|
+
return mode ? <FloorRegionPreview3D mode={mode} /> : null
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The plan pointer on a room floor of `levelId` (the draft's floor while one
|
|
705
|
+
* is open, the host room while the mezzanine tool is armed).
|
|
706
|
+
*/
|
|
707
|
+
export function resolveFloorRegionPlanPoint(
|
|
708
|
+
levelId: string | null,
|
|
709
|
+
clientX: number,
|
|
710
|
+
clientY: number,
|
|
711
|
+
): FloorRegionHit | null {
|
|
712
|
+
const point = clientToPlan(clientX, clientY)
|
|
713
|
+
if (!(point && levelId)) return null
|
|
714
|
+
const plan: FloorRegionPoint = [point[0], point[1]]
|
|
715
|
+
const fixed =
|
|
716
|
+
useFloorRegionDraft.getState().draft?.room ??
|
|
717
|
+
useOpeningDraft.getState().host ??
|
|
718
|
+
useMezzanineDraft.getState().host ??
|
|
719
|
+
useTerraceDraft.getState().host
|
|
720
|
+
if (fixed) return fixed.levelId === levelId ? { room: fixed, point: plan } : null
|
|
721
|
+
const room = resolveFloorRegionRoom(levelId, plan)
|
|
722
|
+
return room ? { room, point: plan } : null
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const MARKER_GLOW_PX = 10
|
|
726
|
+
const MARKER_CORE_PX = 3
|
|
727
|
+
|
|
728
|
+
function PlanMarker({ at, color, upp }: { at: FloorRegionPoint; color: string; upp: number }) {
|
|
729
|
+
return (
|
|
730
|
+
<>
|
|
731
|
+
<circle cx={at[0]} cy={at[1]} fill={color} fillOpacity={0.25} r={MARKER_GLOW_PX * upp} />
|
|
732
|
+
<circle cx={at[0]} cy={at[1]} fill={color} fillOpacity={0.9} r={MARKER_CORE_PX * upp} />
|
|
733
|
+
</>
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function ringsPath(pieces: MultiPolygon) {
|
|
738
|
+
return pieces
|
|
739
|
+
.flatMap(({ outer, holes }) => [outer, ...holes])
|
|
740
|
+
.map((ring) => `M ${ring.map((point) => point.join(' ')).join(' L ')} Z`)
|
|
741
|
+
.join(' ')
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function FloorRegionPreview2D({ levelId }: { levelId: string | null }) {
|
|
745
|
+
const context = useFloorplanRender()
|
|
746
|
+
const draft = useFloorRegionDraft((s) => s.draft)
|
|
747
|
+
const hover = useFloorRegionDraft((s) => s.hover)
|
|
748
|
+
const view = useDraftView(draft && draft.room.levelId === levelId ? draft : null)
|
|
749
|
+
const isDark = useSceneIsDark()
|
|
750
|
+
const upp = context?.unitsPerPixel ?? 0.01
|
|
751
|
+
const outline = isDark ? DRAFT_COLOR : DRAFT_COLOR_LIGHT_PLAN
|
|
752
|
+
const tint = usePaintTint(outline)
|
|
753
|
+
const fill = view?.fill ?? tint
|
|
754
|
+
const mezzanineHost = useMezzanineDraft((s) => (s.host?.levelId === levelId ? s.host : null))
|
|
755
|
+
const openingHost = useOpeningDraft((s) => (s.host?.levelId === levelId ? s.host : null))
|
|
756
|
+
const host = openingHost ?? mezzanineHost
|
|
757
|
+
const hostArea = host && (
|
|
758
|
+
<path
|
|
759
|
+
d={ringsPath(host.clear)}
|
|
760
|
+
data-mezzanine-host
|
|
761
|
+
fill={outline}
|
|
762
|
+
fillOpacity={0.12}
|
|
763
|
+
fillRule="evenodd"
|
|
764
|
+
stroke={outline}
|
|
765
|
+
strokeLinejoin="round"
|
|
766
|
+
strokeWidth={2 * upp}
|
|
767
|
+
/>
|
|
768
|
+
)
|
|
769
|
+
if (!view) {
|
|
770
|
+
if (draft || !(hostArea || (hover && hover.levelId === levelId))) return null
|
|
771
|
+
return (
|
|
772
|
+
<g data-floor-region-preview pointerEvents="none">
|
|
773
|
+
{hostArea}
|
|
774
|
+
{hover && hover.levelId === levelId && (
|
|
775
|
+
<PlanMarker at={hover.point} color={outline} upp={upp} />
|
|
776
|
+
)}
|
|
777
|
+
</g>
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
const stroke = view.message ? INVALID_COLOR : outline
|
|
781
|
+
const path = `M ${view.outline.map((point) => point.join(' ')).join(' L ')}${view.closed ? ' Z' : ''}`
|
|
782
|
+
return (
|
|
783
|
+
<g data-floor-region-preview pointerEvents="none">
|
|
784
|
+
{hostArea}
|
|
785
|
+
{view.clip && (
|
|
786
|
+
<path d={ringsPath(view.clip.pieces)} fill={fill} fillOpacity={0.3} fillRule="evenodd" />
|
|
787
|
+
)}
|
|
788
|
+
{view.outline.length > 1 && (
|
|
789
|
+
<path
|
|
790
|
+
d={path}
|
|
791
|
+
fill="none"
|
|
792
|
+
stroke={stroke}
|
|
793
|
+
strokeLinecap="round"
|
|
794
|
+
strokeLinejoin="round"
|
|
795
|
+
strokeWidth={2 * upp}
|
|
796
|
+
/>
|
|
797
|
+
)}
|
|
798
|
+
{view.points.map((point, index) => (
|
|
799
|
+
<PlanMarker at={point} color={outline} key={index} upp={upp} />
|
|
800
|
+
))}
|
|
801
|
+
{view.cursor && <PlanMarker at={view.cursor} color={stroke} upp={upp} />}
|
|
802
|
+
{view.label && view.anchor && (
|
|
803
|
+
<g
|
|
804
|
+
transform={`translate(${view.anchor[0]} ${view.anchor[1]}) rotate(${-(context?.sceneRotationDeg ?? 0)}) scale(${upp})`}
|
|
805
|
+
>
|
|
806
|
+
<text
|
|
807
|
+
dominantBaseline="middle"
|
|
808
|
+
fill={view.message ? INVALID_COLOR : (context?.palette.measurementStroke ?? outline)}
|
|
809
|
+
fontSize={12}
|
|
810
|
+
fontWeight={600}
|
|
811
|
+
paintOrder="stroke"
|
|
812
|
+
stroke={isDark ? '#0f172a' : '#ffffff'}
|
|
813
|
+
strokeLinejoin="round"
|
|
814
|
+
strokeWidth={3}
|
|
815
|
+
textAnchor="middle"
|
|
816
|
+
y={view.message ? -16 : 0}
|
|
817
|
+
>
|
|
818
|
+
{view.label}
|
|
819
|
+
</text>
|
|
820
|
+
</g>
|
|
821
|
+
)}
|
|
822
|
+
</g>
|
|
823
|
+
)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function FloorRegionControls2D({ levelId }: { levelId: string | null }) {
|
|
827
|
+
const mode = useFloorRegionMode()
|
|
828
|
+
const context = useFloorplanRender()
|
|
829
|
+
const ref = useRef<SVGGElement>(null)
|
|
830
|
+
const upp = useRef(context?.unitsPerPixel ?? 0.01)
|
|
831
|
+
upp.current = context?.unitsPerPixel ?? 0.01
|
|
832
|
+
useFloorRegionLifetime(mode)
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
const svg = ref.current?.ownerSVGElement
|
|
835
|
+
if (!(svg && mode)) return
|
|
836
|
+
return bindFloorRegionPointer(
|
|
837
|
+
svg,
|
|
838
|
+
(event) => {
|
|
839
|
+
if (!(event.target instanceof Element) || event.target.closest('foreignObject')) return null
|
|
840
|
+
return resolveFloorRegionPlanPoint(levelId, event.clientX, event.clientY)
|
|
841
|
+
},
|
|
842
|
+
() => CLOSE_RADIUS_PX * upp.current,
|
|
843
|
+
)
|
|
844
|
+
}, [levelId, mode])
|
|
845
|
+
return <g ref={ref}>{mode && <FloorRegionPreview2D levelId={levelId} />}</g>
|
|
846
|
+
}
|