@pascal-app/editor 1.0.1 → 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 +16 -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 +221 -128
- 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 +37 -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 +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -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 +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- 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 +132 -0
- 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 +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- 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-group-move.tsx +17 -24
- 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-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- 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-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- 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 +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- 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/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- 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/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- 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 +82 -23
- 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 +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- 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/controls/slider-control.tsx +76 -31
- 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 +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- 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-height-mode.tsx +27 -21
- 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 +7 -19
- 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/shortcut-token.tsx +4 -0
- 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/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- 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-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- 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 +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- 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 +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- 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/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- 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 +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -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 +191 -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 +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- 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 +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- 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 +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- 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/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -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 +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- 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 +87 -0
- package/src/lib/scene.ts +39 -45
- 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 +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -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/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- 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/rotation-snapping.test.ts +53 -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 +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- 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,523 @@
|
|
|
1
|
+
import {
|
|
2
|
+
containsPoint,
|
|
3
|
+
createZoneDivisionContext,
|
|
4
|
+
DEFAULT_ANGLE_STEP,
|
|
5
|
+
divideZone,
|
|
6
|
+
generateId,
|
|
7
|
+
type Point,
|
|
8
|
+
type StructureNodes,
|
|
9
|
+
type StructurePlan,
|
|
10
|
+
snapPointAlongAngleRay,
|
|
11
|
+
snapPointToGrid,
|
|
12
|
+
snapZoneBoundary,
|
|
13
|
+
useScene,
|
|
14
|
+
type ZoneDivisionContext,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import useEditor, {
|
|
17
|
+
isAngleSnapActive,
|
|
18
|
+
isGridSnapActive,
|
|
19
|
+
isMagneticSnapActive,
|
|
20
|
+
} from '../store/use-editor'
|
|
21
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
22
|
+
import useWallSnapIndicator from '../store/use-wall-snap-indicator'
|
|
23
|
+
import { beginGesture, type GestureHandle } from './gesture-lifecycle'
|
|
24
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
25
|
+
import { sfxEmitter } from './sfx-bus'
|
|
26
|
+
|
|
27
|
+
let cached: {
|
|
28
|
+
nodes: StructureNodes
|
|
29
|
+
zoneId: string
|
|
30
|
+
revision: string
|
|
31
|
+
context: ZoneDivisionContext
|
|
32
|
+
} | null = null
|
|
33
|
+
|
|
34
|
+
// Everything the division context is built from: the room's own outline and
|
|
35
|
+
// every wall / separator on its level. Intent edits (a name, a finish) leave it
|
|
36
|
+
// alone; any boundary move changes it.
|
|
37
|
+
function topologyRevision(
|
|
38
|
+
nodes: StructureNodes,
|
|
39
|
+
zone: Extract<StructureNodes[string], { type: 'zone' }>,
|
|
40
|
+
) {
|
|
41
|
+
return JSON.stringify([
|
|
42
|
+
zone.parentId,
|
|
43
|
+
zone.polygon,
|
|
44
|
+
zone.holes,
|
|
45
|
+
Object.values(nodes).flatMap((node) =>
|
|
46
|
+
node.parentId === zone.parentId && (node.type === 'wall' || node.type === 'separator')
|
|
47
|
+
? [
|
|
48
|
+
[
|
|
49
|
+
node.id,
|
|
50
|
+
node.type,
|
|
51
|
+
node.start,
|
|
52
|
+
node.end,
|
|
53
|
+
node.type === 'wall' ? [node.curveOffset, node.thickness, node.justification] : null,
|
|
54
|
+
],
|
|
55
|
+
]
|
|
56
|
+
: [],
|
|
57
|
+
),
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function roomDivideContext(nodes: StructureNodes, zoneId: string) {
|
|
62
|
+
if (cached?.nodes === nodes && cached.zoneId === zoneId) return cached.context
|
|
63
|
+
const zone = nodes[zoneId]
|
|
64
|
+
if (zone?.type !== 'zone') throw Error('The selected room no longer exists.')
|
|
65
|
+
const revision = topologyRevision(nodes, zone)
|
|
66
|
+
const context =
|
|
67
|
+
cached?.zoneId === zoneId && cached.revision === revision
|
|
68
|
+
? cached.context
|
|
69
|
+
: createZoneDivisionContext(nodes, zoneId)
|
|
70
|
+
cached = { nodes, zoneId, revision, context }
|
|
71
|
+
return context
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The topology the active session started on. Points, boundary ids and
|
|
75
|
+
// validity all refer to it, so the draft cannot outlive it.
|
|
76
|
+
let sessionRevision: string | null = null
|
|
77
|
+
|
|
78
|
+
/** Whether the room being divided is gone or its boundaries changed since the draft began. */
|
|
79
|
+
export function roomDivideTopologyChanged(nodes: StructureNodes) {
|
|
80
|
+
const scope = useInteractionScope.getState().scope
|
|
81
|
+
if (scope.kind !== 'room-divide') return false
|
|
82
|
+
const zone = nodes[scope.nodeId]
|
|
83
|
+
return zone?.type !== 'zone' || topologyRevision(nodes, zone) !== sessionRevision
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Short cursor-side wording, the way the wall tool labels a draft it cannot
|
|
87
|
+
// build. Keyed by the planner's conflict codes; anything else is generic.
|
|
88
|
+
const DIVIDE_MESSAGES: Record<string, string> = {
|
|
89
|
+
'snap-distance': 'Move to an edge',
|
|
90
|
+
'boundary-overlap': 'Already an edge',
|
|
91
|
+
'short-cut': 'Too short',
|
|
92
|
+
'outside-room': 'Leaves the room',
|
|
93
|
+
'open-room': 'Room is open',
|
|
94
|
+
'self-cross': 'Crosses itself',
|
|
95
|
+
'self-intersection': 'Crosses itself',
|
|
96
|
+
'small-island': 'Too small',
|
|
97
|
+
'wall-clearance': 'Too close to a wall',
|
|
98
|
+
'close-loop': 'Close the loop',
|
|
99
|
+
}
|
|
100
|
+
export function roomDivideMessage(code?: string) {
|
|
101
|
+
return (code && DIVIDE_MESSAGES[code]) || "Can't divide here"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A floor pointer this close to the boundary lands on it; a boundary surface
|
|
105
|
+
// hit (a wall face in 3D) always does. Past it the point is free, so a path can
|
|
106
|
+
// turn inside the room and an island can start there.
|
|
107
|
+
const EDGE_CAPTURE = 0.5
|
|
108
|
+
// A pointer this close to an island's first point closes the loop.
|
|
109
|
+
const CLOSE_CAPTURE = 0.3
|
|
110
|
+
// `lines` squares a segment to the edge / aligns a point with earlier points
|
|
111
|
+
// within this much.
|
|
112
|
+
const LINES_TOLERANCE = 0.15
|
|
113
|
+
const MIN_SEGMENT = 0.05
|
|
114
|
+
|
|
115
|
+
type DivideScope = Extract<
|
|
116
|
+
ReturnType<typeof useInteractionScope.getState>['scope'],
|
|
117
|
+
{ kind: 'room-divide' }
|
|
118
|
+
>
|
|
119
|
+
|
|
120
|
+
/** Plans a divide along `path`: an open cut between two boundary points, or a closed island. */
|
|
121
|
+
function planRoomDivide(
|
|
122
|
+
nodes: StructureNodes,
|
|
123
|
+
zoneId: string,
|
|
124
|
+
input: { path: Point[]; closed: boolean; startBoundaryId?: string; endBoundaryId?: string },
|
|
125
|
+
context: ZoneDivisionContext,
|
|
126
|
+
mintId: Parameters<typeof divideZone>[1]['mintId'],
|
|
127
|
+
): StructurePlan {
|
|
128
|
+
return divideZone(nodes, { zoneId, ...input, mintId }, context)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Previews mint throwaway ids — distinct ones, since a path mints one per segment.
|
|
132
|
+
function previewMint() {
|
|
133
|
+
let n = 0
|
|
134
|
+
return (kind: string) => `${kind}_preview_${n++}`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const distance = (a: Point, b: Point) => Math.hypot(a[0] - b[0], a[1] - b[1])
|
|
138
|
+
|
|
139
|
+
// Proper crossing only: touching at an endpoint is not a crossing.
|
|
140
|
+
function segmentsCross(a: Point, b: Point, c: Point, d: Point) {
|
|
141
|
+
const cross = (o: Point, p: Point, q: Point) =>
|
|
142
|
+
(p[0] - o[0]) * (q[1] - o[1]) - (p[1] - o[1]) * (q[0] - o[0])
|
|
143
|
+
const d1 = cross(c, d, a),
|
|
144
|
+
d2 = cross(c, d, b),
|
|
145
|
+
d3 = cross(a, b, c),
|
|
146
|
+
d4 = cross(a, b, d)
|
|
147
|
+
const eps = 1e-9
|
|
148
|
+
return (
|
|
149
|
+
((d1 > eps && d2 < -eps) || (d1 < -eps && d2 > eps)) &&
|
|
150
|
+
((d3 > eps && d4 < -eps) || (d3 < -eps && d4 > eps))
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type Face = NonNullable<ZoneDivisionContext['face']>
|
|
155
|
+
const rings = (face: Face) => [face.referencePolygon, ...face.holes]
|
|
156
|
+
const inside = (face: Face, point: Point) =>
|
|
157
|
+
containsPoint([{ outer: face.referencePolygon, holes: face.holes }], point)
|
|
158
|
+
|
|
159
|
+
/** Why the segment `from → to` cannot join the path, or null when it can. */
|
|
160
|
+
function segmentConflict(face: Face, points: Point[], from: Point, to: Point) {
|
|
161
|
+
if (distance(from, to) < MIN_SEGMENT) return 'short-cut'
|
|
162
|
+
for (const ring of rings(face))
|
|
163
|
+
for (let i = 0; i < ring.length; i++)
|
|
164
|
+
if (segmentsCross(from, to, ring[i]!, ring[(i + 1) % ring.length]!)) return 'outside-room'
|
|
165
|
+
if (!inside(face, [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2])) return 'outside-room'
|
|
166
|
+
// Earlier segments, skipping the one sharing `from`.
|
|
167
|
+
for (let i = 0; i < points.length - 2; i++)
|
|
168
|
+
if (segmentsCross(from, to, points[i]!, points[i + 1]!)) return 'self-cross'
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Aligns a free point with the x or z of an earlier point (`lines`). */
|
|
173
|
+
function alignToPoints(point: Point, points: Point[]): Point {
|
|
174
|
+
let [x, z] = point
|
|
175
|
+
let bestX = LINES_TOLERANCE,
|
|
176
|
+
bestZ = LINES_TOLERANCE
|
|
177
|
+
for (const other of points) {
|
|
178
|
+
if (Math.abs(other[0] - point[0]) < bestX) {
|
|
179
|
+
bestX = Math.abs(other[0] - point[0])
|
|
180
|
+
x = other[0]
|
|
181
|
+
}
|
|
182
|
+
if (Math.abs(other[1] - point[1]) < bestZ) {
|
|
183
|
+
bestZ = Math.abs(other[1] - point[1])
|
|
184
|
+
z = other[1]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [x, z]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The wall tool's snap beacon where the live point locks on: the first point
|
|
192
|
+
* (closing an island) as a corner, a boundary (starting or finishing a cut)
|
|
193
|
+
* as a point on that wall. Cleared anywhere else.
|
|
194
|
+
*/
|
|
195
|
+
function publishDivideSnap(
|
|
196
|
+
end: Point | null,
|
|
197
|
+
endKind?: DivideScope['endKind'],
|
|
198
|
+
endBoundaryId?: string,
|
|
199
|
+
) {
|
|
200
|
+
const indicator = useWallSnapIndicator.getState()
|
|
201
|
+
if (end && endKind === 'close') indicator.set({ x: end[0], z: end[1], kind: 'endpoint' })
|
|
202
|
+
else if (end && endBoundaryId && (endKind === 'edge' || endKind === 'start'))
|
|
203
|
+
indicator.set({ x: end[0], z: end[1], kind: 'wall', wallIds: [endBoundaryId] })
|
|
204
|
+
else if (indicator.point) indicator.clear()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function invalidPreview(code?: string, end: Point | null = null) {
|
|
208
|
+
publishDivideSnap(null)
|
|
209
|
+
useInteractionScope.getState().update({
|
|
210
|
+
kind: 'room-divide',
|
|
211
|
+
end,
|
|
212
|
+
endKind: undefined,
|
|
213
|
+
endBoundaryId: undefined,
|
|
214
|
+
valid: false,
|
|
215
|
+
message: roomDivideMessage(code),
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let lastPointer: { point: Point; boundaryId?: string } | null = null
|
|
220
|
+
|
|
221
|
+
// The last planner verdict. Snapping lands many pointer moves on the same
|
|
222
|
+
// candidate, and the planner is the expensive step, so an identical candidate
|
|
223
|
+
// on the same context reuses it.
|
|
224
|
+
let planMemo: { context: ZoneDivisionContext; key: string; conflict?: string } | null = null
|
|
225
|
+
|
|
226
|
+
function previewConflict(
|
|
227
|
+
nodes: StructureNodes,
|
|
228
|
+
zoneId: string,
|
|
229
|
+
input: Parameters<typeof planRoomDivide>[2],
|
|
230
|
+
context: ZoneDivisionContext,
|
|
231
|
+
) {
|
|
232
|
+
const key = JSON.stringify(input)
|
|
233
|
+
if (planMemo?.context === context && planMemo.key === key) return planMemo.conflict
|
|
234
|
+
const conflict = planRoomDivide(nodes, zoneId, input, context, previewMint()).conflicts?.[0]?.code
|
|
235
|
+
planMemo = { context, key, conflict }
|
|
236
|
+
return conflict
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let owner: GestureHandle | null = null
|
|
240
|
+
|
|
241
|
+
function resetSession() {
|
|
242
|
+
cached = null
|
|
243
|
+
lastPointer = null
|
|
244
|
+
planMemo = null
|
|
245
|
+
sessionRevision = null
|
|
246
|
+
publishDivideSnap(null)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Starts the draft under the gesture lifecycle owner: a mode, tool, level or
|
|
251
|
+
* selection change, the scope replaced, the room's boundary changing under it
|
|
252
|
+
* (undo, another tool) or its room deselected, and any history command end it.
|
|
253
|
+
*/
|
|
254
|
+
export function startRoomDivide(zoneId: string, levelId: string) {
|
|
255
|
+
owner?.cancel()
|
|
256
|
+
resetSession()
|
|
257
|
+
const nodes: StructureNodes = useScene.getState().nodes
|
|
258
|
+
const zone = nodes[zoneId]
|
|
259
|
+
sessionRevision = zone?.type === 'zone' ? topologyRevision(nodes, zone) : null
|
|
260
|
+
const handle = beginGesture({
|
|
261
|
+
kind: 'room-divide',
|
|
262
|
+
scope: { kind: 'room-divide', nodeId: zoneId, levelId, points: [], end: null, valid: false },
|
|
263
|
+
stale: () =>
|
|
264
|
+
useEditor.getState().room?.zoneId !== zoneId ||
|
|
265
|
+
roomDivideTopologyChanged(useScene.getState().nodes),
|
|
266
|
+
onCancel: () => {
|
|
267
|
+
if (owner === handle) owner = null
|
|
268
|
+
resetSession()
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
owner = handle
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Ends the draft (committed or dropped). Idempotent. */
|
|
275
|
+
export function cancelRoomDivide() {
|
|
276
|
+
const current = owner
|
|
277
|
+
owner = null
|
|
278
|
+
resetSession()
|
|
279
|
+
if (current?.active) current.cancel()
|
|
280
|
+
else useInteractionScope.getState().endIf((scope) => scope.kind === 'room-divide')
|
|
281
|
+
}
|
|
282
|
+
/** Whether a plan point lies inside the room being divided (off its boundary). */
|
|
283
|
+
export function roomDivideContains(point: Point) {
|
|
284
|
+
const scope = useInteractionScope.getState().scope
|
|
285
|
+
if (scope.kind !== 'room-divide') return false
|
|
286
|
+
try {
|
|
287
|
+
const face = roomDivideContext(useScene.getState().nodes, scope.nodeId).face
|
|
288
|
+
return !!face && inside(face, point)
|
|
289
|
+
} catch {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Moves the live point. The first point snaps onto the room's boundary (or,
|
|
296
|
+
* away from it, starts an island inside the room); later points are free,
|
|
297
|
+
* placed by the active snapping mode — the wall tool's modes, cycled with
|
|
298
|
+
* Shift — until the pointer reaches the boundary again (finishing the cut) or
|
|
299
|
+
* an island's first point (closing the loop).
|
|
300
|
+
*/
|
|
301
|
+
export function previewRoomDivide(point: Point, boundaryId?: string) {
|
|
302
|
+
const scope = useInteractionScope.getState().scope
|
|
303
|
+
if (scope.kind !== 'room-divide') return
|
|
304
|
+
lastPointer = { point, boundaryId }
|
|
305
|
+
const hover: Point | null = point.every(Number.isFinite) ? point : null
|
|
306
|
+
try {
|
|
307
|
+
const nodes = useScene.getState().nodes
|
|
308
|
+
const context = roomDivideContext(nodes, scope.nodeId)
|
|
309
|
+
const face = context.face
|
|
310
|
+
if (!face) return invalidPreview('open-room', hover)
|
|
311
|
+
const { points, startBoundaryId } = scope
|
|
312
|
+
const from = points.at(-1) ?? null
|
|
313
|
+
const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
|
314
|
+
|
|
315
|
+
const first = points[0]
|
|
316
|
+
const island = !!first && points.length >= 3 && !startBoundaryId
|
|
317
|
+
const close = () => {
|
|
318
|
+
const conflict = previewConflict(nodes, scope.nodeId, { path: points, closed: true }, context)
|
|
319
|
+
return update(scope, first!, 'close', undefined, conflict)
|
|
320
|
+
}
|
|
321
|
+
if (island && distance(point, first!) <= CLOSE_CAPTURE) return close()
|
|
322
|
+
|
|
323
|
+
const destination = snapZoneBoundary(nodes, scope.nodeId, point, step, context, boundaryId)
|
|
324
|
+
if (destination && (boundaryId || destination.distance <= EDGE_CAPTURE)) {
|
|
325
|
+
if (!from) return update(scope, destination.point, 'start', destination.boundaryId)
|
|
326
|
+
if (!startBoundaryId) return invalidPreview('close-loop', destination.point)
|
|
327
|
+
const target = lockToEdge(nodes, scope, context, destination, from) ?? destination.point
|
|
328
|
+
const path = [...points, target]
|
|
329
|
+
const conflict = previewConflict(
|
|
330
|
+
nodes,
|
|
331
|
+
scope.nodeId,
|
|
332
|
+
{ path, closed: false, startBoundaryId, endBoundaryId: destination.boundaryId },
|
|
333
|
+
context,
|
|
334
|
+
)
|
|
335
|
+
return update(scope, target, 'edge', destination.boundaryId, conflict)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let target = point
|
|
339
|
+
if (from && isAngleSnapActive())
|
|
340
|
+
target = snapPointAlongAngleRay(from, point, DEFAULT_ANGLE_STEP) as Point
|
|
341
|
+
else if (step > 0) target = snapPointToGrid(point, step) as Point
|
|
342
|
+
else if (isMagneticSnapActive()) target = alignToPoints(point, points)
|
|
343
|
+
// The snapping mode can land the point on the island's start by itself.
|
|
344
|
+
if (island && distance(target, first!) < 1e-6) return close()
|
|
345
|
+
if (!inside(face, target)) return invalidPreview(from ? 'outside-room' : 'snap-distance', hover)
|
|
346
|
+
const conflict = from ? segmentConflict(face, points, from, target) : null
|
|
347
|
+
return update(scope, target, from ? 'point' : 'start', undefined, conflict ?? undefined)
|
|
348
|
+
} catch {
|
|
349
|
+
invalidPreview(undefined, hover)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function update(
|
|
354
|
+
scope: DivideScope,
|
|
355
|
+
end: Point,
|
|
356
|
+
endKind: NonNullable<DivideScope['endKind']>,
|
|
357
|
+
endBoundaryId?: string,
|
|
358
|
+
conflict?: string | null,
|
|
359
|
+
) {
|
|
360
|
+
const valid = !conflict
|
|
361
|
+
const message = conflict ? roomDivideMessage(conflict) : undefined
|
|
362
|
+
publishDivideSnap(end, endKind, endBoundaryId)
|
|
363
|
+
const moved = scope.end?.[0] !== end[0] || scope.end?.[1] !== end[1]
|
|
364
|
+
// Unchanged preview: skip the store write and every overlay re-render.
|
|
365
|
+
if (
|
|
366
|
+
!moved &&
|
|
367
|
+
scope.endKind === endKind &&
|
|
368
|
+
scope.endBoundaryId === endBoundaryId &&
|
|
369
|
+
scope.valid === valid &&
|
|
370
|
+
scope.message === message
|
|
371
|
+
)
|
|
372
|
+
return
|
|
373
|
+
// The wall draft's tick: the live point stepped to a new snapped spot.
|
|
374
|
+
if (moved && scope.points.length) sfxEmitter.emit('sfx:grid-snap')
|
|
375
|
+
useInteractionScope.getState().update({
|
|
376
|
+
kind: 'room-divide',
|
|
377
|
+
end,
|
|
378
|
+
endKind,
|
|
379
|
+
endBoundaryId,
|
|
380
|
+
valid,
|
|
381
|
+
message,
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Where the last segment meets the boundary under the snapping mode: `lines`
|
|
387
|
+
* squares it to the edge (to the start edge for a straight cut), `angles`
|
|
388
|
+
* locks it to 15° rays from the previous point. Null keeps the pointer's own
|
|
389
|
+
* boundary point.
|
|
390
|
+
*/
|
|
391
|
+
function lockToEdge(
|
|
392
|
+
nodes: StructureNodes,
|
|
393
|
+
scope: DivideScope,
|
|
394
|
+
context: ZoneDivisionContext,
|
|
395
|
+
destination: NonNullable<ReturnType<typeof snapZoneBoundary>>,
|
|
396
|
+
from: Point,
|
|
397
|
+
): Point | null {
|
|
398
|
+
// Where the ray from `from` along `direction` crosses the boundary the
|
|
399
|
+
// pointer is on — kept only if it really lands on that boundary.
|
|
400
|
+
const lock = (direction: Point): Point | null => {
|
|
401
|
+
const along = destination.direction
|
|
402
|
+
const denominator = direction[0] * along[1] - direction[1] * along[0]
|
|
403
|
+
if (Math.abs(denominator) < 1e-8) return null
|
|
404
|
+
const reach =
|
|
405
|
+
((destination.point[0] - from[0]) * along[1] - (destination.point[1] - from[1]) * along[0]) /
|
|
406
|
+
denominator
|
|
407
|
+
if (reach <= 0) return null
|
|
408
|
+
const crossing: Point = [from[0] + direction[0] * reach, from[1] + direction[1] * reach]
|
|
409
|
+
const snapped = snapZoneBoundary(nodes, scope.nodeId, crossing, 0, context)
|
|
410
|
+
return snapped && snapped.boundaryId === destination.boundaryId && snapped.distance < 1e-6
|
|
411
|
+
? snapped.point
|
|
412
|
+
: null
|
|
413
|
+
}
|
|
414
|
+
const target = destination.point
|
|
415
|
+
if (isMagneticSnapActive()) {
|
|
416
|
+
const straight = scope.points.length === 1
|
|
417
|
+
const anchor = straight ? snapZoneBoundary(nodes, scope.nodeId, from, 0, context) : destination
|
|
418
|
+
if (!anchor) return null
|
|
419
|
+
const tangent = anchor.direction
|
|
420
|
+
const offset = (target[0] - from[0]) * tangent[0] + (target[1] - from[1]) * tangent[1]
|
|
421
|
+
if (Math.abs(offset) > LINES_TOLERANCE) return null
|
|
422
|
+
const normal: Point = [-tangent[1], tangent[0]]
|
|
423
|
+
const side = (target[0] - from[0]) * normal[0] + (target[1] - from[1]) * normal[1] < 0 ? -1 : 1
|
|
424
|
+
return lock([normal[0] * side, normal[1] * side])
|
|
425
|
+
}
|
|
426
|
+
if (isAngleSnapActive()) {
|
|
427
|
+
const angle = Math.atan2(target[1] - from[1], target[0] - from[0])
|
|
428
|
+
const locked = Math.round(angle / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
|
|
429
|
+
return lock([Math.cos(locked), Math.sin(locked)])
|
|
430
|
+
}
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function commit(scope: DivideScope, context: ZoneDivisionContext) {
|
|
435
|
+
const closed = scope.endKind === 'close'
|
|
436
|
+
const path = closed ? scope.points : [...scope.points, scope.end!]
|
|
437
|
+
const plan = planRoomDivide(
|
|
438
|
+
useScene.getState().nodes,
|
|
439
|
+
scope.nodeId,
|
|
440
|
+
{
|
|
441
|
+
path,
|
|
442
|
+
closed,
|
|
443
|
+
startBoundaryId: scope.startBoundaryId,
|
|
444
|
+
endBoundaryId: closed ? undefined : scope.endBoundaryId,
|
|
445
|
+
},
|
|
446
|
+
context,
|
|
447
|
+
generateId,
|
|
448
|
+
)
|
|
449
|
+
if (plan.conflicts?.length) {
|
|
450
|
+
invalidPreview(plan.conflicts[0]?.code, scope.end)
|
|
451
|
+
return false
|
|
452
|
+
}
|
|
453
|
+
// Out of the lifecycle before the write: the boundary it changes must not read as stale.
|
|
454
|
+
const current = owner
|
|
455
|
+
owner = null
|
|
456
|
+
try {
|
|
457
|
+
if (current) current.finish(() => applyRoomPlan(plan))
|
|
458
|
+
else applyRoomPlan(plan)
|
|
459
|
+
} finally {
|
|
460
|
+
resetSession()
|
|
461
|
+
useInteractionScope.getState().endIf((live) => live.kind === 'room-divide')
|
|
462
|
+
}
|
|
463
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
464
|
+
return true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Places the live point: starts the path, adds a point, or — on the boundary
|
|
469
|
+
* or an island's first point — commits the division. True once committed.
|
|
470
|
+
*/
|
|
471
|
+
export function clickRoomDivide() {
|
|
472
|
+
const scope = useInteractionScope.getState().scope
|
|
473
|
+
if (scope.kind !== 'room-divide' || !scope.end || !scope.valid) return false
|
|
474
|
+
try {
|
|
475
|
+
const context = roomDivideContext(useScene.getState().nodes, scope.nodeId)
|
|
476
|
+
if (!context.face) {
|
|
477
|
+
invalidPreview('open-room')
|
|
478
|
+
return false
|
|
479
|
+
}
|
|
480
|
+
if (scope.endKind === 'edge' || scope.endKind === 'close') return commit(scope, context)
|
|
481
|
+
if (!scope.points.length) sfxEmitter.emit('sfx:structure-build-start')
|
|
482
|
+
useInteractionScope.getState().update({
|
|
483
|
+
kind: 'room-divide',
|
|
484
|
+
points: [...scope.points, scope.end],
|
|
485
|
+
startBoundaryId: scope.points.length ? scope.startBoundaryId : scope.endBoundaryId,
|
|
486
|
+
end: null,
|
|
487
|
+
endKind: undefined,
|
|
488
|
+
endBoundaryId: undefined,
|
|
489
|
+
valid: false,
|
|
490
|
+
message: undefined,
|
|
491
|
+
})
|
|
492
|
+
return false
|
|
493
|
+
} catch {
|
|
494
|
+
invalidPreview()
|
|
495
|
+
return false
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Backspace: drops the last placed point and re-reads the pointer. */
|
|
500
|
+
export function removeLastRoomDividePoint() {
|
|
501
|
+
const scope = useInteractionScope.getState().scope
|
|
502
|
+
if (scope.kind !== 'room-divide' || !scope.points.length) return false
|
|
503
|
+
const points = scope.points.slice(0, -1)
|
|
504
|
+
useInteractionScope.getState().update({
|
|
505
|
+
kind: 'room-divide',
|
|
506
|
+
points,
|
|
507
|
+
startBoundaryId: points.length ? scope.startBoundaryId : undefined,
|
|
508
|
+
end: null,
|
|
509
|
+
endKind: undefined,
|
|
510
|
+
endBoundaryId: undefined,
|
|
511
|
+
valid: false,
|
|
512
|
+
message: undefined,
|
|
513
|
+
})
|
|
514
|
+
if (lastPointer) previewRoomDivide(lastPointer.point, lastPointer.boundaryId)
|
|
515
|
+
return true
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Enter: commits an open path whose live point sits on the boundary. */
|
|
519
|
+
export function finishRoomDivide() {
|
|
520
|
+
const scope = useInteractionScope.getState().scope
|
|
521
|
+
if (scope.kind !== 'room-divide' || scope.endKind !== 'edge' || !scope.valid) return false
|
|
522
|
+
return clickRoomDivide()
|
|
523
|
+
}
|