@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
getStoredLevelHeight,
|
|
6
|
+
type Point,
|
|
7
|
+
type SlabNode,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
upperFloorHeightControl,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { Html } from '@react-three/drei'
|
|
14
|
+
import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
15
|
+
import { useRef, useState } from 'react'
|
|
16
|
+
import { type Group, Plane, Vector2, Vector3 } from 'three'
|
|
17
|
+
import {
|
|
18
|
+
beginFootprintHeightPreview,
|
|
19
|
+
footprintHeightMinimum,
|
|
20
|
+
footprintHeightValue,
|
|
21
|
+
THICK_FLOOR_HINT,
|
|
22
|
+
thickFloorAdvice,
|
|
23
|
+
} from '../../lib/floor-footprints'
|
|
24
|
+
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
25
|
+
import { runRoomHandleDrag } from '../../lib/room-handle-drag'
|
|
26
|
+
import useEditor, { isGridSnapActive } from '../../store/use-editor'
|
|
27
|
+
import useInteractionScope from '../../store/use-interaction-scope'
|
|
28
|
+
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
29
|
+
import { ElevationArrows, outlineCentroid, useHandleScale } from './room-handles'
|
|
30
|
+
|
|
31
|
+
export const FOOTPRINT_HEIGHT_DRAG_LABEL = 'footprint-height'
|
|
32
|
+
|
|
33
|
+
const PILL =
|
|
34
|
+
'pointer-events-none flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur'
|
|
35
|
+
const NOTICE =
|
|
36
|
+
'pointer-events-none max-w-72 rounded-full border border-border/60 bg-background/95 px-3 py-1 text-center text-muted-foreground text-xs shadow-md backdrop-blur-md'
|
|
37
|
+
|
|
38
|
+
/** The selected footprint floor (base plate), when it is the one selection in select mode. */
|
|
39
|
+
function useSelectedFootprint(): SlabNode | null {
|
|
40
|
+
const selectedId = useViewer((s) =>
|
|
41
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
|
|
42
|
+
)
|
|
43
|
+
const enabled = useEditor((s) => s.mode === 'select')
|
|
44
|
+
const idle = useInteractionScope(
|
|
45
|
+
(s) =>
|
|
46
|
+
s.scope.kind === 'idle' ||
|
|
47
|
+
(s.scope.kind === 'handle-drag' && s.scope.handle === FOOTPRINT_HEIGHT_DRAG_LABEL),
|
|
48
|
+
)
|
|
49
|
+
const plate = useScene((s) => {
|
|
50
|
+
const node = selectedId ? s.nodes[selectedId as AnyNodeId] : undefined
|
|
51
|
+
return node?.type === 'slab' && node.plateRole === 'base' ? node : null
|
|
52
|
+
})
|
|
53
|
+
return enabled && idle ? plate : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A selected footprint floor's one control in 3D: the rooms' arrow–cube–arrow
|
|
58
|
+
* at the middle of the footprint, dragging its height above the ground. The
|
|
59
|
+
* building follows live; the release lands as one undo step. Refused heights
|
|
60
|
+
* (core's conflict, e.g. an upper floor over another footprint) show beside it.
|
|
61
|
+
*/
|
|
62
|
+
export function FootprintHeightHandle() {
|
|
63
|
+
const plate = useSelectedFootprint()
|
|
64
|
+
const root = useRef<Group>(null)
|
|
65
|
+
useFrame(() => {
|
|
66
|
+
const level = plate?.parentId ? sceneRegistry.nodes.get(plate.parentId) : null
|
|
67
|
+
if (!root.current) return
|
|
68
|
+
root.current.visible = !!level
|
|
69
|
+
if (level) {
|
|
70
|
+
level.updateWorldMatrix(true, false)
|
|
71
|
+
root.current.matrix.copy(level.matrixWorld)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
if (!plate) return null
|
|
75
|
+
return (
|
|
76
|
+
<group matrixAutoUpdate={false} ref={root}>
|
|
77
|
+
<FootprintHeightArrows plate={plate} />
|
|
78
|
+
</group>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function FootprintHeightArrows({ plate }: { plate: SlabNode }) {
|
|
83
|
+
const { camera, raycaster, gl } = useThree()
|
|
84
|
+
const zoom = useHandleScale()
|
|
85
|
+
const unit = useViewer((s) => s.unit)
|
|
86
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
87
|
+
const [hover, setHover] = useState(false)
|
|
88
|
+
const [dragValue, setDragValue] = useState<number | null>(null)
|
|
89
|
+
const [notice, setNotice] = useState<string | null>(null)
|
|
90
|
+
const [hint, setHint] = useState<string | null>(null)
|
|
91
|
+
const upper = useScene((s) => upperFloorHeightControl(s.nodes, plate) !== null)
|
|
92
|
+
const anchor: Point = outlineCentroid([plate.polygon as Point[], ...(plate.holes as Point[][])])
|
|
93
|
+
const elevation = plate.elevation
|
|
94
|
+
|
|
95
|
+
const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
|
|
96
|
+
if (event.button !== 0) return
|
|
97
|
+
event.stopPropagation()
|
|
98
|
+
suppressBoxSelectForPointer(event)
|
|
99
|
+
const levelId = plate.parentId
|
|
100
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
101
|
+
if (!(levelId && level)) return
|
|
102
|
+
level.updateWorldMatrix(true, false)
|
|
103
|
+
const toLocal = level.matrixWorld.clone().invert()
|
|
104
|
+
const world = new Vector3(anchor[0], elevation, anchor[1]).applyMatrix4(level.matrixWorld)
|
|
105
|
+
const normal = camera.getWorldPosition(new Vector3()).sub(world).setY(0)
|
|
106
|
+
if (normal.lengthSq() === 0) return
|
|
107
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(normal.normalize(), world)
|
|
108
|
+
const localY = (clientX: number, clientY: number) => {
|
|
109
|
+
const rect = gl.domElement.getBoundingClientRect()
|
|
110
|
+
raycaster.setFromCamera(
|
|
111
|
+
new Vector2(
|
|
112
|
+
((clientX - rect.left) / rect.width) * 2 - 1,
|
|
113
|
+
-((clientY - rect.top) / rect.height) * 2 + 1,
|
|
114
|
+
),
|
|
115
|
+
camera,
|
|
116
|
+
)
|
|
117
|
+
const hit = raycaster.ray.intersectPlane(plane, new Vector3())
|
|
118
|
+
return hit ? hit.applyMatrix4(toLocal).y : null
|
|
119
|
+
}
|
|
120
|
+
const startY = localY(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
121
|
+
if (startY === null) return
|
|
122
|
+
const nodes = () => useScene.getState().nodes
|
|
123
|
+
// Heights are read against the plate as the drag found it (the preview moves it).
|
|
124
|
+
const startNodes = nodes()
|
|
125
|
+
const start = footprintHeightValue(startNodes, plate)
|
|
126
|
+
const min = footprintHeightMinimum(startNodes, plate)
|
|
127
|
+
const levelNode = nodes()[levelId as AnyNodeId]
|
|
128
|
+
const max = levelNode?.type === 'level' ? getStoredLevelHeight(levelNode) - 0.5 : 3
|
|
129
|
+
const step = isGridSnapActive() ? 0.05 : 0.01
|
|
130
|
+
let value = start
|
|
131
|
+
const preview = beginFootprintHeightPreview(plate.id)
|
|
132
|
+
setNotice(null)
|
|
133
|
+
setDragValue(start)
|
|
134
|
+
runRoomHandleDrag({
|
|
135
|
+
label: FOOTPRINT_HEIGHT_DRAG_LABEL,
|
|
136
|
+
nodeId: plate.id,
|
|
137
|
+
levelId,
|
|
138
|
+
requires: [plate.id],
|
|
139
|
+
sample: (x, y) => {
|
|
140
|
+
const pointerY = localY(x, y)
|
|
141
|
+
if (pointerY === null) return null
|
|
142
|
+
const raw = start + pointerY - startY
|
|
143
|
+
return Math.max(min, Math.min(max, Math.round(raw / step) * step))
|
|
144
|
+
},
|
|
145
|
+
onValue: (next) => {
|
|
146
|
+
if (Math.abs(next - value) < 1e-6) return
|
|
147
|
+
// Live: the building follows, outside history until the release.
|
|
148
|
+
const refused = preview.preview(next)
|
|
149
|
+
setNotice(refused)
|
|
150
|
+
setHint(thickFloorAdvice(startNodes, plate, next) ? THICK_FLOOR_HINT : null)
|
|
151
|
+
if (!refused) value = next
|
|
152
|
+
setDragValue(next)
|
|
153
|
+
},
|
|
154
|
+
onCommit: () => {
|
|
155
|
+
setNotice(preview.commit(value))
|
|
156
|
+
setDragValue(null)
|
|
157
|
+
setHint(null)
|
|
158
|
+
},
|
|
159
|
+
onCancel: () => {
|
|
160
|
+
preview.cancel()
|
|
161
|
+
setDragValue(null)
|
|
162
|
+
setHint(null)
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const shown = dragValue ?? null
|
|
168
|
+
return (
|
|
169
|
+
<group>
|
|
170
|
+
<ElevationArrows
|
|
171
|
+
anchor={[anchor[0], elevation, anchor[1]]}
|
|
172
|
+
hover={hover || shown !== null}
|
|
173
|
+
onHoverChange={setHover}
|
|
174
|
+
onPointerDown={onPointerDown}
|
|
175
|
+
zoom={zoom}
|
|
176
|
+
/>
|
|
177
|
+
{(shown !== null || notice || hint) && (
|
|
178
|
+
<Html
|
|
179
|
+
center
|
|
180
|
+
position={[anchor[0], elevation + 0.75 * zoom, anchor[1]]}
|
|
181
|
+
zIndexRange={[25, 0]}
|
|
182
|
+
>
|
|
183
|
+
<div className="flex flex-col items-center gap-1" data-footprint-height-pill>
|
|
184
|
+
{shown !== null && (
|
|
185
|
+
<div className={PILL}>
|
|
186
|
+
<span className="font-medium text-foreground">
|
|
187
|
+
{upper ? 'Floor top' : 'Foundation'}
|
|
188
|
+
</span>
|
|
189
|
+
<span className="text-muted-foreground">
|
|
190
|
+
{formatLinearMeasurement(shown, unit, metricNotation)}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
{(notice ?? hint) && (
|
|
195
|
+
<div className={NOTICE} data-footprint-height-notice role="status">
|
|
196
|
+
{notice ?? hint}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</Html>
|
|
201
|
+
)}
|
|
202
|
+
</group>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -18,10 +18,13 @@ import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
|
18
18
|
import { Plane, Vector2, Vector3 } from 'three'
|
|
19
19
|
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
20
20
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
21
|
+
import { deleteSelectedSeparator, requestRoomDeletion } from '../../lib/room-structure-commands'
|
|
22
|
+
import { captureElementActionOrigin, completeElementAction } from '../../lib/room-zone-routing'
|
|
21
23
|
import {
|
|
22
24
|
copySelectedNodesToEditorClipboard,
|
|
23
25
|
duplicateNodesToLevel,
|
|
24
26
|
getEditorClipboardSnapshot,
|
|
27
|
+
type PasteRefusal,
|
|
25
28
|
pasteSystemEditorClipboardToLevel,
|
|
26
29
|
} from '../../lib/scene-clipboard'
|
|
27
30
|
import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
|
|
@@ -33,6 +36,7 @@ import useEditor, {
|
|
|
33
36
|
isGridSnapActive,
|
|
34
37
|
isMagneticSnapActive,
|
|
35
38
|
} from '../../store/use-editor'
|
|
39
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
36
40
|
import useInteractionScope from '../../store/use-interaction-scope'
|
|
37
41
|
import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
|
|
38
42
|
import {
|
|
@@ -456,12 +460,39 @@ function removeUnusedPasteMaterials(materialIds: SceneMaterialId[]) {
|
|
|
456
460
|
}
|
|
457
461
|
}
|
|
458
462
|
|
|
463
|
+
const REFUSED_PASTE_REASONS: Record<PasteRefusal, [one: string, many: string]> = {
|
|
464
|
+
'no-access': [
|
|
465
|
+
'you no longer have access to the project it came from.',
|
|
466
|
+
'you no longer have access to the project they came from.',
|
|
467
|
+
],
|
|
468
|
+
'no-copies': [
|
|
469
|
+
'this version is not available to copy from the source project.',
|
|
470
|
+
'these versions are not available to copy from the source project.',
|
|
471
|
+
],
|
|
472
|
+
failed: ["it couldn't be copied. Try again.", "they couldn't be copied. Try again."],
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Why scripted objects were left out of a paste, or of a build loaded from a file. */
|
|
476
|
+
export function refusedObjectsNotice(
|
|
477
|
+
count: number,
|
|
478
|
+
refusal: PasteRefusal,
|
|
479
|
+
action: 'pasted' | 'loaded' = 'pasted',
|
|
480
|
+
) {
|
|
481
|
+
const [one, many] = REFUSED_PASTE_REASONS[refusal]
|
|
482
|
+
return count === 1
|
|
483
|
+
? `1 object wasn't ${action}: ${one}`
|
|
484
|
+
: `${count} objects weren't ${action}: ${many}`
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let pastePending = false
|
|
488
|
+
|
|
459
489
|
/**
|
|
460
490
|
* Paste the Pascal scene payload from the browser clipboard onto the active
|
|
461
491
|
* level, then carry the clones under the cursor until click-to-place. Escape
|
|
462
492
|
* removes the uncommitted clones and any scene materials imported with them.
|
|
463
493
|
*/
|
|
464
494
|
export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promise<boolean> {
|
|
495
|
+
if (pastePending) return false
|
|
465
496
|
const activeScope = useInteractionScope.getState().scope
|
|
466
497
|
if (activeScope.kind === 'placing' || activeScope.kind === 'moving') {
|
|
467
498
|
emitter.emit('tool:cancel')
|
|
@@ -476,7 +507,18 @@ export async function pasteSelectionAndPickUp(targetLevelId?: AnyNodeId): Promis
|
|
|
476
507
|
// above there is nothing to abandon.
|
|
477
508
|
if (isBrushMode(useEditor.getState().mode)) useEditor.getState().setMode('select')
|
|
478
509
|
|
|
479
|
-
|
|
510
|
+
pastePending = true
|
|
511
|
+
let result: Awaited<ReturnType<typeof pasteSystemEditorClipboardToLevel>>
|
|
512
|
+
try {
|
|
513
|
+
result = await pasteSystemEditorClipboardToLevel(targetLevelId)
|
|
514
|
+
} finally {
|
|
515
|
+
pastePending = false
|
|
516
|
+
}
|
|
517
|
+
if (result?.refusal) {
|
|
518
|
+
useFloorplanMode
|
|
519
|
+
.getState()
|
|
520
|
+
.showNotice(refusedObjectsNotice(result.refusedIds.length, result.refusal))
|
|
521
|
+
}
|
|
480
522
|
if (!result || result.pastedIds.length === 0) return false
|
|
481
523
|
|
|
482
524
|
const discardPaste = () => {
|
|
@@ -547,7 +589,16 @@ export function cutSelectionToEditorClipboard(): boolean {
|
|
|
547
589
|
*/
|
|
548
590
|
export function deleteSelection(): boolean {
|
|
549
591
|
const selectedIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
550
|
-
if (selectedIds.length === 0)
|
|
592
|
+
if (selectedIds.length === 0) {
|
|
593
|
+
const room = useEditor.getState().room
|
|
594
|
+
if (!room) return false
|
|
595
|
+
requestRoomDeletion(room.zoneId)
|
|
596
|
+
return true
|
|
597
|
+
}
|
|
598
|
+
// A drilled piece of a room (a wall, a separator, its ceiling) deleted: back to the room.
|
|
599
|
+
const origin = captureElementActionOrigin(selectedIds)
|
|
600
|
+
if (selectedIds.length === 1 && useScene.getState().nodes[selectedIds[0]!]?.type === 'separator')
|
|
601
|
+
return deleteSelectedSeparator(selectedIds[0]!, origin)
|
|
551
602
|
|
|
552
603
|
const commitDelete = () => {
|
|
553
604
|
const detail =
|
|
@@ -562,6 +613,7 @@ export function deleteSelection(): boolean {
|
|
|
562
613
|
}
|
|
563
614
|
useScene.getState().deleteNodes(selectedIds)
|
|
564
615
|
useViewer.getState().setSelection({ selectedIds: [] })
|
|
616
|
+
completeElementAction(origin)
|
|
565
617
|
}
|
|
566
618
|
|
|
567
619
|
if (selectedIds.length >= BULK_DELETE_THRESHOLD) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { type RootState, useThree } from '@react-three/fiber'
|
|
3
|
+
import { create } from '@react-three/test-renderer'
|
|
4
|
+
import { useLayoutEffect } from 'react'
|
|
5
|
+
import { type Intersection, Object3D } from 'three'
|
|
6
|
+
import { EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY } from '../../../lib/direct-manipulation'
|
|
7
|
+
import { EditorHandleHitPriority, prioritizeEditorHandleHits } from './handle-hit-priority'
|
|
8
|
+
|
|
9
|
+
function hit(name: string, distance: number, handle = false): Intersection {
|
|
10
|
+
const object = new Object3D()
|
|
11
|
+
object.name = name
|
|
12
|
+
if (handle) object.userData[EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY] = true
|
|
13
|
+
return { object, distance, point: undefined as never }
|
|
14
|
+
}
|
|
15
|
+
const names = (hits: Intersection[]) => hits.map((entry) => entry.object.name)
|
|
16
|
+
|
|
17
|
+
test('a handle behind a wall comes first, so the hover goes where the press goes', () => {
|
|
18
|
+
const hits = [hit('wall', 3), hit('floor', 5), hit('up', 6, true), hit('cube', 6.2, true)]
|
|
19
|
+
expect(names(prioritizeEditorHandleHits(hits))).toEqual(['up', 'cube', 'wall', 'floor'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('without a handle on the ray the order is untouched', () => {
|
|
23
|
+
const hits = [hit('wall', 3), hit('floor', 5)]
|
|
24
|
+
expect(prioritizeEditorHandleHits(hits)).toBe(hits)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('mounted, it is the canvas event filter; unmounted, the previous one comes back', async () => {
|
|
28
|
+
let state: RootState | null = null
|
|
29
|
+
const Probe = () => {
|
|
30
|
+
state = useThree((root) => root)
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
const previous = (hits: Intersection[]) => hits.filter((entry) => entry.object.name !== 'grid')
|
|
34
|
+
const Previous = () => {
|
|
35
|
+
const setEvents = useThree((root) => root.setEvents)
|
|
36
|
+
useLayoutEffect(() => setEvents({ filter: previous }), [setEvents])
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
const before = await create(
|
|
40
|
+
<>
|
|
41
|
+
<Probe />
|
|
42
|
+
<Previous />
|
|
43
|
+
</>,
|
|
44
|
+
)
|
|
45
|
+
await before.update(
|
|
46
|
+
<>
|
|
47
|
+
<Probe />
|
|
48
|
+
<Previous />
|
|
49
|
+
<EditorHandleHitPriority />
|
|
50
|
+
</>,
|
|
51
|
+
)
|
|
52
|
+
const hits = [hit('grid', 1), hit('wall', 3), hit('cube', 6, true)]
|
|
53
|
+
const filter = (state as RootState | null)?.events.filter
|
|
54
|
+
expect(names(filter!(hits, state!))).toEqual(['cube', 'wall'])
|
|
55
|
+
await before.update(
|
|
56
|
+
<>
|
|
57
|
+
<Probe />
|
|
58
|
+
<Previous />
|
|
59
|
+
</>,
|
|
60
|
+
)
|
|
61
|
+
expect((state as RootState | null)?.events.filter).toBe(previous)
|
|
62
|
+
await before.unmount()
|
|
63
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useThree } from '@react-three/fiber'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import type { Intersection, Object3D } from 'three'
|
|
6
|
+
import { EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY } from '../../../lib/direct-manipulation'
|
|
7
|
+
|
|
8
|
+
const isHandleHit = (hit: { object: Object3D }) =>
|
|
9
|
+
hit.object.userData?.[EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY] === true
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Editor handles first, everything else after, each in distance order. Handles
|
|
13
|
+
* draw on top of walls and a handle anywhere along the ray already owns the
|
|
14
|
+
* press; ordering them first gives them the hover too. Distance order alone
|
|
15
|
+
* lets a wall in front win the hover for good: once its enter stops
|
|
16
|
+
* propagation, R3F keeps stopping every later move at it, so a handle behind
|
|
17
|
+
* it never hears its enter however the pointer reaches it.
|
|
18
|
+
*/
|
|
19
|
+
export function prioritizeEditorHandleHits<T extends { object: Object3D }>(hits: T[]): T[] {
|
|
20
|
+
if (!hits.some(isHandleHit)) return hits
|
|
21
|
+
return [...hits.filter(isHandleHit), ...hits.filter((hit) => !isHandleHit(hit))]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Installs `prioritizeEditorHandleHits` as the canvas's pointer-event filter while mounted. */
|
|
25
|
+
export function EditorHandleHitPriority() {
|
|
26
|
+
const get = useThree((state) => state.get)
|
|
27
|
+
const setEvents = useThree((state) => state.setEvents)
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const previous = get().events.filter
|
|
30
|
+
setEvents({
|
|
31
|
+
filter: (hits: Intersection[], state) =>
|
|
32
|
+
prioritizeEditorHandleHits(previous ? previous(hits, state) : hits),
|
|
33
|
+
})
|
|
34
|
+
return () => setEvents({ filter: previous })
|
|
35
|
+
}, [get, setEvents])
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import { swallowNextClick } from './use-handle-drag'
|
|
3
|
+
|
|
4
|
+
test('deferred click cleanup removes the listener from its original window', () => {
|
|
5
|
+
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
6
|
+
const target = new EventTarget()
|
|
7
|
+
const remove = spyOn(target, 'removeEventListener')
|
|
8
|
+
let cleanup: (() => void) | undefined
|
|
9
|
+
const timer = spyOn(globalThis, 'setTimeout').mockImplementation(((
|
|
10
|
+
callback: Parameters<typeof setTimeout>[0],
|
|
11
|
+
) => {
|
|
12
|
+
cleanup = callback as () => void
|
|
13
|
+
return undefined as unknown as ReturnType<typeof setTimeout>
|
|
14
|
+
}) as typeof setTimeout)
|
|
15
|
+
try {
|
|
16
|
+
Object.defineProperty(globalThis, 'window', { configurable: true, value: target })
|
|
17
|
+
swallowNextClick()
|
|
18
|
+
Reflect.deleteProperty(globalThis, 'window')
|
|
19
|
+
expect(cleanup).toBeDefined()
|
|
20
|
+
expect(() => cleanup?.()).not.toThrow()
|
|
21
|
+
expect(remove).toHaveBeenCalledTimes(1)
|
|
22
|
+
} finally {
|
|
23
|
+
timer.mockRestore()
|
|
24
|
+
remove.mockRestore()
|
|
25
|
+
if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow)
|
|
26
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
27
|
+
}
|
|
28
|
+
})
|