@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,324 @@
|
|
|
1
|
+
import { CameraControlsImpl } from '@react-three/drei'
|
|
2
|
+
import { Vector3 } from 'three'
|
|
3
|
+
import {
|
|
4
|
+
floorLimitAt,
|
|
5
|
+
glideFloorLimit,
|
|
6
|
+
ORBIT_FLOOR_CLEARANCE,
|
|
7
|
+
orbitCameraAboveFloor,
|
|
8
|
+
} from '../../lib/orbit-floor-clearance'
|
|
9
|
+
|
|
10
|
+
const virtualTarget = new Vector3()
|
|
11
|
+
const virtualPosition = new Vector3()
|
|
12
|
+
const groundedPosition = new Vector3()
|
|
13
|
+
const drawnTarget = new Vector3()
|
|
14
|
+
const shiftDelta = new Vector3()
|
|
15
|
+
// Below this height over the level's floor limit the surface under the camera
|
|
16
|
+
// is looked up; higher up it cannot matter.
|
|
17
|
+
const SURFACE_BAND = 3
|
|
18
|
+
// While the limit glides up over a platform's edge, the camera still never
|
|
19
|
+
// sits closer than this to the floor it is over.
|
|
20
|
+
const SURFACE_MIN_GAP = 0.05
|
|
21
|
+
|
|
22
|
+
/** The walkable surface under the camera (see `FloorSurface`). */
|
|
23
|
+
export type CameraFloorSurface = {
|
|
24
|
+
/** Changes whenever an earlier answer may no longer hold. */
|
|
25
|
+
readonly revision: number
|
|
26
|
+
topAt(x: number, z: number, levelY: number): number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Orbit controls whose own state never puts a perspective camera under the
|
|
31
|
+
* floor surface below it: an orbit past the clearance keeps the camera on it
|
|
32
|
+
* and pitches the view up (walls do not stop it: it passes through them). The
|
|
33
|
+
* push and pitch are applied to the controls' own target and angle, so every
|
|
34
|
+
* reader (`getPosition`, `getTarget`, `camera.position`, truck speed,
|
|
35
|
+
* snapshots) sees the pose that is drawn. `floorShift` is the
|
|
36
|
+
* push currently applied; the orbit underneath it is what a later rotate or
|
|
37
|
+
* zoom continues from, which is why orbiting back up returns the exact
|
|
38
|
+
* earlier pose.
|
|
39
|
+
*/
|
|
40
|
+
export class FloorAwareCameraControls extends CameraControlsImpl {
|
|
41
|
+
/**
|
|
42
|
+
* The active level's floor plus the clearance: the limit where no floor
|
|
43
|
+
* surface is found under the camera. Null turns the floor off.
|
|
44
|
+
*/
|
|
45
|
+
floorMinY: number | null = null
|
|
46
|
+
/** Walkable surface of the active level; the limit follows its top. */
|
|
47
|
+
floorSurface: CameraFloorSurface | null = null
|
|
48
|
+
// The floor limit under the camera, eased as the camera crosses floors of
|
|
49
|
+
// different heights; the last floor found on this level, kept over holes
|
|
50
|
+
// (a stair void) and through a slab's rebuild; and the last lookup, reused
|
|
51
|
+
// at rest.
|
|
52
|
+
private surfaceMinY: number | null = null
|
|
53
|
+
private surfaceTop: number | null = null
|
|
54
|
+
private surfaceLevelY: number | null = null
|
|
55
|
+
private surfaceQuery = { x: Number.NaN, z: Number.NaN, revision: -1, top: null as number | null }
|
|
56
|
+
private frame = 0
|
|
57
|
+
private frameDelta = 0
|
|
58
|
+
private glideFrame = -1
|
|
59
|
+
private readonly floorShift = new Vector3()
|
|
60
|
+
// How much steeper the drawn view is than the orbit (the look-up pitch).
|
|
61
|
+
private polarShift = 0
|
|
62
|
+
// A pose written with `setLookAt` (applied pose, saved view, snapshot
|
|
63
|
+
// replay) is shown exactly, even under the clearance; the floor holds at
|
|
64
|
+
// its height until the orbit heads back above the clearance or the floor
|
|
65
|
+
// itself changes (another level).
|
|
66
|
+
private heldMinY: number | null = null
|
|
67
|
+
private heldFloorMinY: number | null = null
|
|
68
|
+
private relativeMove = false
|
|
69
|
+
private relativeRotate = false
|
|
70
|
+
private readonly floorUpdateEvent = { type: 'update' }
|
|
71
|
+
/** The lowest the camera may sit right now: the clearance above the floor under it. */
|
|
72
|
+
get floorLimitY() {
|
|
73
|
+
return this.surfaceMinY ?? this.floorMinY
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Level navigation translates the orbit's floor, rather than its pushed target.
|
|
77
|
+
getOrbitTarget(out: Vector3) {
|
|
78
|
+
return this.getTarget(out).sub(this.floorShift)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The orbit's own polar angle, under the floor's look-up pitch. */
|
|
82
|
+
get orbitPolarAngle() {
|
|
83
|
+
return this._spherical.phi - this.polarShift
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Back to the orbit under the floor's push and pitch. Nothing is drawn in
|
|
87
|
+
// between: the next update re-derives the same drawn pose from it.
|
|
88
|
+
private dropFloorShift() {
|
|
89
|
+
this._target.sub(this.floorShift)
|
|
90
|
+
this._targetEnd.sub(this.floorShift)
|
|
91
|
+
this._spherical.phi -= this.polarShift
|
|
92
|
+
this._sphericalEnd.phi -= this.polarShift
|
|
93
|
+
this.floorShift.set(0, 0, 0)
|
|
94
|
+
this.polarShift = 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override moveTo(x: number, y: number, z: number, enableTransition = false) {
|
|
98
|
+
// Absolute destinations replace the orbit; trucks keep its remembered push.
|
|
99
|
+
if (!this.relativeMove) {
|
|
100
|
+
// Travel from the orbit under the push, not from the pushed target: the
|
|
101
|
+
// push is recomputed on the way, so nothing jumps, and a low camera
|
|
102
|
+
// does not start the trip lifted above its orbit.
|
|
103
|
+
this.dropFloorShift()
|
|
104
|
+
this.heldMinY = null
|
|
105
|
+
}
|
|
106
|
+
return super.moveTo(x, y, z, enableTransition)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override fitToBox(...args: Parameters<CameraControlsImpl['fitToBox']>) {
|
|
110
|
+
// Framing starts from the orbit, not from the floor's look-up pitch.
|
|
111
|
+
this.dropFloorShift()
|
|
112
|
+
return super.fitToBox(...args)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override rotate(azimuthAngle: number, polarAngle: number, enableTransition = false) {
|
|
116
|
+
this.relativeRotate = true
|
|
117
|
+
try {
|
|
118
|
+
return super.rotate(azimuthAngle, polarAngle, enableTransition)
|
|
119
|
+
} finally {
|
|
120
|
+
this.relativeRotate = false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override rotateTo(azimuthAngle: number, polarAngle: number, enableTransition = false) {
|
|
125
|
+
// An absolute angle is the orbit's; drags (`rotate`) keep the look-up pitch.
|
|
126
|
+
if (!this.relativeRotate) this.dropFloorShift()
|
|
127
|
+
return super.rotateTo(azimuthAngle, polarAngle, enableTransition)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override truck(x: number, y: number, enableTransition = false) {
|
|
131
|
+
this.relativeMove = true
|
|
132
|
+
try {
|
|
133
|
+
return super.truck(x, y, enableTransition)
|
|
134
|
+
} finally {
|
|
135
|
+
this.relativeMove = false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override forward(distance: number, enableTransition = false) {
|
|
140
|
+
this.relativeMove = true
|
|
141
|
+
try {
|
|
142
|
+
return super.forward(distance, enableTransition)
|
|
143
|
+
} finally {
|
|
144
|
+
this.relativeMove = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override elevate(height: number, enableTransition = false) {
|
|
149
|
+
this.relativeMove = true
|
|
150
|
+
try {
|
|
151
|
+
return super.elevate(height, enableTransition)
|
|
152
|
+
} finally {
|
|
153
|
+
this.relativeMove = false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override setLookAt(
|
|
158
|
+
positionX: number,
|
|
159
|
+
positionY: number,
|
|
160
|
+
positionZ: number,
|
|
161
|
+
targetX: number,
|
|
162
|
+
targetY: number,
|
|
163
|
+
targetZ: number,
|
|
164
|
+
enableTransition = false,
|
|
165
|
+
) {
|
|
166
|
+
const done = super.setLookAt(
|
|
167
|
+
positionX,
|
|
168
|
+
positionY,
|
|
169
|
+
positionZ,
|
|
170
|
+
targetX,
|
|
171
|
+
targetY,
|
|
172
|
+
targetZ,
|
|
173
|
+
enableTransition,
|
|
174
|
+
)
|
|
175
|
+
this.floorShift.set(0, 0, 0)
|
|
176
|
+
this.polarShift = 0
|
|
177
|
+
this.heldMinY = positionY
|
|
178
|
+
this.heldFloorMinY = this.floorMinY
|
|
179
|
+
return done
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
override update(delta: number): boolean {
|
|
183
|
+
// The base constructor runs one update before this class's fields exist.
|
|
184
|
+
if (this.floorShift !== undefined) {
|
|
185
|
+
this.frame += 1
|
|
186
|
+
this.frameDelta = delta
|
|
187
|
+
}
|
|
188
|
+
const updated = super.update(delta)
|
|
189
|
+
// The base constructor runs one update before this class's fields exist.
|
|
190
|
+
if (this.floorShift !== undefined && this.keepAboveFloor() && !updated) {
|
|
191
|
+
super.dispatchEvent(this.floorUpdateEvent)
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
return updated
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
override dispatchEvent(event: Parameters<CameraControlsImpl['dispatchEvent']>[0]) {
|
|
198
|
+
// Base update listeners run before update returns; publish the drawn pose.
|
|
199
|
+
if (event.type === 'update' && this.floorShift !== undefined) this.keepAboveFloor()
|
|
200
|
+
super.dispatchEvent(event)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private keepAboveFloor() {
|
|
204
|
+
const floorMinY = 'isOrthographicCamera' in this._camera ? null : this.floorMinY
|
|
205
|
+
if (floorMinY === null) {
|
|
206
|
+
// Floor off: what is drawn becomes the orbit, so turning it back on
|
|
207
|
+
// never jumps.
|
|
208
|
+
this.floorShift.set(0, 0, 0)
|
|
209
|
+
this.polarShift = 0
|
|
210
|
+
this.heldMinY = null
|
|
211
|
+
this.forgetFloorSurface()
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const levelY = floorMinY - ORBIT_FLOOR_CLEARANCE
|
|
216
|
+
if (levelY !== this.surfaceLevelY) {
|
|
217
|
+
this.forgetFloorSurface()
|
|
218
|
+
this.surfaceLevelY = levelY
|
|
219
|
+
}
|
|
220
|
+
const knownLimitY = this.surfaceMinY ?? floorMinY
|
|
221
|
+
const endCameraY =
|
|
222
|
+
this._targetEnd.y -
|
|
223
|
+
this.floorShift.y +
|
|
224
|
+
this._sphericalEnd.radius * Math.cos(this._sphericalEnd.phi - this.polarShift)
|
|
225
|
+
if (this.heldFloorMinY !== floorMinY || endCameraY >= knownLimitY) {
|
|
226
|
+
this.heldMinY = null
|
|
227
|
+
this.heldFloorMinY = floorMinY
|
|
228
|
+
}
|
|
229
|
+
// The orbit never aims below the level's floor: the pivot's destination is
|
|
230
|
+
// lifted, so a transition toward it glides rather than jumps.
|
|
231
|
+
const pivotEndY = this._targetEnd.y - this.floorShift.y
|
|
232
|
+
if (this.heldMinY === null && pivotEndY < levelY) {
|
|
233
|
+
this._targetEnd.y += levelY - pivotEndY
|
|
234
|
+
this._needsUpdate = true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { radius, theta } = this._spherical
|
|
238
|
+
const orbitPolar = this._spherical.phi - this.polarShift
|
|
239
|
+
virtualTarget.copy(this._target).sub(this.floorShift)
|
|
240
|
+
virtualPosition.setFromSphericalCoords(radius, orbitPolar, theta).add(virtualTarget)
|
|
241
|
+
// Where the camera is headed at the limit known so far; then the floor
|
|
242
|
+
// under that spot, in the same pass, so no motion skips the lookup.
|
|
243
|
+
let minY = this.heldLimit(knownLimitY)
|
|
244
|
+
let drawnPolar = orbitCameraAboveFloor(groundedPosition, virtualPosition, virtualTarget, minY)
|
|
245
|
+
const limitY = this.resolveFloorLimit(groundedPosition, levelY)
|
|
246
|
+
if (limitY !== knownLimitY) {
|
|
247
|
+
minY = this.heldLimit(limitY)
|
|
248
|
+
drawnPolar = orbitCameraAboveFloor(groundedPosition, virtualPosition, virtualTarget, minY)
|
|
249
|
+
}
|
|
250
|
+
// The drawn target sits the orbit's distance out along the drawn view.
|
|
251
|
+
drawnTarget.setFromSphericalCoords(radius, drawnPolar, theta).negate().add(groundedPosition)
|
|
252
|
+
shiftDelta.subVectors(drawnTarget, this._target)
|
|
253
|
+
const polarDelta = drawnPolar - orbitPolar - this.polarShift
|
|
254
|
+
if (shiftDelta.lengthSq() < 1e-12 && Math.abs(polarDelta) < 1e-9) return false
|
|
255
|
+
|
|
256
|
+
this._target.add(shiftDelta)
|
|
257
|
+
this._targetEnd.add(shiftDelta)
|
|
258
|
+
this._spherical.phi += polarDelta
|
|
259
|
+
this._sphericalEnd.phi += polarDelta
|
|
260
|
+
this.floorShift.add(shiftDelta)
|
|
261
|
+
this.polarShift += polarDelta
|
|
262
|
+
this._camera.position.copy(groundedPosition)
|
|
263
|
+
this._camera.lookAt(this._target)
|
|
264
|
+
this._needsUpdate = true
|
|
265
|
+
return true
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The user has taken over: a written pose under the clearance stops being
|
|
270
|
+
* held, and the limit eases up from where the camera is rather than popping.
|
|
271
|
+
*/
|
|
272
|
+
releaseWrittenPose() {
|
|
273
|
+
if (this.heldMinY === null) return
|
|
274
|
+
this.heldMinY = null
|
|
275
|
+
const y = this._camera.position.y
|
|
276
|
+
this.surfaceMinY = Math.min(this.surfaceMinY ?? y, y)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A written pose may sit under the clearance, but not under the floor itself.
|
|
280
|
+
private heldLimit(limitY: number) {
|
|
281
|
+
if (this.heldMinY === null) return limitY
|
|
282
|
+
return Math.min(limitY, Math.max(this.heldMinY, limitY - ORBIT_FLOOR_CLEARANCE))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private forgetFloorSurface() {
|
|
286
|
+
this.surfaceMinY = null
|
|
287
|
+
this.surfaceTop = null
|
|
288
|
+
this.surfaceQuery.revision = -1
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The floor limit at `position`: the clearance above the floor surface under
|
|
293
|
+
* it, eased once per frame as the floor height changes. Over a hole, or
|
|
294
|
+
* while a slab is rebuilt, the last floor found on this level holds; with
|
|
295
|
+
* none found yet, the level's own floor does. Far above the floor nothing
|
|
296
|
+
* is cast and the floor is forgotten.
|
|
297
|
+
*/
|
|
298
|
+
private resolveFloorLimit(position: Vector3, levelY: number) {
|
|
299
|
+
const surface = this.floorSurface
|
|
300
|
+
if (!surface || position.y > levelY + ORBIT_FLOOR_CLEARANCE + SURFACE_BAND) {
|
|
301
|
+
this.forgetFloorSurface()
|
|
302
|
+
return levelY + ORBIT_FLOOR_CLEARANCE
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const query = this.surfaceQuery
|
|
306
|
+
const revision = surface.revision
|
|
307
|
+
if (position.x !== query.x || position.z !== query.z || revision !== query.revision) {
|
|
308
|
+
query.x = position.x
|
|
309
|
+
query.z = position.z
|
|
310
|
+
query.revision = revision
|
|
311
|
+
query.top = surface.topAt(position.x, position.z, levelY)
|
|
312
|
+
if (query.top !== null) this.surfaceTop = query.top
|
|
313
|
+
}
|
|
314
|
+
const floorTop = this.surfaceTop ?? levelY
|
|
315
|
+
const step = this.glideFrame === this.frame ? 0 : this.frameDelta
|
|
316
|
+
this.glideFrame = this.frame
|
|
317
|
+
const limit = Math.max(
|
|
318
|
+
glideFloorLimit(this.surfaceMinY, floorLimitAt(floorTop, levelY), step),
|
|
319
|
+
floorTop + SURFACE_MIN_GAP,
|
|
320
|
+
)
|
|
321
|
+
this.surfaceMinY = limit
|
|
322
|
+
return limit
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type FloorOpeningNode,
|
|
7
|
+
resolveCeilingHeight,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import { type ThreeEvent, useFrame } from '@react-three/fiber'
|
|
13
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
14
|
+
import {
|
|
15
|
+
BufferGeometry,
|
|
16
|
+
DoubleSide,
|
|
17
|
+
Float32BufferAttribute,
|
|
18
|
+
type Group,
|
|
19
|
+
ShapeUtils,
|
|
20
|
+
Vector2,
|
|
21
|
+
} from 'three'
|
|
22
|
+
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
23
|
+
import { useCeilingEditSession } from '../../lib/ceiling-edit-session'
|
|
24
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
25
|
+
import { openingRoom, roomSurfaceOpenings, useFloorEditSession } from '../../lib/floor-edit-session'
|
|
26
|
+
import { useOpeningDraft } from '../../lib/floor-opening-draft'
|
|
27
|
+
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
28
|
+
import { PolygonEditor } from '../tools/shared/polygon-editor'
|
|
29
|
+
|
|
30
|
+
// The openings of the room being edited ("Edit floor", "Edit ceiling", or a
|
|
31
|
+
// "Cut opening" in progress), outlined where they cut: amber, a light fill to
|
|
32
|
+
// click them by, and the selected one's corner handles. Openings are intent
|
|
33
|
+
// nodes with no mesh of their own — the surfaces they cut show the void.
|
|
34
|
+
|
|
35
|
+
const COLOR = '#f59e0b'
|
|
36
|
+
const LIFT = 0.015
|
|
37
|
+
|
|
38
|
+
type Nodes = Readonly<Record<string, AnyNode>>
|
|
39
|
+
|
|
40
|
+
/** Where an opening is drawn: its room's floor, or the ceiling it was drawn on. */
|
|
41
|
+
export function openingElevation(nodes: Nodes, opening: FloorOpeningNode): number {
|
|
42
|
+
if (opening.hostZoneId) return roomFloorElevation(nodes, opening.hostZoneId)
|
|
43
|
+
const zoneId = openingRoom(nodes, opening)
|
|
44
|
+
if (opening.drawnOn === 'ceiling') {
|
|
45
|
+
const ceiling = Object.values(nodes).find(
|
|
46
|
+
(node) =>
|
|
47
|
+
node.type === 'ceiling' && node.parentId === opening.parentId && node.zoneId === zoneId,
|
|
48
|
+
)
|
|
49
|
+
return ceiling?.type === 'ceiling'
|
|
50
|
+
? resolveCeilingHeight(ceiling, nodes as Record<AnyNodeId, AnyNode>)
|
|
51
|
+
: 2.5
|
|
52
|
+
}
|
|
53
|
+
return zoneId ? roomFloorElevation(nodes, zoneId) : 0.05
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function useShownOpenings(): { levelId: string | null; openings: FloorOpeningNode[] } {
|
|
57
|
+
const nodes = useScene((s) => s.nodes)
|
|
58
|
+
const floorSession = useFloorEditSession((s) => s.session)
|
|
59
|
+
const ceilingSession = useCeilingEditSession((s) => s.session)
|
|
60
|
+
const draftHost = useOpeningDraft((s) => s.host)
|
|
61
|
+
const selectedId = useViewer((s) =>
|
|
62
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
|
|
63
|
+
)
|
|
64
|
+
return useMemo(() => {
|
|
65
|
+
const shown = new Map<string, FloorOpeningNode>()
|
|
66
|
+
const add = (list: FloorOpeningNode[]) => {
|
|
67
|
+
for (const opening of list) shown.set(opening.id, opening)
|
|
68
|
+
}
|
|
69
|
+
let levelId: string | null = null
|
|
70
|
+
if (floorSession) {
|
|
71
|
+
add(roomSurfaceOpenings(nodes, floorSession.zoneId, 'floor'))
|
|
72
|
+
levelId = floorSession.levelId
|
|
73
|
+
}
|
|
74
|
+
if (ceilingSession?.zoneId) {
|
|
75
|
+
add(roomSurfaceOpenings(nodes, ceilingSession.zoneId, 'ceiling'))
|
|
76
|
+
levelId = ceilingSession.levelId
|
|
77
|
+
}
|
|
78
|
+
if (draftHost) {
|
|
79
|
+
add(roomSurfaceOpenings(nodes, draftHost.zoneId, draftHost.drawnOn))
|
|
80
|
+
levelId = draftHost.levelId
|
|
81
|
+
}
|
|
82
|
+
const selected = selectedId ? nodes[selectedId as AnyNodeId] : undefined
|
|
83
|
+
if (selected?.type === 'floor-opening') {
|
|
84
|
+
shown.set(selected.id, selected)
|
|
85
|
+
levelId = selected.parentId
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
levelId,
|
|
89
|
+
openings: [...shown.values()].filter((opening) => opening.parentId === levelId),
|
|
90
|
+
}
|
|
91
|
+
}, [nodes, floorSession, ceilingSession, draftHost, selectedId])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function outlineGeometry(polygon: FloorOpeningNode['polygon'], y: number) {
|
|
95
|
+
const positions: number[] = []
|
|
96
|
+
polygon.forEach((a, i) => {
|
|
97
|
+
const b = polygon[(i + 1) % polygon.length]!
|
|
98
|
+
positions.push(a[0], y, a[1], b[0], y, b[1])
|
|
99
|
+
})
|
|
100
|
+
const geometry = new BufferGeometry()
|
|
101
|
+
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
|
|
102
|
+
return geometry
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fillGeometry(polygon: FloorOpeningNode['polygon'], y: number) {
|
|
106
|
+
const contour = polygon.map(([x, z]) => new Vector2(x, z))
|
|
107
|
+
const positions: number[] = []
|
|
108
|
+
for (const triangle of ShapeUtils.triangulateShape(contour, []))
|
|
109
|
+
for (const index of triangle) positions.push(contour[index]!.x, y, contour[index]!.y)
|
|
110
|
+
const geometry = new BufferGeometry()
|
|
111
|
+
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
|
|
112
|
+
return geometry
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function OpeningOutline({
|
|
116
|
+
opening,
|
|
117
|
+
elevation,
|
|
118
|
+
selected,
|
|
119
|
+
materials,
|
|
120
|
+
}: {
|
|
121
|
+
opening: FloorOpeningNode
|
|
122
|
+
elevation: number
|
|
123
|
+
selected: boolean
|
|
124
|
+
materials: {
|
|
125
|
+
line: LineBasicNodeMaterial
|
|
126
|
+
fill: MeshBasicNodeMaterial
|
|
127
|
+
fillSelected: MeshBasicNodeMaterial
|
|
128
|
+
}
|
|
129
|
+
}) {
|
|
130
|
+
const geometry = useMemo(
|
|
131
|
+
() => ({
|
|
132
|
+
line: outlineGeometry(opening.polygon, elevation + LIFT),
|
|
133
|
+
fill: fillGeometry(opening.polygon, elevation + LIFT),
|
|
134
|
+
}),
|
|
135
|
+
[opening.polygon, elevation],
|
|
136
|
+
)
|
|
137
|
+
useEffect(
|
|
138
|
+
() => () => {
|
|
139
|
+
geometry.line.dispose()
|
|
140
|
+
geometry.fill.dispose()
|
|
141
|
+
},
|
|
142
|
+
[geometry],
|
|
143
|
+
)
|
|
144
|
+
const select = (event: ThreeEvent<MouseEvent>) => {
|
|
145
|
+
event.stopPropagation()
|
|
146
|
+
useViewer.getState().setSelection({ selectedIds: [opening.id] })
|
|
147
|
+
}
|
|
148
|
+
return (
|
|
149
|
+
<group data-floor-opening={opening.id}>
|
|
150
|
+
<lineSegments
|
|
151
|
+
geometry={geometry.line}
|
|
152
|
+
layers={EDITOR_LAYER}
|
|
153
|
+
material={materials.line}
|
|
154
|
+
raycast={() => {}}
|
|
155
|
+
renderOrder={1001}
|
|
156
|
+
/>
|
|
157
|
+
<mesh
|
|
158
|
+
geometry={geometry.fill}
|
|
159
|
+
layers={EDITOR_LAYER}
|
|
160
|
+
material={selected ? materials.fillSelected : materials.fill}
|
|
161
|
+
onClick={select}
|
|
162
|
+
renderOrder={1000}
|
|
163
|
+
/>
|
|
164
|
+
</group>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function FloorOpeningsOverlay3D() {
|
|
169
|
+
const { levelId, openings } = useShownOpenings()
|
|
170
|
+
const nodes = useScene((s) => s.nodes)
|
|
171
|
+
const readOnly = useScene((s) => s.readOnly)
|
|
172
|
+
const selectedId = useViewer((s) =>
|
|
173
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
|
|
174
|
+
)
|
|
175
|
+
const root = useRef<Group>(null)
|
|
176
|
+
const materials = useMemo(
|
|
177
|
+
() => ({
|
|
178
|
+
line: new LineBasicNodeMaterial({ color: COLOR, depthTest: false, depthWrite: false }),
|
|
179
|
+
fill: new MeshBasicNodeMaterial({
|
|
180
|
+
color: COLOR,
|
|
181
|
+
depthTest: false,
|
|
182
|
+
depthWrite: false,
|
|
183
|
+
opacity: 0.12,
|
|
184
|
+
side: DoubleSide,
|
|
185
|
+
transparent: true,
|
|
186
|
+
}),
|
|
187
|
+
fillSelected: new MeshBasicNodeMaterial({
|
|
188
|
+
color: COLOR,
|
|
189
|
+
depthTest: false,
|
|
190
|
+
depthWrite: false,
|
|
191
|
+
opacity: 0.28,
|
|
192
|
+
side: DoubleSide,
|
|
193
|
+
transparent: true,
|
|
194
|
+
}),
|
|
195
|
+
}),
|
|
196
|
+
[],
|
|
197
|
+
)
|
|
198
|
+
useEffect(
|
|
199
|
+
() => () => {
|
|
200
|
+
materials.line.dispose()
|
|
201
|
+
materials.fill.dispose()
|
|
202
|
+
materials.fillSelected.dispose()
|
|
203
|
+
},
|
|
204
|
+
[materials],
|
|
205
|
+
)
|
|
206
|
+
useFrame(() => {
|
|
207
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
208
|
+
if (!root.current) return
|
|
209
|
+
root.current.visible = !!level
|
|
210
|
+
if (level) {
|
|
211
|
+
level.updateWorldMatrix(true, false)
|
|
212
|
+
root.current.matrix.copy(level.matrixWorld)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
if (!levelId || openings.length === 0) return null
|
|
216
|
+
const selected = openings.find((opening) => opening.id === selectedId) ?? null
|
|
217
|
+
return (
|
|
218
|
+
<>
|
|
219
|
+
<group matrixAutoUpdate={false} ref={root}>
|
|
220
|
+
{openings.map((opening) => (
|
|
221
|
+
<OpeningOutline
|
|
222
|
+
elevation={openingElevation(nodes, opening)}
|
|
223
|
+
key={opening.id}
|
|
224
|
+
materials={materials}
|
|
225
|
+
opening={opening}
|
|
226
|
+
selected={opening.id === selectedId}
|
|
227
|
+
/>
|
|
228
|
+
))}
|
|
229
|
+
</group>
|
|
230
|
+
{selected && !readOnly && selected.source !== 'stair' && selected.source !== 'elevator' && (
|
|
231
|
+
<PolygonEditor
|
|
232
|
+
allowEdgeMove
|
|
233
|
+
color={COLOR}
|
|
234
|
+
levelId={levelId}
|
|
235
|
+
minVertices={3}
|
|
236
|
+
onPolygonChange={(polygon) => useScene.getState().updateNode(selected.id, { polygon })}
|
|
237
|
+
polygon={selected.polygon}
|
|
238
|
+
surfaceHeight={openingElevation(nodes, selected)}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
243
|
+
}
|