@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,509 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
getWallBaseElevationForNodes,
|
|
7
|
+
getWallEffectiveHeightForNodes,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
useLiveNodeOverrides,
|
|
10
|
+
useScene,
|
|
11
|
+
type WallFace,
|
|
12
|
+
type WallFaceRegion,
|
|
13
|
+
type WallNode,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { getWallFaceBaseAt, getWallFinishData, useViewer } from '@pascal-app/viewer'
|
|
16
|
+
import { createPortal, useFrame, useThree } from '@react-three/fiber'
|
|
17
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
18
|
+
import {
|
|
19
|
+
BufferAttribute,
|
|
20
|
+
BufferGeometry,
|
|
21
|
+
Color,
|
|
22
|
+
DoubleSide,
|
|
23
|
+
type Group,
|
|
24
|
+
type Mesh,
|
|
25
|
+
type Object3D,
|
|
26
|
+
OrthographicCamera,
|
|
27
|
+
Ray,
|
|
28
|
+
Vector3,
|
|
29
|
+
} from 'three'
|
|
30
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
31
|
+
import type { WallRegionBounds } from '../../lib/paint-regions'
|
|
32
|
+
import { updateWallRegion } from '../../lib/paint-regions'
|
|
33
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
34
|
+
import { resolveWallFaceBase } from '../../lib/wall-region-face'
|
|
35
|
+
import {
|
|
36
|
+
buildOutlineRibbon,
|
|
37
|
+
faceSurfacePoint,
|
|
38
|
+
getWallFaceSurface,
|
|
39
|
+
intersectFaceSurface,
|
|
40
|
+
type RegionEdge,
|
|
41
|
+
type RegionHandlePlacement,
|
|
42
|
+
regionHandlePlacements,
|
|
43
|
+
regionOutline,
|
|
44
|
+
resolveRegionBoundDrag,
|
|
45
|
+
resolveRegionBounds,
|
|
46
|
+
type WallFaceSurface,
|
|
47
|
+
wallLocalMatrix,
|
|
48
|
+
withRegionBounds,
|
|
49
|
+
} from '../../lib/wall-region-handles'
|
|
50
|
+
import { wallRegionSnapTargets } from '../../lib/wall-region-snap'
|
|
51
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
52
|
+
import useInteractionScope from '../../store/use-interaction-scope'
|
|
53
|
+
import {
|
|
54
|
+
handleWallRegionDeleteKey,
|
|
55
|
+
installWallRegionSceneSync,
|
|
56
|
+
syncWallRegionSelection,
|
|
57
|
+
useWallRegionSelection,
|
|
58
|
+
} from '../../store/use-wall-region-handles-selection'
|
|
59
|
+
import {
|
|
60
|
+
ARROW_COLOR,
|
|
61
|
+
ARROW_HOVER_COLOR,
|
|
62
|
+
ARROW_SCALE,
|
|
63
|
+
HandleArrow,
|
|
64
|
+
InvisibleHandleHitArea,
|
|
65
|
+
NO_RAYCAST,
|
|
66
|
+
useInvisibleHitAreaMaterial,
|
|
67
|
+
} from './handles/handle-arrow'
|
|
68
|
+
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
69
|
+
|
|
70
|
+
// Resize handles for the paint regions of the one selected wall: a thin
|
|
71
|
+
// outline per region and an arrow on each bounded edge, shown only while the
|
|
72
|
+
// region is active, hovered or being resized so they do not crowd the wall's
|
|
73
|
+
// own arrows. Dragging an edge previews through a live override and commits
|
|
74
|
+
// one `updateWallRegion`.
|
|
75
|
+
// Clicking an outline or handle makes that region active, and Delete then
|
|
76
|
+
// removes the region instead of the wall.
|
|
77
|
+
|
|
78
|
+
export const PAINT_REGION_HANDLE = 'paint-region-bound'
|
|
79
|
+
|
|
80
|
+
const OUTLINE_HALF_WIDTH = 0.006
|
|
81
|
+
const OUTLINE_ACTIVE_HALF_WIDTH = 0.011
|
|
82
|
+
const OUTLINE_HIT_HALF_WIDTH = 0.04
|
|
83
|
+
const NOOP_DRAG_CONTROLS: HandleDragControls = { onStart: () => {}, onEnd: () => {} }
|
|
84
|
+
// The arrows sit on the outline: the delay lets the pointer cross from one to the other.
|
|
85
|
+
const ARROW_HIDE_DELAY_MS = 200
|
|
86
|
+
|
|
87
|
+
const outlineMaterials = new Map<boolean, MeshBasicNodeMaterial>()
|
|
88
|
+
function outlineMaterial(active: boolean) {
|
|
89
|
+
let material = outlineMaterials.get(active)
|
|
90
|
+
if (!material) {
|
|
91
|
+
material = new MeshBasicNodeMaterial({
|
|
92
|
+
color: new Color(active ? ARROW_HOVER_COLOR : ARROW_COLOR),
|
|
93
|
+
side: DoubleSide,
|
|
94
|
+
transparent: true,
|
|
95
|
+
opacity: active ? 1 : 0.8,
|
|
96
|
+
depthTest: false,
|
|
97
|
+
depthWrite: false,
|
|
98
|
+
})
|
|
99
|
+
outlineMaterials.set(active, material)
|
|
100
|
+
}
|
|
101
|
+
return material
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isOwnScope(
|
|
105
|
+
scope: ReturnType<typeof useInteractionScope.getState>['scope'],
|
|
106
|
+
wallId: string | undefined,
|
|
107
|
+
) {
|
|
108
|
+
return (
|
|
109
|
+
scope.kind === 'handle-drag' && scope.handle === PAINT_REGION_HANDLE && scope.nodeId === wallId
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function WallRegionHandles() {
|
|
114
|
+
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
115
|
+
const mode = useEditor((state) => state.mode)
|
|
116
|
+
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
117
|
+
const scope = useInteractionScope((state) => state.scope)
|
|
118
|
+
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
119
|
+
const wall = useScene((state) => {
|
|
120
|
+
const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
|
|
121
|
+
return node?.type === 'wall' && node.faceRegions?.length ? node : null
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
useEffect(() => syncWallRegionSelection(selectedIds), [selectedIds])
|
|
125
|
+
|
|
126
|
+
// Capture phase, so an active region takes Delete before the global
|
|
127
|
+
// use-keyboard arm deletes the wall.
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
130
|
+
handleWallRegionDeleteKey(event)
|
|
131
|
+
}
|
|
132
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
133
|
+
const uninstallSceneSync = installWallRegionSceneSync()
|
|
134
|
+
return () => {
|
|
135
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
136
|
+
uninstallSceneSync()
|
|
137
|
+
}
|
|
138
|
+
}, [])
|
|
139
|
+
|
|
140
|
+
const ownScope = isOwnScope(scope, wall?.id)
|
|
141
|
+
const shouldRender =
|
|
142
|
+
Boolean(wall) &&
|
|
143
|
+
mode === 'select' &&
|
|
144
|
+
(ownScope || (scope.kind === 'idle' && !isFloorplanHovered))
|
|
145
|
+
|
|
146
|
+
if (!(shouldRender && wall)) return null
|
|
147
|
+
return <WallRegionHandlesForWall wall={wall} />
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function useLevelObject(parentId: string | null | undefined) {
|
|
151
|
+
const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
|
|
152
|
+
parentId ? (sceneRegistry.nodes.get(parentId) ?? null) : null,
|
|
153
|
+
)
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
let frameId = 0
|
|
156
|
+
const resolve = () => {
|
|
157
|
+
const next = parentId ? (sceneRegistry.nodes.get(parentId) ?? null) : null
|
|
158
|
+
setLevelObject(next)
|
|
159
|
+
if (!next) frameId = window.requestAnimationFrame(resolve)
|
|
160
|
+
}
|
|
161
|
+
resolve()
|
|
162
|
+
return () => {
|
|
163
|
+
if (frameId) window.cancelAnimationFrame(frameId)
|
|
164
|
+
}
|
|
165
|
+
}, [parentId])
|
|
166
|
+
return levelObject
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type WallFrame = {
|
|
170
|
+
wall: WallNode
|
|
171
|
+
baseElevation: number
|
|
172
|
+
wallHeight: number
|
|
173
|
+
angle: number
|
|
174
|
+
levelObject: Object3D
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function faceBaseSampler(wallId: string, face: WallFace) {
|
|
178
|
+
const mesh = sceneRegistry.nodes.get(wallId as AnyNodeId) as Mesh | undefined
|
|
179
|
+
const data = getWallFinishData(mesh?.geometry)
|
|
180
|
+
return (u: number) => getWallFaceBaseAt(data, face, u)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function regionFaceHeight(
|
|
184
|
+
region: WallFaceRegion,
|
|
185
|
+
length: number,
|
|
186
|
+
wallHeight: number,
|
|
187
|
+
baseAt: (u: number) => number,
|
|
188
|
+
) {
|
|
189
|
+
const midU = ((region.u0 ?? 0) + (region.u1 ?? length)) / 2
|
|
190
|
+
return Math.max(0, wallHeight - baseAt(midU))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function WallRegionHandlesForWall({ wall }: { wall: WallNode }) {
|
|
194
|
+
const liveOverride = useLiveNodeOverrides((state) => state.overrides.get(wall.id))
|
|
195
|
+
const effectiveWall = useMemo(
|
|
196
|
+
() => (liveOverride ? ({ ...wall, ...liveOverride } as WallNode) : wall),
|
|
197
|
+
[wall, liveOverride],
|
|
198
|
+
)
|
|
199
|
+
const nodes = useScene((state) => state.nodes)
|
|
200
|
+
const levelObject = useLevelObject(wall.parentId)
|
|
201
|
+
if (!levelObject) return null
|
|
202
|
+
|
|
203
|
+
const frame: WallFrame = {
|
|
204
|
+
wall: effectiveWall,
|
|
205
|
+
baseElevation: getWallBaseElevationForNodes(effectiveWall, nodes),
|
|
206
|
+
wallHeight: getWallEffectiveHeightForNodes(effectiveWall, nodes),
|
|
207
|
+
angle: Math.atan2(
|
|
208
|
+
effectiveWall.end[1] - effectiveWall.start[1],
|
|
209
|
+
effectiveWall.end[0] - effectiveWall.start[0],
|
|
210
|
+
),
|
|
211
|
+
levelObject,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return createPortal(
|
|
215
|
+
<group
|
|
216
|
+
position={[effectiveWall.start[0], frame.baseElevation, effectiveWall.start[1]]}
|
|
217
|
+
rotation={[0, -frame.angle, 0]}
|
|
218
|
+
>
|
|
219
|
+
<FaceRegionHandles face="a" frame={frame} />
|
|
220
|
+
<FaceRegionHandles face="b" frame={frame} />
|
|
221
|
+
</group>,
|
|
222
|
+
levelObject,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function FaceRegionHandles({ face, frame }: { face: WallFace; frame: WallFrame }) {
|
|
227
|
+
const { wall } = frame
|
|
228
|
+
const groupRef = useRef<Group>(null)
|
|
229
|
+
const cameraPosition = useRef(new Vector3())
|
|
230
|
+
const [facing, setFacing] = useState(true)
|
|
231
|
+
const { camera } = useThree()
|
|
232
|
+
// Only the frame-defining fields: region edits must not rebuild the surface.
|
|
233
|
+
const { start, end, curveOffset, thickness, justification } = wall
|
|
234
|
+
const surface = useMemo(
|
|
235
|
+
() => getWallFaceSurface({ start, end, curveOffset, thickness, justification }, face),
|
|
236
|
+
[start, end, curveOffset, thickness, justification, face],
|
|
237
|
+
)
|
|
238
|
+
const regions = (wall.faceRegions ?? []).filter((region) => region.face === face)
|
|
239
|
+
|
|
240
|
+
// A face turned away from the camera hides its handles: they draw on top of
|
|
241
|
+
// everything, so they would read as belonging to the near face.
|
|
242
|
+
useFrame(() => {
|
|
243
|
+
const group = groupRef.current
|
|
244
|
+
if (!group) return
|
|
245
|
+
if (isOwnScope(useInteractionScope.getState().scope, wall.id)) return
|
|
246
|
+
const local = group.worldToLocal(camera.getWorldPosition(cameraPosition.current))
|
|
247
|
+
const point = faceSurfacePoint(surface, surface.length / 2)
|
|
248
|
+
const next = (local.x - point.x) * point.nx + (local.z - point.z) * point.nz > 0
|
|
249
|
+
if (next !== facing) setFacing(next)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
if (regions.length === 0) return null
|
|
253
|
+
const baseAt = faceBaseSampler(wall.id, face)
|
|
254
|
+
return (
|
|
255
|
+
<group ref={groupRef}>
|
|
256
|
+
{facing &&
|
|
257
|
+
regions.map((region) => (
|
|
258
|
+
<RegionEditor
|
|
259
|
+
baseAt={baseAt}
|
|
260
|
+
frame={frame}
|
|
261
|
+
key={region.id}
|
|
262
|
+
region={region}
|
|
263
|
+
surface={surface}
|
|
264
|
+
/>
|
|
265
|
+
))}
|
|
266
|
+
</group>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function useRibbonGeometry(positions: Float32Array) {
|
|
271
|
+
const geometry = useMemo(() => {
|
|
272
|
+
const next = new BufferGeometry()
|
|
273
|
+
next.setAttribute('position', new BufferAttribute(positions, 3))
|
|
274
|
+
next.computeBoundingSphere()
|
|
275
|
+
return next
|
|
276
|
+
}, [positions])
|
|
277
|
+
useEffect(() => () => geometry.dispose(), [geometry])
|
|
278
|
+
return geometry
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function RegionEditor({
|
|
282
|
+
region,
|
|
283
|
+
surface,
|
|
284
|
+
frame,
|
|
285
|
+
baseAt,
|
|
286
|
+
}: {
|
|
287
|
+
region: WallFaceRegion
|
|
288
|
+
surface: WallFaceSurface
|
|
289
|
+
frame: WallFrame
|
|
290
|
+
baseAt: (u: number) => number
|
|
291
|
+
}) {
|
|
292
|
+
const { wall } = frame
|
|
293
|
+
const [outlineHovered, setOutlineHovered] = useState(false)
|
|
294
|
+
const [hoveredArrow, setHoveredArrow] = useState<RegionEdge | null>(null)
|
|
295
|
+
const [draggingEdge, setDraggingEdge] = useState<RegionEdge | null>(null)
|
|
296
|
+
const [arrowsLinger, setArrowsLinger] = useState(false)
|
|
297
|
+
const isActive = useWallRegionSelection(
|
|
298
|
+
(state) => state.active?.wallId === wall.id && state.active.regionId === region.id,
|
|
299
|
+
)
|
|
300
|
+
const faceHeight = regionFaceHeight(region, surface.length, frame.wallHeight, baseAt)
|
|
301
|
+
const boundsKey = [region.u0, region.u1, region.v0, region.v1, faceHeight, surface.length].join(
|
|
302
|
+
':',
|
|
303
|
+
)
|
|
304
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: boundsKey captures every input
|
|
305
|
+
const outline = useMemo(
|
|
306
|
+
() => regionOutline(surface, region, faceHeight, baseAt),
|
|
307
|
+
[boundsKey, surface],
|
|
308
|
+
)
|
|
309
|
+
const placements = regionHandlePlacements(surface, region, faceHeight, baseAt, draggingEdge)
|
|
310
|
+
const hovered =
|
|
311
|
+
outlineHovered ||
|
|
312
|
+
(hoveredArrow !== null && placements.some((placement) => placement.edge === hoveredArrow))
|
|
313
|
+
const showArrows = isActive || hovered || arrowsLinger || draggingEdge !== null
|
|
314
|
+
const halfWidth = isActive || hovered ? OUTLINE_ACTIVE_HALF_WIDTH : OUTLINE_HALF_WIDTH
|
|
315
|
+
const ribbon = useRibbonGeometry(
|
|
316
|
+
useMemo(() => buildOutlineRibbon(outline, halfWidth), [outline, halfWidth]),
|
|
317
|
+
)
|
|
318
|
+
const hitRibbon = useRibbonGeometry(
|
|
319
|
+
useMemo(() => buildOutlineRibbon(outline, OUTLINE_HIT_HALF_WIDTH), [outline]),
|
|
320
|
+
)
|
|
321
|
+
const hitMaterial = useInvisibleHitAreaMaterial()
|
|
322
|
+
const select = () =>
|
|
323
|
+
useWallRegionSelection.getState().setActive({ wallId: wall.id, regionId: region.id })
|
|
324
|
+
const activateTap = useHandleDrag({ kind: 'tap', onTap: select })
|
|
325
|
+
const onArrowHoverChange = (edge: RegionEdge, next: boolean) =>
|
|
326
|
+
setHoveredArrow((current) => (next ? edge : current === edge ? null : current))
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (hovered) {
|
|
330
|
+
setArrowsLinger(true)
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
const timer = window.setTimeout(() => setArrowsLinger(false), ARROW_HIDE_DELAY_MS)
|
|
334
|
+
return () => window.clearTimeout(timer)
|
|
335
|
+
}, [hovered])
|
|
336
|
+
|
|
337
|
+
useEffect(
|
|
338
|
+
() => () => {
|
|
339
|
+
if (document.body.style.cursor === 'pointer') document.body.style.cursor = ''
|
|
340
|
+
},
|
|
341
|
+
[],
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<>
|
|
346
|
+
<mesh
|
|
347
|
+
frustumCulled={false}
|
|
348
|
+
geometry={ribbon}
|
|
349
|
+
material={outlineMaterial(isActive || hovered)}
|
|
350
|
+
raycast={NO_RAYCAST}
|
|
351
|
+
renderOrder={1001}
|
|
352
|
+
/>
|
|
353
|
+
<InvisibleHandleHitArea
|
|
354
|
+
geometry={hitRibbon}
|
|
355
|
+
material={hitMaterial}
|
|
356
|
+
onPointerDown={activateTap}
|
|
357
|
+
onPointerEnter={(event) => {
|
|
358
|
+
event.stopPropagation()
|
|
359
|
+
setOutlineHovered(true)
|
|
360
|
+
document.body.style.cursor = 'pointer'
|
|
361
|
+
}}
|
|
362
|
+
onPointerLeave={(event) => {
|
|
363
|
+
event.stopPropagation()
|
|
364
|
+
setOutlineHovered(false)
|
|
365
|
+
if (document.body.style.cursor === 'pointer') document.body.style.cursor = ''
|
|
366
|
+
}}
|
|
367
|
+
scale={1}
|
|
368
|
+
/>
|
|
369
|
+
{showArrows &&
|
|
370
|
+
placements.map((placement) => (
|
|
371
|
+
<RegionEdgeHandle
|
|
372
|
+
frame={frame}
|
|
373
|
+
key={placement.edge}
|
|
374
|
+
onDraggingChange={setDraggingEdge}
|
|
375
|
+
onHoverChange={onArrowHoverChange}
|
|
376
|
+
onSelect={select}
|
|
377
|
+
placement={placement}
|
|
378
|
+
regionId={region.id}
|
|
379
|
+
/>
|
|
380
|
+
))}
|
|
381
|
+
</>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function RegionEdgeHandle({
|
|
386
|
+
frame,
|
|
387
|
+
regionId,
|
|
388
|
+
placement,
|
|
389
|
+
onSelect,
|
|
390
|
+
onDraggingChange,
|
|
391
|
+
onHoverChange,
|
|
392
|
+
}: {
|
|
393
|
+
frame: WallFrame
|
|
394
|
+
regionId: string
|
|
395
|
+
placement: RegionHandlePlacement
|
|
396
|
+
onSelect: () => void
|
|
397
|
+
onDraggingChange: (edge: RegionEdge | null) => void
|
|
398
|
+
onHoverChange: (edge: RegionEdge, hovered: boolean) => void
|
|
399
|
+
}) {
|
|
400
|
+
const { wall, levelObject } = frame
|
|
401
|
+
const [hovered, setHovered] = useState(false)
|
|
402
|
+
const [dragging, setDragging] = useState(false)
|
|
403
|
+
const { camera } = useThree()
|
|
404
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
405
|
+
const edge: RegionEdge = placement.edge
|
|
406
|
+
const cursor = edge[0] === 'u' ? 'ew-resize' : 'ns-resize'
|
|
407
|
+
|
|
408
|
+
const activate = useHandleDrag({
|
|
409
|
+
kind: 'drag',
|
|
410
|
+
cursor,
|
|
411
|
+
dragControls: NOOP_DRAG_CONTROLS,
|
|
412
|
+
handleIndex: 0,
|
|
413
|
+
node: wall,
|
|
414
|
+
rideObject: levelObject,
|
|
415
|
+
setIsDragging: (next) => {
|
|
416
|
+
setDragging(next)
|
|
417
|
+
onDraggingChange(next ? edge : null)
|
|
418
|
+
},
|
|
419
|
+
onStart: ({ event, getPointerRay, initialNode, nodeId, rideObject }) => {
|
|
420
|
+
if (initialNode.type !== 'wall') return null
|
|
421
|
+
const regions = initialNode.faceRegions ?? []
|
|
422
|
+
const region = regions.find((entry) => entry.id === regionId)
|
|
423
|
+
if (!region) return null
|
|
424
|
+
onSelect()
|
|
425
|
+
const nodes = useScene.getState().nodes
|
|
426
|
+
const face = region.face
|
|
427
|
+
const faceSurface = getWallFaceSurface(initialNode, face)
|
|
428
|
+
const baseAt = faceBaseSampler(initialNode.id, face)
|
|
429
|
+
const wallHeight = getWallEffectiveHeightForNodes(initialNode, nodes)
|
|
430
|
+
const faceHeight = regionFaceHeight(region, faceSurface.length, wallHeight, baseAt)
|
|
431
|
+
const bounds = resolveRegionBounds(region, faceSurface.length, faceHeight)
|
|
432
|
+
const midU = (bounds.u0 + bounds.u1) / 2
|
|
433
|
+
const mesh = sceneRegistry.nodes.get(initialNode.id) as Mesh | undefined
|
|
434
|
+
const runs = resolveWallFaceBase(initialNode, mesh?.geometry, nodes)?.[face] ?? null
|
|
435
|
+
const faceTargets = wallRegionSnapTargets(initialNode, face, nodes, runs, {
|
|
436
|
+
excludeRegionId: regionId,
|
|
437
|
+
})
|
|
438
|
+
const snapTargets = edge[0] === 'u' ? faceTargets.u : faceTargets.v
|
|
439
|
+
|
|
440
|
+
rideObject.updateWorldMatrix(true, false)
|
|
441
|
+
const toWallLocal = rideObject.matrixWorld
|
|
442
|
+
.clone()
|
|
443
|
+
.multiply(wallLocalMatrix(initialNode, getWallBaseElevationForNodes(initialNode, nodes)))
|
|
444
|
+
.invert()
|
|
445
|
+
const ray = new Ray()
|
|
446
|
+
const pointerValue = (worldRay: Ray) => {
|
|
447
|
+
ray.copy(worldRay).applyMatrix4(toWallLocal)
|
|
448
|
+
const hit = intersectFaceSurface(faceSurface, ray.origin, ray.direction)
|
|
449
|
+
if (!hit) return null
|
|
450
|
+
return edge[0] === 'u' ? hit.u : hit.y - baseAt(midU)
|
|
451
|
+
}
|
|
452
|
+
const initialValue = bounds[edge]
|
|
453
|
+
const initialPointer =
|
|
454
|
+
pointerValue(getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, ray)) ??
|
|
455
|
+
initialValue
|
|
456
|
+
let lastBounds: WallRegionBounds | null = null
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
onBegin: () => {
|
|
460
|
+
useInteractionScope
|
|
461
|
+
.getState()
|
|
462
|
+
.begin({ kind: 'handle-drag', nodeId, handle: PAINT_REGION_HANDLE })
|
|
463
|
+
},
|
|
464
|
+
onEnd: () => {
|
|
465
|
+
useInteractionScope.getState().endIf((scope) => isOwnScope(scope, nodeId))
|
|
466
|
+
},
|
|
467
|
+
move: ({ event: moveEvent, getPointerRay: getMoveRay }) => {
|
|
468
|
+
const value = pointerValue(getMoveRay(moveEvent.clientX, moveEvent.clientY, new Ray()))
|
|
469
|
+
if (value === null) return null
|
|
470
|
+
const next = resolveRegionBoundDrag({
|
|
471
|
+
region,
|
|
472
|
+
edge,
|
|
473
|
+
raw: initialValue + (value - initialPointer),
|
|
474
|
+
length: faceSurface.length,
|
|
475
|
+
faceHeight,
|
|
476
|
+
gridStep: isGridSnapActive() ? useEditor.getState().gridSnapStep : null,
|
|
477
|
+
edgeSnap: isMagneticSnapActive(),
|
|
478
|
+
snapTargets,
|
|
479
|
+
})
|
|
480
|
+
if (lastBounds && lastBounds[edge] !== next[edge]) sfxEmitter.emit('sfx:resize')
|
|
481
|
+
lastBounds = next
|
|
482
|
+
return { faceRegions: withRegionBounds(regions, regionId, next) } as Partial<AnyNode>
|
|
483
|
+
},
|
|
484
|
+
commit: () => {
|
|
485
|
+
if (lastBounds) updateWallRegion(nodeId, regionId, lastBounds)
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
<group rotation={[0, placement.yaw, 0]} position={placement.position}>
|
|
493
|
+
<HandleArrow
|
|
494
|
+
activeCursor={cursor}
|
|
495
|
+
cursor={cursor}
|
|
496
|
+
hover={hovered || dragging}
|
|
497
|
+
indicatorRotation={[Math.PI / 2, placement.tipAngle, 0]}
|
|
498
|
+
onHoverChange={(next) => {
|
|
499
|
+
setHovered(next)
|
|
500
|
+
onHoverChange(edge, next)
|
|
501
|
+
}}
|
|
502
|
+
onPointerDown={activate}
|
|
503
|
+
placement={{ position: [0, 0, 0], baseScale: zoom * ARROW_SCALE }}
|
|
504
|
+
shape="chevron"
|
|
505
|
+
thin
|
|
506
|
+
/>
|
|
507
|
+
</group>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
@@ -155,6 +155,9 @@ function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
|
|
|
155
155
|
wall.curveOffset ?? 0,
|
|
156
156
|
wall.thickness,
|
|
157
157
|
wall.supportSlabId,
|
|
158
|
+
undefined,
|
|
159
|
+
undefined,
|
|
160
|
+
wall.justification,
|
|
158
161
|
)
|
|
159
162
|
const planeTop = getWallPlaneTop(wall, levelId, nodes as Record<string, AnyNode>)
|
|
160
163
|
return resolveWallTop(wall, planeTop, support.elevation) + WALL_TOP_HIGHLIGHT_LIFT
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
4
|
import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
5
|
+
import { wallCursorIcon } from '../../lib/tool-cursor-icon'
|
|
5
6
|
import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
|
|
6
7
|
import { useFloorplanDraftPreview } from '../../store/use-floorplan-draft-preview'
|
|
7
8
|
import { furnishTools } from '../ui/action-menu/furnish-tools'
|
|
@@ -45,8 +46,8 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
45
46
|
}: FloorplanCursorIndicatorOverlayProps) {
|
|
46
47
|
const mode = useEditor((state) => state.mode)
|
|
47
48
|
const tool = useEditor((state) => state.tool)
|
|
48
|
-
const structureLayer = useEditor((state) => state.structureLayer)
|
|
49
49
|
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
50
|
+
const wallMode = useEditor((state) => state.continuationByContext.wall)
|
|
50
51
|
const cursorPoint = useFloorplanDraftPreview((state) => state.cursorPoint)
|
|
51
52
|
const anchorRef = useRef<HTMLDivElement>(null)
|
|
52
53
|
const [projectedCursorPosition, setProjectedCursorPosition] =
|
|
@@ -65,15 +66,18 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
65
66
|
return furnishTools.find((entry) => entry.catalogCategory === catalogCategory) ?? null
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// The wall shows its Rooms variant, like the 3D cursor bubble.
|
|
70
|
+
if (tool === 'wall') return wallCursorIcon(wallMode)
|
|
71
|
+
|
|
68
72
|
return structureTools.find((entry) => entry.id === tool) ?? null
|
|
69
|
-
}, [catalogCategory, mode, movingOpeningType, tool])
|
|
73
|
+
}, [catalogCategory, mode, movingOpeningType, tool, wallMode])
|
|
70
74
|
|
|
71
75
|
const indicator = useMemo<FloorplanCursorIndicator | null>(() => {
|
|
72
76
|
if (activeFloorplanToolConfig) {
|
|
73
77
|
return { kind: 'asset', iconSrc: activeFloorplanToolConfig.iconSrc }
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
if (mode === 'select' && floorplanSelectionTool === 'marquee'
|
|
80
|
+
if (mode === 'select' && floorplanSelectionTool === 'marquee') {
|
|
77
81
|
return { kind: 'icon', icon: 'mdi:select-drag' }
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -86,7 +90,7 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
return null
|
|
89
|
-
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode
|
|
93
|
+
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode])
|
|
90
94
|
|
|
91
95
|
useLayoutEffect(() => {
|
|
92
96
|
const anchor = anchorRef.current
|
|
@@ -37,8 +37,7 @@ import useInteractionScope, { useMovingNode } from '../../store/use-interaction-
|
|
|
37
37
|
import {
|
|
38
38
|
classifyParticipant,
|
|
39
39
|
collectParticipants,
|
|
40
|
-
|
|
41
|
-
expandToComponent,
|
|
40
|
+
computeGroupPlanBox,
|
|
42
41
|
type GroupPlanBounds,
|
|
43
42
|
groupPlanBounds,
|
|
44
43
|
levelFrame,
|
|
@@ -128,9 +127,8 @@ export function startFloorplanGroupMove(
|
|
|
128
127
|
const participantIds = selectedIds.filter(
|
|
129
128
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
130
129
|
)
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
130
|
+
// Only the selection moves; connected walls stretch through `links`, as in 3D.
|
|
131
|
+
const { starts, links } = collectParticipants(participantIds, nodes, levelId)
|
|
134
132
|
if (starts.length === 0) return null
|
|
135
133
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
136
134
|
|
|
@@ -144,10 +142,9 @@ export function startFloorplanGroupMove(
|
|
|
144
142
|
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
145
143
|
|
|
146
144
|
// The group aligns as one rigid footprint: its bbox corners + center are
|
|
147
|
-
// the moving anchors
|
|
148
|
-
// mounted under every view mode); plan coords are level-frame, so convert.
|
|
145
|
+
// the moving anchors, measured in the level frame like the plan itself.
|
|
149
146
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
150
|
-
const restBounds = groupPlanBounds(
|
|
147
|
+
const restBounds = groupPlanBounds(starts, frameInv)
|
|
151
148
|
if (!restBounds) return null
|
|
152
149
|
const restAnchors = bboxCornerAnchors(
|
|
153
150
|
'group-move',
|
|
@@ -441,12 +438,11 @@ export function startFloorplanGroupRotate(event: {
|
|
|
441
438
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
442
439
|
)
|
|
443
440
|
if (participantIds.length === 0) return false
|
|
444
|
-
const
|
|
445
|
-
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
441
|
+
const { starts, links } = collectParticipants(participantIds, nodes, levelId)
|
|
446
442
|
if (starts.length === 0) return false
|
|
447
443
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
448
444
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
449
|
-
const bounds = groupPlanBounds(
|
|
445
|
+
const bounds = groupPlanBounds(starts, frameInv)
|
|
450
446
|
if (!bounds) return false
|
|
451
447
|
// Same pivot as the dashed box the handles hang off (and as the 3D rotate
|
|
452
448
|
// gizmo): its centre, not the anchor points' centre.
|
|
@@ -605,8 +601,9 @@ const GROUP_BOX_ROTATE_CURSOR_STYLE = {
|
|
|
605
601
|
|
|
606
602
|
/**
|
|
607
603
|
* Dashed bounding box around the current multi-selection's transformable
|
|
608
|
-
* participants
|
|
609
|
-
*
|
|
604
|
+
* participants — what a group drag carries (connected walls outside the
|
|
605
|
+
* selection stretch at their shared ends to stay joined) — and IS the group's
|
|
606
|
+
* drag handle: press anywhere
|
|
610
607
|
* inside it to slide the group, click to pick it up. Holding a selection
|
|
611
608
|
* modifier (Cmd/Ctrl/Shift) lets pointer events pass through so members under
|
|
612
609
|
* the box can still be toggled in and out. Rides the live drag delta so it
|
|
@@ -653,7 +650,7 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
653
650
|
}, [])
|
|
654
651
|
|
|
655
652
|
// `meshEpoch` re-runs the measurement once the meshes settle after a scene
|
|
656
|
-
// change (undo/redo included) — `
|
|
653
|
+
// change (undo/redo included) — `computeGroupPlanBox` reads mesh bounds,
|
|
657
654
|
// which lag the `nodes` commit by a frame or two.
|
|
658
655
|
const meshEpoch = useMeshSettleEpoch(nodes)
|
|
659
656
|
const box = useMemo(() => {
|
|
@@ -663,17 +660,13 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
663
660
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
664
661
|
)
|
|
665
662
|
if (participantIds.length === 0) return null
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
if (!world) return null
|
|
669
|
-
const { inverse } = levelFrame(levelId)
|
|
670
|
-
const min = world.min.clone().applyMatrix4(inverse)
|
|
671
|
-
const max = world.max.clone().applyMatrix4(inverse)
|
|
663
|
+
const plan = computeGroupPlanBox(participantIds, levelId)
|
|
664
|
+
if (!plan) return null
|
|
672
665
|
return {
|
|
673
|
-
x:
|
|
674
|
-
z:
|
|
675
|
-
width:
|
|
676
|
-
depth:
|
|
666
|
+
x: plan.minX,
|
|
667
|
+
z: plan.minZ,
|
|
668
|
+
width: plan.maxX - plan.minX,
|
|
669
|
+
depth: plan.maxZ - plan.minZ,
|
|
677
670
|
}
|
|
678
671
|
}, [selectedIds, levelId, nodes, meshEpoch])
|
|
679
672
|
|