@pascal-app/editor 0.9.1 → 1.0.0-beta.1
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 +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +188 -0
- package/src/lib/measurements.ts +210 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
|
|
2
2
|
import { SCENE_LAYER, useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { createPortal, type ThreeEvent } from '@react-three/fiber'
|
|
3
|
+
import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
|
|
4
4
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
import {
|
|
6
|
+
BoxGeometry,
|
|
6
7
|
BufferGeometry,
|
|
7
8
|
Color,
|
|
8
9
|
CylinderGeometry,
|
|
10
|
+
DoubleSide,
|
|
9
11
|
ExtrudeGeometry,
|
|
10
12
|
Float32BufferAttribute,
|
|
11
13
|
type Line,
|
|
12
14
|
type Object3D,
|
|
13
15
|
Shape,
|
|
16
|
+
Vector3,
|
|
14
17
|
} from 'three'
|
|
15
18
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
16
19
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
@@ -20,10 +23,10 @@ import {
|
|
|
20
23
|
ARROW_COLOR as EDGE_ARROW_COLOR,
|
|
21
24
|
ARROW_HOVER_COLOR as EDGE_ARROW_HOVER_COLOR,
|
|
22
25
|
ARROW_SCALE as EDGE_ARROW_SCALE,
|
|
23
|
-
useArrowMaterial,
|
|
24
26
|
useInvisibleHitAreaMaterial,
|
|
25
27
|
} from '../../editor/node-arrow-handles'
|
|
26
28
|
import { snapToHalf } from '../item/placement-math'
|
|
29
|
+
import { suppressBoxSelectForPointer } from '../select/box-select-state'
|
|
27
30
|
|
|
28
31
|
const Y_OFFSET = 0.02
|
|
29
32
|
// Per-side resize arrows: indigo chevrons that match the registry arrow
|
|
@@ -39,6 +42,19 @@ const EDGE_ARROW_OFFSET = 0.34
|
|
|
39
42
|
// the `NO_RAYCAST` sentinel in node-arrow-handles.tsx.
|
|
40
43
|
const NO_RAYCAST = () => null
|
|
41
44
|
|
|
45
|
+
// Maps the screen-space direction of an edge's outward normal to the closest
|
|
46
|
+
// directional resize cursor, so an edge arrow advertises the axis it will
|
|
47
|
+
// actually drag along regardless of camera orbit. Input is an NDC delta
|
|
48
|
+
// (y up); the angle is folded to [0°, 180°) since resize cursors are
|
|
49
|
+
// bidirectional.
|
|
50
|
+
function resizeCursorForScreenDirection(dx: number, dy: number): string {
|
|
51
|
+
const angle = ((((Math.atan2(dy, dx) * 180) / Math.PI) % 180) + 180) % 180
|
|
52
|
+
if (angle < 22.5 || angle >= 157.5) return 'ew-resize'
|
|
53
|
+
if (angle < 67.5) return 'nesw-resize'
|
|
54
|
+
if (angle < 112.5) return 'ns-resize'
|
|
55
|
+
return 'nwse-resize'
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
function createEdgeArrowGeometry() {
|
|
43
59
|
const shape = new Shape()
|
|
44
60
|
shape.moveTo(0.22, 0)
|
|
@@ -77,6 +93,17 @@ type DragState = {
|
|
|
77
93
|
pointerId: number
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
export type PolygonEditorPlanPointSnapContext = {
|
|
97
|
+
rawPoint: [number, number]
|
|
98
|
+
gridPoint: [number, number]
|
|
99
|
+
mode: DragState['mode']
|
|
100
|
+
vertexIndex: number | null
|
|
101
|
+
edgeIndex?: number
|
|
102
|
+
initialPosition: [number, number]
|
|
103
|
+
initialPolygon: Array<[number, number]>
|
|
104
|
+
nativeEvent?: GridEvent['nativeEvent']
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
export interface PolygonEditorProps {
|
|
81
108
|
polygon: Array<[number, number]>
|
|
82
109
|
color?: string
|
|
@@ -104,6 +131,8 @@ export interface PolygonEditorProps {
|
|
|
104
131
|
onVertexHoverChange?: (vertexIndex: number | null) => void
|
|
105
132
|
/** Called when a midpoint add-vertex handle enters or leaves hover. */
|
|
106
133
|
onMidpointHoverChange?: (edgeIndex: number | null) => void
|
|
134
|
+
/** Called when an edge move handle enters or leaves hover. */
|
|
135
|
+
onEdgeHoverChange?: (edgeIndex: number | null) => void
|
|
107
136
|
/** Called when any polygon drag starts or ends. */
|
|
108
137
|
onDragStateChange?: (isDragging: boolean) => void
|
|
109
138
|
/** Called once when a polygon drag starts. */
|
|
@@ -114,6 +143,10 @@ export interface PolygonEditorProps {
|
|
|
114
143
|
showBorderLine?: boolean
|
|
115
144
|
/** Whether midpoint handles can add new vertices. */
|
|
116
145
|
showMidpointHandles?: boolean
|
|
146
|
+
/** Whether hovering a handle should also tint its connected edges and endpoint handles. */
|
|
147
|
+
highlightConnectedHandles?: boolean
|
|
148
|
+
/** Optional host-owned point snapper. Defaults to the existing half-grid snap. */
|
|
149
|
+
resolvePlanPoint?: (context: PolygonEditorPlanPointSnapContext) => [number, number]
|
|
117
150
|
/** Optional vertex handle renderer for host-specific affordances. */
|
|
118
151
|
renderVertexHandle?: PolygonVertexHandleRenderer
|
|
119
152
|
/** Optional midpoint handle renderer for host-specific add-vertex affordances. */
|
|
@@ -127,6 +160,7 @@ export interface PolygonEditorProps {
|
|
|
127
160
|
const MIN_HANDLE_HEIGHT = 0.15
|
|
128
161
|
const EDGE_HANDLE_HEIGHT = 0.06
|
|
129
162
|
const EDGE_HANDLE_THICKNESS = 0.12
|
|
163
|
+
const EDGE_HANDLE_GEOMETRY = new BoxGeometry(1, 1, 1)
|
|
130
164
|
|
|
131
165
|
function getEdgeNormal(start: [number, number], end: [number, number]): [number, number] | null {
|
|
132
166
|
const dx = end[0] - start[0]
|
|
@@ -137,6 +171,11 @@ function getEdgeNormal(start: [number, number], end: [number, number]): [number,
|
|
|
137
171
|
return [-dz / length, dx / length]
|
|
138
172
|
}
|
|
139
173
|
|
|
174
|
+
function stopHandlePointerDown(event: ThreeEvent<PointerEvent>) {
|
|
175
|
+
event.stopPropagation()
|
|
176
|
+
suppressBoxSelectForPointer(event, { markHandled: false })
|
|
177
|
+
}
|
|
178
|
+
|
|
140
179
|
type HandleClickHandler = (event: ThreeEvent<MouseEvent>) => void
|
|
141
180
|
type HandlePointerHandler = (event: ThreeEvent<PointerEvent>) => void
|
|
142
181
|
|
|
@@ -176,12 +215,16 @@ export type PolygonMidpointHandleRenderer = (
|
|
|
176
215
|
props: PolygonMidpointHandleRenderProps,
|
|
177
216
|
) => React.ReactNode
|
|
178
217
|
|
|
179
|
-
function usePolygonNodeMaterial(
|
|
218
|
+
function usePolygonNodeMaterial(
|
|
219
|
+
color: string,
|
|
220
|
+
opacity = 1,
|
|
221
|
+
depthTest = true,
|
|
222
|
+
): MeshBasicNodeMaterial {
|
|
180
223
|
const material = useMemo(
|
|
181
224
|
() =>
|
|
182
225
|
new MeshBasicNodeMaterial({
|
|
183
226
|
color: new Color('#ffffff'),
|
|
184
|
-
depthTest:
|
|
227
|
+
depthTest: true,
|
|
185
228
|
depthWrite: true,
|
|
186
229
|
opacity: 1,
|
|
187
230
|
transparent: true,
|
|
@@ -192,17 +235,31 @@ function usePolygonNodeMaterial(color: string, opacity = 1): MeshBasicNodeMateri
|
|
|
192
235
|
useEffect(() => {
|
|
193
236
|
material.color.set(color)
|
|
194
237
|
material.opacity = opacity
|
|
195
|
-
|
|
238
|
+
material.depthTest = depthTest
|
|
239
|
+
}, [color, depthTest, material, opacity])
|
|
196
240
|
useEffect(() => () => material.dispose(), [material])
|
|
197
241
|
|
|
198
242
|
return material
|
|
199
243
|
}
|
|
200
244
|
|
|
245
|
+
function usePolygonArrowMaterial(): MeshBasicNodeMaterial {
|
|
246
|
+
return useMemo(
|
|
247
|
+
() =>
|
|
248
|
+
new MeshBasicNodeMaterial({
|
|
249
|
+
color: new Color(EDGE_ARROW_COLOR),
|
|
250
|
+
depthTest: false,
|
|
251
|
+
depthWrite: true,
|
|
252
|
+
opacity: 1,
|
|
253
|
+
side: DoubleSide,
|
|
254
|
+
transparent: true,
|
|
255
|
+
}),
|
|
256
|
+
[],
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
201
260
|
// One mesh per handle: lives on SCENE_LAYER with a node material so the
|
|
202
|
-
// post-processing ink-edge pass outlines it
|
|
203
|
-
//
|
|
204
|
-
// node-arrow-handles.tsx. No paired hit mesh is needed; the R3F event
|
|
205
|
-
// raycaster picks SCENE_LAYER meshes too.
|
|
261
|
+
// post-processing ink-edge pass outlines it. Ignores scene depth like the
|
|
262
|
+
// edge arrows so the handle stays visible through walls and slabs.
|
|
206
263
|
function OutlinedCylinderHandle({
|
|
207
264
|
radius,
|
|
208
265
|
height,
|
|
@@ -218,7 +275,7 @@ function OutlinedCylinderHandle({
|
|
|
218
275
|
position: [number, number, number]
|
|
219
276
|
} & PolygonHandleHandlers) {
|
|
220
277
|
const geometry = useMemo(() => new CylinderGeometry(radius, radius, height, 16), [height, radius])
|
|
221
|
-
const material = usePolygonNodeMaterial(color, opacity)
|
|
278
|
+
const material = usePolygonNodeMaterial(color, opacity, false)
|
|
222
279
|
useEffect(() => () => geometry.dispose(), [geometry])
|
|
223
280
|
|
|
224
281
|
return (
|
|
@@ -290,7 +347,7 @@ function OutlinedEdgeArrowHandle({
|
|
|
290
347
|
rotationY: number
|
|
291
348
|
scale: number
|
|
292
349
|
} & PolygonHandleHandlers) {
|
|
293
|
-
const material =
|
|
350
|
+
const material = usePolygonArrowMaterial()
|
|
294
351
|
useEffect(() => {
|
|
295
352
|
material.color.set(color)
|
|
296
353
|
}, [color, material])
|
|
@@ -311,6 +368,47 @@ function OutlinedEdgeArrowHandle({
|
|
|
311
368
|
)
|
|
312
369
|
}
|
|
313
370
|
|
|
371
|
+
function HighlightedEdgeSegment({
|
|
372
|
+
end,
|
|
373
|
+
start,
|
|
374
|
+
y,
|
|
375
|
+
}: {
|
|
376
|
+
end: [number, number]
|
|
377
|
+
start: [number, number]
|
|
378
|
+
y: number
|
|
379
|
+
}) {
|
|
380
|
+
const geometry = useMemo(() => {
|
|
381
|
+
const nextGeometry = new BufferGeometry()
|
|
382
|
+
nextGeometry.setAttribute(
|
|
383
|
+
'position',
|
|
384
|
+
new Float32BufferAttribute([start[0], y, start[1], end[0], y, end[1]], 3),
|
|
385
|
+
)
|
|
386
|
+
return nextGeometry
|
|
387
|
+
}, [end, start, y])
|
|
388
|
+
|
|
389
|
+
useEffect(() => () => geometry.dispose(), [geometry])
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<line
|
|
393
|
+
// @ts-expect-error R3F <line> element conflicts with SVG <line> type
|
|
394
|
+
frustumCulled={false}
|
|
395
|
+
geometry={geometry}
|
|
396
|
+
layers={EDITOR_LAYER}
|
|
397
|
+
raycast={NO_RAYCAST}
|
|
398
|
+
renderOrder={12}
|
|
399
|
+
>
|
|
400
|
+
<lineBasicNodeMaterial
|
|
401
|
+
color={EDGE_ARROW_HOVER_COLOR}
|
|
402
|
+
depthTest
|
|
403
|
+
depthWrite={false}
|
|
404
|
+
linewidth={4}
|
|
405
|
+
opacity={0.95}
|
|
406
|
+
transparent
|
|
407
|
+
/>
|
|
408
|
+
</line>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
314
412
|
export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
315
413
|
polygon,
|
|
316
414
|
color = '#3b82f6',
|
|
@@ -324,11 +422,14 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
324
422
|
onBeforeVertexDrag,
|
|
325
423
|
onVertexHoverChange,
|
|
326
424
|
onMidpointHoverChange,
|
|
425
|
+
onEdgeHoverChange,
|
|
327
426
|
onDragStateChange,
|
|
328
427
|
onDragStart,
|
|
329
428
|
onDragCommit,
|
|
330
429
|
showBorderLine = true,
|
|
331
430
|
showMidpointHandles = true,
|
|
431
|
+
highlightConnectedHandles = false,
|
|
432
|
+
resolvePlanPoint,
|
|
332
433
|
renderMidpointHandle,
|
|
333
434
|
renderVertexHandle,
|
|
334
435
|
}) => {
|
|
@@ -371,6 +472,39 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
371
472
|
// When not using portal, edit at world origin
|
|
372
473
|
const editY = levelNode ? Y_OFFSET : 0
|
|
373
474
|
|
|
475
|
+
const camera = useThree((state) => state.camera)
|
|
476
|
+
|
|
477
|
+
// Handle-hover cursor, applied to document.body like the registry arrow
|
|
478
|
+
// handles (see handle-arrow.tsx). Tracked in a ref so we only ever clear a
|
|
479
|
+
// cursor we set ourselves.
|
|
480
|
+
const activeBodyCursorRef = useRef<string | null>(null)
|
|
481
|
+
const setBodyCursor = useCallback((cursor: string) => {
|
|
482
|
+
activeBodyCursorRef.current = cursor
|
|
483
|
+
document.body.style.cursor = cursor
|
|
484
|
+
}, [])
|
|
485
|
+
const clearBodyCursor = useCallback(() => {
|
|
486
|
+
if (activeBodyCursorRef.current && document.body.style.cursor === activeBodyCursorRef.current) {
|
|
487
|
+
document.body.style.cursor = ''
|
|
488
|
+
}
|
|
489
|
+
activeBodyCursorRef.current = null
|
|
490
|
+
}, [])
|
|
491
|
+
useEffect(() => () => clearBodyCursor(), [clearBodyCursor])
|
|
492
|
+
|
|
493
|
+
const edgeResizeCursor = useCallback(
|
|
494
|
+
(midpoint: [number, number], outwardNormal: [number, number]) => {
|
|
495
|
+
const from = new Vector3(midpoint[0], editY, midpoint[1])
|
|
496
|
+
const to = new Vector3(midpoint[0] + outwardNormal[0], editY, midpoint[1] + outwardNormal[1])
|
|
497
|
+
if (levelNode) {
|
|
498
|
+
levelNode.localToWorld(from)
|
|
499
|
+
levelNode.localToWorld(to)
|
|
500
|
+
}
|
|
501
|
+
from.project(camera)
|
|
502
|
+
to.project(camera)
|
|
503
|
+
return resizeCursorForScreenDirection(to.x - from.x, to.y - from.y)
|
|
504
|
+
},
|
|
505
|
+
[camera, editY, levelNode],
|
|
506
|
+
)
|
|
507
|
+
|
|
374
508
|
// Local state for dragging
|
|
375
509
|
const [dragState, setDragState] = useState<DragState | null>(null)
|
|
376
510
|
const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]> | null>(null)
|
|
@@ -429,6 +563,12 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
429
563
|
|
|
430
564
|
useEffect(() => () => onMidpointHoverChange?.(null), [onMidpointHoverChange])
|
|
431
565
|
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
onEdgeHoverChange?.(hoveredEdge)
|
|
568
|
+
}, [hoveredEdge, onEdgeHoverChange])
|
|
569
|
+
|
|
570
|
+
useEffect(() => () => onEdgeHoverChange?.(null), [onEdgeHoverChange])
|
|
571
|
+
|
|
432
572
|
const lineRef = useRef<Line>(null!)
|
|
433
573
|
const previousPositionRef = useRef<[number, number] | null>(null)
|
|
434
574
|
|
|
@@ -556,6 +696,47 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
556
696
|
})
|
|
557
697
|
}, [displayPolygon])
|
|
558
698
|
|
|
699
|
+
const activeVertexIndex = dragState?.mode === 'vertex' ? dragState.vertexIndex : hoveredVertex
|
|
700
|
+
const activeEdgeIndex = dragState?.mode === 'edge' ? dragState.edgeIndex : hoveredEdge
|
|
701
|
+
|
|
702
|
+
const highlightedEdgeIndices = useMemo(() => {
|
|
703
|
+
const next = new Set<number>()
|
|
704
|
+
const edgeCount = displayPolygon.length
|
|
705
|
+
if (!highlightConnectedHandles || edgeCount < 2) return next
|
|
706
|
+
|
|
707
|
+
if (activeVertexIndex !== null && activeVertexIndex !== undefined) {
|
|
708
|
+
next.add(activeVertexIndex)
|
|
709
|
+
next.add((activeVertexIndex - 1 + edgeCount) % edgeCount)
|
|
710
|
+
}
|
|
711
|
+
if (hoveredMidpoint !== null) {
|
|
712
|
+
next.add(hoveredMidpoint)
|
|
713
|
+
}
|
|
714
|
+
if (activeEdgeIndex !== null && activeEdgeIndex !== undefined) {
|
|
715
|
+
next.add(activeEdgeIndex)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return next
|
|
719
|
+
}, [
|
|
720
|
+
activeEdgeIndex,
|
|
721
|
+
activeVertexIndex,
|
|
722
|
+
displayPolygon.length,
|
|
723
|
+
highlightConnectedHandles,
|
|
724
|
+
hoveredMidpoint,
|
|
725
|
+
])
|
|
726
|
+
|
|
727
|
+
const isVertexLinkedHighlighted = useCallback(
|
|
728
|
+
(index: number) => {
|
|
729
|
+
if (!highlightConnectedHandles || highlightedEdgeIndices.size === 0) return false
|
|
730
|
+
const edgeCount = displayPolygon.length
|
|
731
|
+
if (edgeCount < 2) return false
|
|
732
|
+
return (
|
|
733
|
+
highlightedEdgeIndices.has(index) ||
|
|
734
|
+
highlightedEdgeIndices.has((index - 1 + edgeCount) % edgeCount)
|
|
735
|
+
)
|
|
736
|
+
},
|
|
737
|
+
[displayPolygon.length, highlightConnectedHandles, highlightedEdgeIndices],
|
|
738
|
+
)
|
|
739
|
+
|
|
559
740
|
const arrowGeometry = useMemo(() => createEdgeArrowGeometry(), [])
|
|
560
741
|
useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
|
|
561
742
|
|
|
@@ -578,7 +759,8 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
578
759
|
onDragCommitRef.current?.()
|
|
579
760
|
updatePreviewPolygon(null)
|
|
580
761
|
setDragState(null)
|
|
581
|
-
|
|
762
|
+
clearBodyCursor()
|
|
763
|
+
}, [clearBodyCursor, onPolygonChange, updatePreviewPolygon])
|
|
582
764
|
|
|
583
765
|
// Handle adding a new vertex at midpoint
|
|
584
766
|
const handleAddVertex = useCallback(
|
|
@@ -616,9 +798,23 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
616
798
|
useEffect(() => {
|
|
617
799
|
const onGridMove = (event: GridEvent) => {
|
|
618
800
|
const point = levelNode ? event.localPosition : event.position
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
801
|
+
const rawPoint: [number, number] = [point[0], point[2]]
|
|
802
|
+
// Snapping follows the active mode (snapToHalf returns raw in Off / non-grid);
|
|
803
|
+
// no Shift bypass — Shift cycles the mode, Off is the bypass.
|
|
804
|
+
const gridPoint: [number, number] = [snapToHalf(rawPoint[0]), snapToHalf(rawPoint[1])]
|
|
805
|
+
const newPosition =
|
|
806
|
+
dragState?.isDragging && resolvePlanPoint
|
|
807
|
+
? resolvePlanPoint({
|
|
808
|
+
rawPoint,
|
|
809
|
+
gridPoint,
|
|
810
|
+
mode: dragState.mode,
|
|
811
|
+
vertexIndex: dragState.vertexIndex,
|
|
812
|
+
edgeIndex: dragState.edgeIndex,
|
|
813
|
+
initialPosition: dragState.initialPosition,
|
|
814
|
+
initialPolygon: dragState.initialPolygon,
|
|
815
|
+
nativeEvent: event.nativeEvent,
|
|
816
|
+
})
|
|
817
|
+
: gridPoint
|
|
622
818
|
|
|
623
819
|
// Play snap sound when cursor moves to a new grid cell during drag
|
|
624
820
|
if (
|
|
@@ -673,7 +869,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
673
869
|
return () => {
|
|
674
870
|
emitter.off('grid:move', onGridMove)
|
|
675
871
|
}
|
|
676
|
-
}, [dragState, handleVertexDrag, levelNode, updatePreviewPolygon])
|
|
872
|
+
}, [dragState, handleVertexDrag, levelNode, resolvePlanPoint, updatePreviewPolygon])
|
|
677
873
|
|
|
678
874
|
// Set up pointer up listener for ending drag
|
|
679
875
|
useEffect(() => {
|
|
@@ -741,9 +937,10 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
741
937
|
const handleHeight = Math.max(MIN_HANDLE_HEIGHT, surfaceHeight + 0.02)
|
|
742
938
|
const edgeHandleY = editY + handleHeight - EDGE_HANDLE_HEIGHT / 2
|
|
743
939
|
|
|
744
|
-
// Interactive handles are
|
|
745
|
-
//
|
|
746
|
-
//
|
|
940
|
+
// Interactive handles are SCENE_LAYER node-material meshes so the ink-edge
|
|
941
|
+
// pass outlines them. Edge arrows ignore scene depth so the active resize
|
|
942
|
+
// affordance remains visible through walls and slabs. The edge BAR and border
|
|
943
|
+
// line stay on EDITOR_LAYER, visual-only
|
|
747
944
|
// (raycast disabled) so they never steal clicks from the vertex/midpoint
|
|
748
945
|
// handles overlapping them — edge dragging starts from the chevron arrow
|
|
749
946
|
// outside the polygon edge.
|
|
@@ -771,11 +968,32 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
771
968
|
</line>
|
|
772
969
|
)}
|
|
773
970
|
|
|
774
|
-
{
|
|
971
|
+
{highlightConnectedHandles &&
|
|
972
|
+
highlightedEdgeIndices.size > 0 &&
|
|
973
|
+
Array.from(highlightedEdgeIndices).map((edgeIndex) => {
|
|
974
|
+
const start = displayPolygon[edgeIndex]
|
|
975
|
+
const end = displayPolygon[(edgeIndex + 1) % displayPolygon.length]
|
|
976
|
+
if (!(start && end)) return null
|
|
977
|
+
return (
|
|
978
|
+
<HighlightedEdgeSegment
|
|
979
|
+
end={end}
|
|
980
|
+
key={`highlight-edge-${edgeIndex}`}
|
|
981
|
+
start={start}
|
|
982
|
+
y={edgeHandleY}
|
|
983
|
+
/>
|
|
984
|
+
)
|
|
985
|
+
})}
|
|
986
|
+
|
|
987
|
+
{/* Vertex handles - blue cylinders that match surface height. During a
|
|
988
|
+
drag only the active handle stays mounted so the gesture reads
|
|
989
|
+
clearly (same rule for the cross / edge handles below). */}
|
|
775
990
|
{displayPolygon.map(([x, z], index) => {
|
|
776
991
|
const isHovered = hoveredVertex === index
|
|
777
992
|
const isDragging = dragState?.mode === 'vertex' && dragState.vertexIndex === index
|
|
778
|
-
|
|
993
|
+
if (dragState && !isDragging) return null
|
|
994
|
+
const isLinkedHighlighted = isVertexLinkedHighlighted(index)
|
|
995
|
+
const isHighlighted = isDragging || isHovered || isLinkedHighlighted
|
|
996
|
+
const radius = 0.08
|
|
779
997
|
const height = handleHeight
|
|
780
998
|
const point: [number, number] = [x!, z!]
|
|
781
999
|
const position: [number, number, number] = [x!, editY + height / 2, z!]
|
|
@@ -793,7 +1011,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
793
1011
|
},
|
|
794
1012
|
onPointerDown: (e) => {
|
|
795
1013
|
if (e.button !== 0) return
|
|
796
|
-
e
|
|
1014
|
+
stopHandlePointerDown(e)
|
|
797
1015
|
setHoveredEdge(null)
|
|
798
1016
|
onBeforeVertexDrag?.(index, point)
|
|
799
1017
|
startDrag({
|
|
@@ -808,10 +1026,14 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
808
1026
|
onPointerEnter: (e) => {
|
|
809
1027
|
e.stopPropagation()
|
|
810
1028
|
setHoveredVertex(index)
|
|
1029
|
+
setBodyCursor('move')
|
|
811
1030
|
},
|
|
812
1031
|
onPointerLeave: (e) => {
|
|
813
1032
|
e.stopPropagation()
|
|
814
1033
|
setHoveredVertex(null)
|
|
1034
|
+
// Keep the cursor for the whole gesture when the pointer slips
|
|
1035
|
+
// off the handle mid-drag; commit clears it.
|
|
1036
|
+
if (!dragState?.isDragging) clearBodyCursor()
|
|
815
1037
|
},
|
|
816
1038
|
}
|
|
817
1039
|
|
|
@@ -835,7 +1057,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
835
1057
|
|
|
836
1058
|
return (
|
|
837
1059
|
<OutlinedCylinderHandle
|
|
838
|
-
color={
|
|
1060
|
+
color={isHighlighted ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
|
|
839
1061
|
height={height}
|
|
840
1062
|
key={`vertex-${index}`}
|
|
841
1063
|
{...handleProps}
|
|
@@ -845,7 +1067,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
845
1067
|
)
|
|
846
1068
|
})}
|
|
847
1069
|
|
|
848
|
-
{allowPolygonMove && (
|
|
1070
|
+
{allowPolygonMove && (!dragState || dragState.mode === 'polygon') && (
|
|
849
1071
|
<OutlinedCrossHandle
|
|
850
1072
|
color={dragState?.mode === 'polygon' ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
|
|
851
1073
|
onClick={(e) => {
|
|
@@ -854,7 +1076,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
854
1076
|
}}
|
|
855
1077
|
onPointerDown={(e) => {
|
|
856
1078
|
if (e.button !== 0) return
|
|
857
|
-
e
|
|
1079
|
+
stopHandlePointerDown(e)
|
|
858
1080
|
setHoveredEdge(null)
|
|
859
1081
|
startDrag({
|
|
860
1082
|
isDragging: true,
|
|
@@ -865,14 +1087,24 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
865
1087
|
pointerId: e.pointerId,
|
|
866
1088
|
})
|
|
867
1089
|
}}
|
|
1090
|
+
onPointerEnter={(e) => {
|
|
1091
|
+
e.stopPropagation()
|
|
1092
|
+
setBodyCursor('move')
|
|
1093
|
+
}}
|
|
1094
|
+
onPointerLeave={(e) => {
|
|
1095
|
+
e.stopPropagation()
|
|
1096
|
+
if (!dragState?.isDragging) clearBodyCursor()
|
|
1097
|
+
}}
|
|
868
1098
|
position={[polygonCenter[0], editY + handleHeight + 0.08, polygonCenter[1]]}
|
|
869
1099
|
/>
|
|
870
1100
|
)}
|
|
871
|
-
|
|
872
1101
|
{allowEdgeMove &&
|
|
873
1102
|
edgeHandles.map(({ index, length, midpoint, rotationY, outwardNormal, outwardAngle }) => {
|
|
874
1103
|
const isHovered = hoveredEdge === index
|
|
875
1104
|
const isDragging = dragState?.mode === 'edge' && dragState.edgeIndex === index
|
|
1105
|
+
if (dragState && !isDragging) return null
|
|
1106
|
+
const isLinkedHighlighted = highlightedEdgeIndices.has(index)
|
|
1107
|
+
const isHighlighted = isDragging || isHovered || isLinkedHighlighted
|
|
876
1108
|
const arrowX = midpoint[0] + outwardNormal[0] * EDGE_ARROW_OFFSET
|
|
877
1109
|
const arrowZ = midpoint[1] + outwardNormal[1] * EDGE_ARROW_OFFSET
|
|
878
1110
|
|
|
@@ -906,15 +1138,16 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
906
1138
|
which sits outside the polygon and never overlaps a
|
|
907
1139
|
vertex/midpoint handle. */}
|
|
908
1140
|
<mesh
|
|
1141
|
+
geometry={EDGE_HANDLE_GEOMETRY}
|
|
909
1142
|
layers={EDITOR_LAYER}
|
|
910
1143
|
position={[midpoint[0], edgeHandleY, midpoint[1]]}
|
|
911
1144
|
raycast={NO_RAYCAST}
|
|
912
1145
|
rotation={[0, rotationY, 0]}
|
|
1146
|
+
scale={[length, EDGE_HANDLE_HEIGHT, EDGE_HANDLE_THICKNESS]}
|
|
913
1147
|
>
|
|
914
|
-
<boxGeometry args={[length, EDGE_HANDLE_HEIGHT, EDGE_HANDLE_THICKNESS]} />
|
|
915
1148
|
<meshBasicMaterial
|
|
916
|
-
color={
|
|
917
|
-
opacity={isDragging ? 0.5 :
|
|
1149
|
+
color={isHighlighted ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
|
|
1150
|
+
opacity={isDragging ? 0.5 : isHighlighted ? 0.38 : 0.14}
|
|
918
1151
|
transparent
|
|
919
1152
|
/>
|
|
920
1153
|
</mesh>
|
|
@@ -922,7 +1155,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
922
1155
|
Points outward from the edge; dragging it translates only this
|
|
923
1156
|
edge's two vertices along the outward normal. */}
|
|
924
1157
|
<OutlinedEdgeArrowHandle
|
|
925
|
-
color={
|
|
1158
|
+
color={isHighlighted ? EDGE_ARROW_HOVER_COLOR : EDGE_ARROW_COLOR}
|
|
926
1159
|
geometry={arrowGeometry}
|
|
927
1160
|
onClick={(e) => {
|
|
928
1161
|
if (e.button !== 0) return
|
|
@@ -930,16 +1163,18 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
930
1163
|
}}
|
|
931
1164
|
onPointerDown={(e) => {
|
|
932
1165
|
if (e.button !== 0) return
|
|
933
|
-
e
|
|
1166
|
+
stopHandlePointerDown(e)
|
|
934
1167
|
beginEdgeDrag(e)
|
|
935
1168
|
}}
|
|
936
1169
|
onPointerEnter={(e) => {
|
|
937
1170
|
e.stopPropagation()
|
|
938
1171
|
setHoveredEdge(index)
|
|
1172
|
+
setBodyCursor(edgeResizeCursor(midpoint, outwardNormal))
|
|
939
1173
|
}}
|
|
940
1174
|
onPointerLeave={(e) => {
|
|
941
1175
|
e.stopPropagation()
|
|
942
1176
|
setHoveredEdge(null)
|
|
1177
|
+
if (!dragState?.isDragging) clearBodyCursor()
|
|
943
1178
|
}}
|
|
944
1179
|
position={[arrowX, edgeHandleY, arrowZ]}
|
|
945
1180
|
rotationY={outwardAngle}
|
|
@@ -954,7 +1189,9 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
954
1189
|
!dragState &&
|
|
955
1190
|
midpoints.map(([x, z], index) => {
|
|
956
1191
|
const isHovered = hoveredMidpoint === index
|
|
957
|
-
const
|
|
1192
|
+
const isLinkedHighlighted = highlightedEdgeIndices.has(index)
|
|
1193
|
+
const isHighlighted = isHovered || isLinkedHighlighted
|
|
1194
|
+
const radius = 0.05
|
|
958
1195
|
const height = handleHeight
|
|
959
1196
|
const point: [number, number] = [x!, z!]
|
|
960
1197
|
const position: [number, number, number] = [x!, editY + height / 2, z!]
|
|
@@ -965,7 +1202,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
965
1202
|
},
|
|
966
1203
|
onPointerDown: (e) => {
|
|
967
1204
|
if (e.button !== 0) return
|
|
968
|
-
e
|
|
1205
|
+
stopHandlePointerDown(e)
|
|
969
1206
|
onBeforeVertexDrag?.(index + 1, point)
|
|
970
1207
|
const insertedVertex = handleAddVertex(index, point)
|
|
971
1208
|
if (insertedVertex.vertexIndex >= 0) {
|
|
@@ -983,10 +1220,12 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
983
1220
|
onPointerEnter: (e) => {
|
|
984
1221
|
e.stopPropagation()
|
|
985
1222
|
setHoveredMidpoint(index)
|
|
1223
|
+
setBodyCursor('move')
|
|
986
1224
|
},
|
|
987
1225
|
onPointerLeave: (e) => {
|
|
988
1226
|
e.stopPropagation()
|
|
989
1227
|
setHoveredMidpoint(null)
|
|
1228
|
+
clearBodyCursor()
|
|
990
1229
|
},
|
|
991
1230
|
}
|
|
992
1231
|
|
|
@@ -1008,11 +1247,13 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|
|
1008
1247
|
|
|
1009
1248
|
return (
|
|
1010
1249
|
<OutlinedCylinderHandle
|
|
1011
|
-
|
|
1250
|
+
// Midpoints read as secondary add-points: the brighter hover
|
|
1251
|
+
// shade at rest distinguishes them from the vertex handles.
|
|
1252
|
+
color={EDGE_ARROW_HOVER_COLOR}
|
|
1012
1253
|
height={height}
|
|
1013
1254
|
key={`midpoint-${index}`}
|
|
1014
1255
|
{...handleProps}
|
|
1015
|
-
opacity={
|
|
1256
|
+
opacity={isHighlighted ? 1 : 0.7}
|
|
1016
1257
|
position={position}
|
|
1017
1258
|
radius={radius}
|
|
1018
1259
|
/>
|