@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,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
area,
|
|
5
|
+
createZone,
|
|
6
|
+
generateId,
|
|
7
|
+
type MultiPolygon,
|
|
8
|
+
type StructurePlan,
|
|
9
|
+
type ToolHint,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { create } from 'zustand'
|
|
14
|
+
import useEditor from '../store/use-editor'
|
|
15
|
+
import { CROSSING_MESSAGE, floorRegionSelfIntersects } from './floor-region-geometry'
|
|
16
|
+
import {
|
|
17
|
+
cancelFloorRegion,
|
|
18
|
+
type FloorDraftCommit,
|
|
19
|
+
type FloorDraftPurpose,
|
|
20
|
+
type FloorRegionRoom,
|
|
21
|
+
useFloorRegionDraft,
|
|
22
|
+
} from './floor-region-session'
|
|
23
|
+
import { type FloorRegionPoint, floorRegionSnapTargets } from './floor-region-snap'
|
|
24
|
+
import { beginGesture, type GestureHandle } from './gesture-lifecycle'
|
|
25
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
26
|
+
import { sfxEmitter } from './sfx-bus'
|
|
27
|
+
|
|
28
|
+
// "Terrace" (Build panel → Outdoor): draws an outdoor room on the level's
|
|
29
|
+
// ground, rectangle or polygon, snapped to the rooms already there and the
|
|
30
|
+
// grid. The terrace is a room like any other — bounded by separators (and by
|
|
31
|
+
// the walls it shares), no walls of its own and no ceiling — so it gets
|
|
32
|
+
// everything rooms have: finish paint, height arrows, Divide, Move, Delete.
|
|
33
|
+
// The tool is one lifecycle gesture from `startTerraceDraft` until cancelled;
|
|
34
|
+
// each outline commits as one undo step and the tool stays armed, like the
|
|
35
|
+
// room tools.
|
|
36
|
+
|
|
37
|
+
export const TERRACE_DRAFT_HANDLE = 'terrace-draft'
|
|
38
|
+
export const TERRACE_NAME = 'Terrace'
|
|
39
|
+
export const TERRACE_TOO_SMALL_MESSAGE = 'Too small for a terrace.'
|
|
40
|
+
export const TERRACE_FAILED_MESSAGE = "Can't place a terrace here."
|
|
41
|
+
|
|
42
|
+
export type TerraceShape = 'rectangle' | 'polygon'
|
|
43
|
+
|
|
44
|
+
export const useTerraceDraft = create<{
|
|
45
|
+
host: FloorRegionRoom | null
|
|
46
|
+
shape: TerraceShape
|
|
47
|
+
}>(() => ({ host: null, shape: 'rectangle' }))
|
|
48
|
+
|
|
49
|
+
/** How far the terrace tool reaches from the origin when the site has no outline. */
|
|
50
|
+
const OPEN_GROUND = 500
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Why a terrace outline cannot be built, or its plan: an outdoor room of the
|
|
54
|
+
* outline, closed with separators where no wall runs, without a ceiling.
|
|
55
|
+
* Core refuses an outline over a room (the same rule MCP's outdoor
|
|
56
|
+
* `create_room` follows) and names the room.
|
|
57
|
+
*/
|
|
58
|
+
export function planTerrace(
|
|
59
|
+
nodes: Record<string, AnyNode>,
|
|
60
|
+
levelId: string,
|
|
61
|
+
polygon: readonly FloorRegionPoint[],
|
|
62
|
+
): { plan: StructurePlan & { zoneId: string } } | { message: string } {
|
|
63
|
+
const outline = polygon.map(([x, z]): [number, number] => [x, z])
|
|
64
|
+
if (floorRegionSelfIntersects(outline)) return { message: CROSSING_MESSAGE }
|
|
65
|
+
if (area([{ outer: outline, holes: [] }]) < 1) return { message: TERRACE_TOO_SMALL_MESSAGE }
|
|
66
|
+
try {
|
|
67
|
+
const plan = createZone(nodes, {
|
|
68
|
+
levelId,
|
|
69
|
+
polygon: outline,
|
|
70
|
+
name: TERRACE_NAME,
|
|
71
|
+
enclose: false,
|
|
72
|
+
intent: { hasCeiling: false },
|
|
73
|
+
mintId: generateId,
|
|
74
|
+
})
|
|
75
|
+
const conflict = plan.conflicts?.[0]
|
|
76
|
+
return conflict ? { message: conflict.message || TERRACE_FAILED_MESSAGE } : { plan }
|
|
77
|
+
} catch {
|
|
78
|
+
return { message: TERRACE_FAILED_MESSAGE }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let tool: GestureHandle | null = null
|
|
83
|
+
|
|
84
|
+
function reset() {
|
|
85
|
+
if (useTerraceDraft.getState().host) useTerraceDraft.setState({ host: null })
|
|
86
|
+
const { draft } = useFloorRegionDraft.getState()
|
|
87
|
+
if (draft?.room.purpose === TERRACE) useFloorRegionDraft.setState({ draft: null, hover: null })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function commitTerrace(
|
|
91
|
+
host: FloorRegionRoom,
|
|
92
|
+
polygon: readonly FloorRegionPoint[],
|
|
93
|
+
): FloorDraftCommit {
|
|
94
|
+
const result = planTerrace(useScene.getState().nodes, host.levelId, polygon)
|
|
95
|
+
if ('message' in result) return { ok: false, message: result.message }
|
|
96
|
+
applyRoomPlan(result.plan)
|
|
97
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
98
|
+
// The rooms (and this terrace) now snap the next outline.
|
|
99
|
+
useTerraceDraft.setState({ host: describeTerraceHost(useScene.getState().nodes, host.levelId) })
|
|
100
|
+
return { ok: true }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function beginTool(host: FloorRegionRoom) {
|
|
104
|
+
const handle = beginGesture({
|
|
105
|
+
kind: 'terrace-draft',
|
|
106
|
+
scope: { kind: 'handle-drag', nodeId: host.levelId, handle: TERRACE_DRAFT_HANDLE },
|
|
107
|
+
stale: () =>
|
|
108
|
+
useScene.getState().readOnly || !useScene.getState().nodes[host.levelId as AnyNodeId],
|
|
109
|
+
onCancel: () => {
|
|
110
|
+
if (tool === handle) tool = null
|
|
111
|
+
reset()
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
tool = handle
|
|
115
|
+
useTerraceDraft.setState({ host })
|
|
116
|
+
return handle
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const TERRACE: FloorDraftPurpose = {
|
|
120
|
+
persistent: true,
|
|
121
|
+
own: (room) => (tool?.active ? tool : beginTool(room)),
|
|
122
|
+
commit: (room, polygon) => {
|
|
123
|
+
const current = useTerraceDraft.getState().host ?? room
|
|
124
|
+
return commitTerrace(current, polygon)
|
|
125
|
+
},
|
|
126
|
+
fill: '#818cf8',
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The level's ground as a draft room: the site (or open ground), snapping to the rooms on it. */
|
|
130
|
+
export function describeTerraceHost(
|
|
131
|
+
nodes: Record<string, AnyNode>,
|
|
132
|
+
levelId: string,
|
|
133
|
+
): FloorRegionRoom | null {
|
|
134
|
+
const level = nodes[levelId]
|
|
135
|
+
if (level?.type !== 'level') return null
|
|
136
|
+
const site = Object.values(nodes).find((node) => node.type === 'site')
|
|
137
|
+
const outline =
|
|
138
|
+
site?.type === 'site' && site.polygon?.points?.length && site.polygon.points.length >= 3
|
|
139
|
+
? site.polygon.points.map(([x, z]): [number, number] => [x, z])
|
|
140
|
+
: ([
|
|
141
|
+
[-OPEN_GROUND, -OPEN_GROUND],
|
|
142
|
+
[OPEN_GROUND, -OPEN_GROUND],
|
|
143
|
+
[OPEN_GROUND, OPEN_GROUND],
|
|
144
|
+
[-OPEN_GROUND, OPEN_GROUND],
|
|
145
|
+
] as [number, number][])
|
|
146
|
+
const clear: MultiPolygon = [{ outer: outline, holes: [] }]
|
|
147
|
+
const rooms = Object.values(nodes).flatMap((node) =>
|
|
148
|
+
node.type === 'zone' && node.parentId === levelId && node.spaceRole === 'room'
|
|
149
|
+
? [node.polygon]
|
|
150
|
+
: [],
|
|
151
|
+
)
|
|
152
|
+
return {
|
|
153
|
+
zoneId: '',
|
|
154
|
+
levelId,
|
|
155
|
+
clear,
|
|
156
|
+
elevation: 0,
|
|
157
|
+
angle: 0,
|
|
158
|
+
targets: floorRegionSnapTargets([], rooms),
|
|
159
|
+
purpose: TERRACE,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Arms the terrace tool on the current level (select mode, so nothing else draws). */
|
|
164
|
+
export function startTerraceDraft(levelId = useViewer.getState().selection.levelId): boolean {
|
|
165
|
+
if (!levelId || useScene.getState().readOnly) return false
|
|
166
|
+
const host = describeTerraceHost(useScene.getState().nodes, levelId)
|
|
167
|
+
if (!host) return false
|
|
168
|
+
cancelTerraceDraft()
|
|
169
|
+
cancelFloorRegion()
|
|
170
|
+
const editor = useEditor.getState()
|
|
171
|
+
if (editor.phase === 'site') editor.setPhase('building')
|
|
172
|
+
if (editor.mode !== 'select') editor.setMode('select')
|
|
173
|
+
if (editor.room) editor.clearRoom()
|
|
174
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
175
|
+
beginTool(host)
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function cancelTerraceDraft() {
|
|
180
|
+
const current = tool
|
|
181
|
+
tool = null
|
|
182
|
+
if (current?.active) current.cancel()
|
|
183
|
+
else reset()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function terraceDraftActive() {
|
|
187
|
+
return !!useTerraceDraft.getState().host
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function setTerraceShape(shape: TerraceShape) {
|
|
191
|
+
if (useTerraceDraft.getState().shape === shape) return
|
|
192
|
+
const { draft } = useFloorRegionDraft.getState()
|
|
193
|
+
if (draft?.room.purpose === TERRACE) useFloorRegionDraft.setState({ draft: null })
|
|
194
|
+
useTerraceDraft.setState({ shape })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function toggleTerraceShape() {
|
|
198
|
+
setTerraceShape(useTerraceDraft.getState().shape === 'rectangle' ? 'polygon' : 'rectangle')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The HUD's shape chip (T). */
|
|
202
|
+
export const TERRACE_SHAPE_HINT: ToolHint = {
|
|
203
|
+
key: 'T',
|
|
204
|
+
label: 'Shape',
|
|
205
|
+
chip: {
|
|
206
|
+
subscribe: (onChange) => useTerraceDraft.subscribe(onChange),
|
|
207
|
+
value: () => useTerraceDraft.getState().shape,
|
|
208
|
+
cycle: toggleTerraceShape,
|
|
209
|
+
labels: { rectangle: 'Shape: Rectangle', polygon: 'Shape: Polygon' },
|
|
210
|
+
icons: { rectangle: 'lucide:square', polygon: 'lucide:pentagon' },
|
|
211
|
+
tooltip: 'Outline shape — click or press T to switch',
|
|
212
|
+
},
|
|
213
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { wallCursorIcon } from './tool-cursor-icon'
|
|
3
|
+
import { getWallDrawVariantInfo, WALL_DRAW_VARIANTS } from './wall-draw-variant'
|
|
4
|
+
|
|
5
|
+
test('the wall cursor bubble shows the Rooms tile icon of the variant in hand', () => {
|
|
6
|
+
for (const variant of WALL_DRAW_VARIANTS) {
|
|
7
|
+
expect(wallCursorIcon(variant.mode)).toEqual({
|
|
8
|
+
label: getWallDrawVariantInfo(variant.id).title,
|
|
9
|
+
iconSrc: variant.iconSrc,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
expect(
|
|
13
|
+
new Set(WALL_DRAW_VARIANTS.map((variant) => wallCursorIcon(variant.mode).iconSrc)).size,
|
|
14
|
+
).toBe(3)
|
|
15
|
+
expect(getWallDrawVariantInfo('walls').iconSrc).toBe('/icons/wall.webp')
|
|
16
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ContinuationMode } from './continuation'
|
|
2
|
+
import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The cursor bubble's icon for the wall tool: the Rooms variant in hand
|
|
6
|
+
* (Rectangle / Polygon / Walls), the same art as the Build panel tile.
|
|
7
|
+
*/
|
|
8
|
+
export function wallCursorIcon(mode: ContinuationMode): { label: string; iconSrc: string } {
|
|
9
|
+
const variant = getWallDrawVariantInfo(wallDrawVariantOf(mode))
|
|
10
|
+
return { label: variant.title, iconSrc: variant.iconSrc }
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Keys typed into a field (the room-name combobox, any input, an open dialog)
|
|
2
|
+
// belong to that field, never to a canvas gesture.
|
|
3
|
+
const TYPING_TARGET =
|
|
4
|
+
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="combobox"], [role="textbox"], [role="dialog"], [role="alertdialog"]'
|
|
5
|
+
|
|
6
|
+
export function isTypingTarget(target: EventTarget | null) {
|
|
7
|
+
const element = target as { closest?: (selector: string) => unknown; isContentEditable?: boolean }
|
|
8
|
+
return !!element && (element.isContentEditable === true || !!element.closest?.(TYPING_TARGET))
|
|
9
|
+
}
|
package/src/lib/units.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
type BuildingNode,
|
|
5
5
|
type LevelNode,
|
|
6
6
|
pointInPolygon2D,
|
|
7
|
-
|
|
7
|
+
runAsSingleSceneHistoryStep,
|
|
8
|
+
sceneRegistry,
|
|
8
9
|
snapWorldXZToBuildingLocal,
|
|
9
10
|
UnitNode,
|
|
10
11
|
useScene,
|
|
@@ -12,14 +13,13 @@ import {
|
|
|
12
13
|
} from '@pascal-app/core'
|
|
13
14
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
15
|
import { useEffect } from 'react'
|
|
15
|
-
import useEditor
|
|
16
|
+
import useEditor from '../store/use-editor'
|
|
16
17
|
import { getActiveBuildingPose } from './world-grid-snap'
|
|
17
18
|
|
|
18
19
|
type Nodes = Readonly<Record<AnyNodeId, AnyNode>>
|
|
19
20
|
|
|
20
21
|
// Unit focus is a session gesture: the layer the user was on when focus began
|
|
21
22
|
// comes back when focus ends, unless they switched layers by hand meanwhile.
|
|
22
|
-
let restoreLayer: StructureLayer | null = null
|
|
23
23
|
let entering = false
|
|
24
24
|
let leaving = false
|
|
25
25
|
|
|
@@ -49,11 +49,7 @@ export function enterUnitFocus(unitId: UnitNode['id']): void {
|
|
|
49
49
|
const unit = nodes[unitId]
|
|
50
50
|
if (unit?.type !== 'unit') return
|
|
51
51
|
const editor = useEditor.getState()
|
|
52
|
-
if (
|
|
53
|
-
if (editor.phase !== 'structure') editor.setPhase('structure')
|
|
54
|
-
if (useEditor.getState().structureLayer !== 'zones') {
|
|
55
|
-
useEditor.getState().setStructureLayer('zones')
|
|
56
|
-
}
|
|
52
|
+
if (editor.phase === 'site') editor.setPhase('building')
|
|
57
53
|
const viewer = useViewer.getState()
|
|
58
54
|
if (viewer.focusedUnitId !== unitId) viewer.setFocusedUnit(unitId)
|
|
59
55
|
|
|
@@ -72,19 +68,7 @@ export function enterUnitFocus(unitId: UnitNode['id']): void {
|
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
function finishLeave(unitId: UnitNode['id']
|
|
76
|
-
const layer = restoreLayer
|
|
77
|
-
restoreLayer = null
|
|
78
|
-
const editor = useEditor.getState()
|
|
79
|
-
if (
|
|
80
|
-
!keepLayer &&
|
|
81
|
-
layer &&
|
|
82
|
-
layer !== 'zones' &&
|
|
83
|
-
editor.phase === 'structure' &&
|
|
84
|
-
editor.structureLayer === 'zones'
|
|
85
|
-
) {
|
|
86
|
-
editor.setStructureLayer(layer)
|
|
87
|
-
}
|
|
71
|
+
function finishLeave(unitId: UnitNode['id']) {
|
|
88
72
|
const { selection, setSelection } = useViewer.getState()
|
|
89
73
|
if (selection.selectedIds.length === 1 && selection.selectedIds[0] === unitId) {
|
|
90
74
|
setSelection({ selectedIds: [] })
|
|
@@ -92,7 +76,7 @@ function finishLeave(unitId: UnitNode['id'], keepLayer: boolean) {
|
|
|
92
76
|
}
|
|
93
77
|
|
|
94
78
|
/** Ends unit focus. Returns false when no unit was focused. */
|
|
95
|
-
export function leaveUnitFocus(
|
|
79
|
+
export function leaveUnitFocus(): boolean {
|
|
96
80
|
const viewer = useViewer.getState()
|
|
97
81
|
const unitId = viewer.focusedUnitId
|
|
98
82
|
if (!unitId) return false
|
|
@@ -102,7 +86,7 @@ export function leaveUnitFocus(options?: { keepLayer?: boolean }): boolean {
|
|
|
102
86
|
} finally {
|
|
103
87
|
leaving = false
|
|
104
88
|
}
|
|
105
|
-
finishLeave(unitId
|
|
89
|
+
finishLeave(unitId)
|
|
106
90
|
return true
|
|
107
91
|
}
|
|
108
92
|
|
|
@@ -124,28 +108,75 @@ export function assignZoneToUnit(zoneId: ZoneNode['id'], unitId: UnitNode['id']
|
|
|
124
108
|
if (updates.length > 0) updateNodes(updates)
|
|
125
109
|
}
|
|
126
110
|
|
|
111
|
+
/** The units of the building a zone's level belongs to, and the one listing the zone. */
|
|
112
|
+
export function zoneUnits(
|
|
113
|
+
nodes: Nodes,
|
|
114
|
+
zoneId: string,
|
|
115
|
+
): { buildingId: BuildingNode['id'] | null; units: UnitNode[]; current: UnitNode | null } {
|
|
116
|
+
const zone = nodes[zoneId as AnyNodeId]
|
|
117
|
+
const level = zone?.parentId ? nodes[zone.parentId as AnyNodeId] : undefined
|
|
118
|
+
const building = level?.parentId ? nodes[level.parentId as AnyNodeId] : undefined
|
|
119
|
+
if (building?.type !== 'building') return { buildingId: null, units: [], current: null }
|
|
120
|
+
const units = building.children.flatMap((id) => {
|
|
121
|
+
const child = nodes[id as AnyNodeId]
|
|
122
|
+
return child?.type === 'unit' ? [child] : []
|
|
123
|
+
})
|
|
124
|
+
return {
|
|
125
|
+
buildingId: building.id,
|
|
126
|
+
units,
|
|
127
|
+
current: units.find((unit) => unit.members.includes(zoneId as ZoneNode['id'])) ?? null,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Puts a zone in a new unit of its building (named "Unit N"), in one undo step, without focusing it. */
|
|
132
|
+
export function addZoneToNewUnit(zoneId: ZoneNode['id']): UnitNode['id'] | null {
|
|
133
|
+
const { buildingId, units } = zoneUnits(useScene.getState().nodes, zoneId)
|
|
134
|
+
if (!buildingId) return null
|
|
135
|
+
const unit = UnitNode.parse({ name: `Unit ${units.length + 1}` })
|
|
136
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
137
|
+
useScene.getState().createNode(unit, buildingId)
|
|
138
|
+
assignZoneToUnit(zoneId, unit.id)
|
|
139
|
+
})
|
|
140
|
+
return unit.id
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
export function toggleZoneMembership(unitId: UnitNode['id'], zoneId: ZoneNode['id']): void {
|
|
128
144
|
const unit = useScene.getState().nodes[unitId]
|
|
129
145
|
if (unit?.type !== 'unit') return
|
|
130
146
|
assignZoneToUnit(zoneId, unit.members.includes(zoneId) ? null : unitId)
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
/** How far (m) below a mezzanine's walking surface a hit still counts as on it (its plate and underside). */
|
|
150
|
+
const MEZZANINE_HIT_TOLERANCE = 0.05
|
|
151
|
+
|
|
133
152
|
/**
|
|
134
153
|
* The zone under a point on the current level, in building-local XZ. A 3D
|
|
135
154
|
* click lands on whatever surface is closest (a floor slab sits above the
|
|
136
155
|
* zone fill), so painting resolves the zone from the hit point instead of the
|
|
137
|
-
* hit mesh. Nested zones resolve to the smallest one
|
|
156
|
+
* hit mesh. Nested zones resolve to the smallest one, except that a mezzanine
|
|
157
|
+
* the hit may be on always wins over its host. With the hit's height (`y`,
|
|
158
|
+
* level-local) a mezzanine counts only when the hit is on or above its plate —
|
|
159
|
+
* the host floor under it is the host's; without one (the plan, seen from
|
|
160
|
+
* above) the mezzanine on top wins.
|
|
138
161
|
*/
|
|
139
|
-
export function zoneAtLevelPoint(x: number, z: number): ZoneNode | null {
|
|
162
|
+
export function zoneAtLevelPoint(x: number, z: number, y?: number): ZoneNode | null {
|
|
140
163
|
const levelId = useViewer.getState().selection.levelId
|
|
141
164
|
if (!levelId) return null
|
|
142
165
|
const nodes = useScene.getState().nodes
|
|
143
|
-
const resolve = (id: AnyNodeId) => nodes[id]
|
|
144
166
|
let best: ZoneNode | null = null
|
|
145
167
|
let bestArea = Number.POSITIVE_INFINITY
|
|
168
|
+
let bestOnTop = false
|
|
146
169
|
for (const node of Object.values(nodes)) {
|
|
147
170
|
if (node.type !== 'zone' || node.parentId !== levelId) continue
|
|
148
|
-
|
|
171
|
+
// An eligible mezzanine is the surface on top: it beats its host whatever
|
|
172
|
+
// their areas (a mezzanine may cover its whole host).
|
|
173
|
+
const onTop = node.floor?.support === 'open'
|
|
174
|
+
if (y !== undefined && onTop) {
|
|
175
|
+
const underside = (node.floor!.elevation ?? 0) - (node.floor!.thickness ?? 0.2)
|
|
176
|
+
if (y < underside - MEZZANINE_HIT_TOLERANCE) continue
|
|
177
|
+
}
|
|
178
|
+
if (bestOnTop && !onTop) continue
|
|
179
|
+
const polygon = node.polygon
|
|
149
180
|
if (!pointInPolygon2D([x, z], polygon)) continue
|
|
150
181
|
let area = 0
|
|
151
182
|
for (let i = 0; i < polygon.length; i++) {
|
|
@@ -154,20 +185,30 @@ export function zoneAtLevelPoint(x: number, z: number): ZoneNode | null {
|
|
|
154
185
|
area += a[0] * b[1] - b[0] * a[1]
|
|
155
186
|
}
|
|
156
187
|
area = Math.abs(area) / 2
|
|
157
|
-
if (area < bestArea) {
|
|
188
|
+
if (area < bestArea || (onTop && !bestOnTop)) {
|
|
158
189
|
best = node
|
|
159
190
|
bestArea = area
|
|
191
|
+
bestOnTop = onTop
|
|
160
192
|
}
|
|
161
193
|
}
|
|
162
194
|
return best
|
|
163
195
|
}
|
|
164
196
|
|
|
165
|
-
|
|
197
|
+
/** `worldY` (optional): the hit's height, so a click under a mezzanine picks the host. */
|
|
198
|
+
export function zoneAtWorldPoint(worldX: number, worldZ: number, worldY?: number): ZoneNode | null {
|
|
166
199
|
const pose = getActiveBuildingPose()
|
|
167
200
|
const [x, z] = pose
|
|
168
201
|
? snapWorldXZToBuildingLocal(worldX, worldZ, pose.position, pose.rotationY, 0).local
|
|
169
202
|
: [worldX, worldZ]
|
|
170
|
-
return zoneAtLevelPoint(x, z)
|
|
203
|
+
return zoneAtLevelPoint(x, z, worldY === undefined ? undefined : levelLocalY(worldY))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function levelLocalY(worldY: number): number {
|
|
207
|
+
const levelId = useViewer.getState().selection.levelId
|
|
208
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : undefined
|
|
209
|
+
if (!level) return worldY
|
|
210
|
+
level.updateWorldMatrix(true, false)
|
|
211
|
+
return worldY - level.matrixWorld.elements[13]!
|
|
171
212
|
}
|
|
172
213
|
|
|
173
214
|
export const ZONE_PAINT_DELAY_MS = 250
|
|
@@ -211,32 +252,29 @@ export function createUnitInBuilding(buildingId: BuildingNode['id']): UnitNode['
|
|
|
211
252
|
/**
|
|
212
253
|
* Keeps unit focus coherent with the rest of the editor: a focus set from
|
|
213
254
|
* anywhere (the unit inspector calls the viewer store directly) gets the full
|
|
214
|
-
* enter treatment, and focus ends when the user
|
|
215
|
-
*
|
|
255
|
+
* enter treatment, and focus ends when the user goes to site, selects
|
|
256
|
+
* anything other than the focused unit, or the unit is deleted.
|
|
216
257
|
*/
|
|
217
258
|
export function useUnitFocusRules(): void {
|
|
218
259
|
useEffect(() => {
|
|
219
260
|
const unsubscribeViewer = useViewer.subscribe((state, prev) => {
|
|
220
261
|
if (state.focusedUnitId !== prev.focusedUnitId) {
|
|
221
262
|
if (state.focusedUnitId) enterUnitFocus(state.focusedUnitId)
|
|
222
|
-
else if (prev.focusedUnitId && !leaving) finishLeave(prev.focusedUnitId
|
|
263
|
+
else if (prev.focusedUnitId && !leaving) finishLeave(prev.focusedUnitId)
|
|
223
264
|
return
|
|
224
265
|
}
|
|
225
266
|
const unitId = state.focusedUnitId
|
|
226
267
|
if (!unitId || state.selection === prev.selection) return
|
|
227
268
|
// Selecting a zone keeps focus: renaming or recolouring a zone row is
|
|
228
|
-
// part of arranging the unit
|
|
269
|
+
// part of arranging the unit.
|
|
229
270
|
const { selectedIds } = state.selection
|
|
230
271
|
const selectsOther =
|
|
231
272
|
selectedIds.length > 0 && !(selectedIds.length === 1 && selectedIds[0] === unitId)
|
|
232
273
|
if (selectsOther) leaveUnitFocus()
|
|
233
274
|
})
|
|
234
275
|
const unsubscribeEditor = useEditor.subscribe((state, prev) => {
|
|
235
|
-
if (state.
|
|
236
|
-
if (
|
|
237
|
-
if (state.phase !== 'structure' || state.structureLayer !== 'zones') {
|
|
238
|
-
leaveUnitFocus({ keepLayer: true })
|
|
239
|
-
}
|
|
276
|
+
if (state.phase === prev.phase || state.phase === 'building') return
|
|
277
|
+
if (useViewer.getState().focusedUnitId) leaveUnitFocus()
|
|
240
278
|
})
|
|
241
279
|
const unsubscribeScene = useScene.subscribe((state, prev) => {
|
|
242
280
|
if (state.nodes === prev.nodes) return
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { candidateEdges, hideUndrawnMeshes, materialShows } from './vector-edges'
|
|
4
|
+
|
|
5
|
+
/** A unit box's feature edges: 12, six floats each. */
|
|
6
|
+
const BOX_EDGE_FLOATS = 12 * 6
|
|
7
|
+
|
|
8
|
+
function sceneWithProxy() {
|
|
9
|
+
const scene = new THREE.Scene()
|
|
10
|
+
const house = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial())
|
|
11
|
+
// a utility-pole pick proxy: a tall box that draws nothing
|
|
12
|
+
const proxy = new THREE.Mesh(
|
|
13
|
+
new THREE.BoxGeometry(0.5, 10.7, 0.5),
|
|
14
|
+
new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: false }),
|
|
15
|
+
)
|
|
16
|
+
proxy.position.set(3, 0, 0)
|
|
17
|
+
scene.add(house, proxy)
|
|
18
|
+
scene.updateMatrixWorld(true)
|
|
19
|
+
return { scene, house, proxy }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('vector edges skip what draws nothing', () => {
|
|
23
|
+
test('a pick proxy (colorWrite false) has no lines', () => {
|
|
24
|
+
const { scene } = sceneWithProxy()
|
|
25
|
+
const camera = new THREE.OrthographicCamera(-10, 10, 10, -10, 0.1, 100)
|
|
26
|
+
const world = candidateEdges(scene, camera, 20, 400_000)
|
|
27
|
+
expect(world).toHaveLength(BOX_EDGE_FLOATS)
|
|
28
|
+
// every edge is the 1 m box's, none the 10.7 m proxy's
|
|
29
|
+
for (let i = 1; i < world.length; i += 3)
|
|
30
|
+
expect(Math.abs(world[i] as number)).toBeLessThanOrEqual(0.5)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('hidden materials, see-through hit boxes and colour-masked colliders all count as undrawn', () => {
|
|
34
|
+
expect(materialShows(new THREE.MeshBasicMaterial())).toBe(true)
|
|
35
|
+
expect(materialShows(new THREE.MeshBasicMaterial({ colorWrite: false }))).toBe(false)
|
|
36
|
+
expect(materialShows(new THREE.MeshBasicMaterial({ visible: false }))).toBe(false)
|
|
37
|
+
expect(materialShows(new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }))).toBe(
|
|
38
|
+
false,
|
|
39
|
+
)
|
|
40
|
+
expect(
|
|
41
|
+
materialShows([
|
|
42
|
+
new THREE.MeshBasicMaterial({ colorWrite: false }),
|
|
43
|
+
new THREE.MeshBasicMaterial(),
|
|
44
|
+
]),
|
|
45
|
+
).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('the depth and normal passes see no pick proxy, and it comes back after', () => {
|
|
49
|
+
const { scene, house, proxy } = sceneWithProxy()
|
|
50
|
+
const show = hideUndrawnMeshes(scene)
|
|
51
|
+
// under the passes' override material the proxy would write depth and
|
|
52
|
+
// hide the real edges behind it
|
|
53
|
+
expect(proxy.visible).toBe(false)
|
|
54
|
+
expect(house.visible).toBe(true)
|
|
55
|
+
show()
|
|
56
|
+
expect(proxy.visible).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
})
|