@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
|
@@ -7,15 +7,26 @@ import { Html } from '@react-three/drei'
|
|
|
7
7
|
import { createPortal, useFrame, useThree } from '@react-three/fiber'
|
|
8
8
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
9
9
|
import { type Camera, type Object3D, Vector3 } from 'three'
|
|
10
|
+
import { groundHeightAt } from '../../lib/ground-surface'
|
|
11
|
+
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
10
12
|
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
|
|
11
|
-
import
|
|
12
|
-
import { formatMeasurement } from './measurement-pill'
|
|
13
|
+
import { useActiveHandleDrag } from '../../store/use-interaction-scope'
|
|
13
14
|
|
|
14
15
|
type ViewportSize = {
|
|
15
16
|
width: number
|
|
16
17
|
height: number
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* How far the label floats above the ground under it.
|
|
22
|
+
*
|
|
23
|
+
* Was a bare `0.5` in the position literal, which on flat ground is the same thing.
|
|
24
|
+
* Named now because it is no longer the label's *height* but its clearance: on a
|
|
25
|
+
* sculpted lot the number underneath moves, and half a metre is what keeps the text
|
|
26
|
+
* clear of a rim the boundary line is draped over.
|
|
27
|
+
*/
|
|
28
|
+
const LABEL_LIFT = 0.5
|
|
29
|
+
|
|
19
30
|
const htmlPosition = new Vector3()
|
|
20
31
|
|
|
21
32
|
function calculateHtmlPosition(el: Object3D, camera: Camera, size: ViewportSize) {
|
|
@@ -37,7 +48,7 @@ export function SiteEdgeLabels() {
|
|
|
37
48
|
const node = state.nodes[firstRoot]
|
|
38
49
|
return node?.type === 'site' ? (node as SiteNode) : null
|
|
39
50
|
})
|
|
40
|
-
const activeHandleDrag =
|
|
51
|
+
const activeHandleDrag = useActiveHandleDrag()
|
|
41
52
|
const unit = useViewer((state) => state.unit)
|
|
42
53
|
const cameraMode = useViewer((state) => state.cameraMode)
|
|
43
54
|
const isNight = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
|
@@ -78,14 +89,25 @@ export function SiteEdgeLabels() {
|
|
|
78
89
|
if (obj) setSiteObj(obj)
|
|
79
90
|
})
|
|
80
91
|
|
|
92
|
+
// No live-terrain subscription here, deliberately: activating a site-boundary
|
|
93
|
+
// handle exits sculpt mode before the handle-drag scope begins, so the ground
|
|
94
|
+
// cannot move while these labels are on screen. The drag itself rewrites
|
|
95
|
+
// `polygon` every frame, which re-runs this memo against the current field.
|
|
81
96
|
const edges = useMemo(() => {
|
|
82
97
|
if (polygon.length < 2) return []
|
|
83
98
|
return polygon.map(([x1, z1], i) => {
|
|
84
99
|
const [x2, z2] = polygon[(i + 1) % polygon.length]!
|
|
85
100
|
const midX = (x1! + x2) / 2
|
|
86
101
|
const midZ = (z1! + z2) / 2
|
|
102
|
+
// Length stays the *plan* length. A property line is a horizontal measurement
|
|
103
|
+
// — a deed says 30 m whether the lot is flat or a hillside — and reporting the
|
|
104
|
+
// slope distance would make the same lot appear to grow as it is sculpted.
|
|
105
|
+
// Only the label's Y follows the ground.
|
|
87
106
|
const dist = Math.sqrt((x2 - x1!) ** 2 + (z2 - z1!) ** 2)
|
|
88
|
-
|
|
107
|
+
// `?? 0` covers both no-terrain cases, and 0 *is* the datum, so this reduces
|
|
108
|
+
// to the flat constant it replaced whenever the lot is unsculpted.
|
|
109
|
+
const groundY = groundHeightAt(midX, midZ, 0) ?? 0
|
|
110
|
+
return { midX, midY: groundY + LABEL_LIFT, midZ, dist }
|
|
89
111
|
})
|
|
90
112
|
}, [polygon])
|
|
91
113
|
|
|
@@ -99,7 +121,7 @@ export function SiteEdgeLabels() {
|
|
|
99
121
|
calculatePosition={calculateLabelPosition}
|
|
100
122
|
key={`${cameraMode}-${camera.uuid}-edge-${i}`}
|
|
101
123
|
occlude
|
|
102
|
-
position={[edge.midX,
|
|
124
|
+
position={[edge.midX, edge.midY, edge.midZ]}
|
|
103
125
|
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
104
126
|
zIndexRange={[10, 0]}
|
|
105
127
|
>
|
|
@@ -110,7 +132,7 @@ export function SiteEdgeLabels() {
|
|
|
110
132
|
textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`,
|
|
111
133
|
}}
|
|
112
134
|
>
|
|
113
|
-
{
|
|
135
|
+
{formatLinearMeasurement(edge.dist, unit)}
|
|
114
136
|
</div>
|
|
115
137
|
</Html>
|
|
116
138
|
))}
|
|
@@ -25,7 +25,9 @@ import {
|
|
|
25
25
|
} from 'three'
|
|
26
26
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
27
27
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
28
|
+
import { holeEditScope } from '../../lib/interaction/scope'
|
|
28
29
|
import useEditor from '../../store/use-editor'
|
|
30
|
+
import useInteractionScope, { useEditingHole } from '../../store/use-interaction-scope'
|
|
29
31
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
30
32
|
import { swallowNextClick } from './handles/use-handle-drag'
|
|
31
33
|
|
|
@@ -239,7 +241,9 @@ export function SlabHoleHighlights() {
|
|
|
239
241
|
}
|
|
240
242
|
}, [hoveredHole, selectedIds, setHoveredHole])
|
|
241
243
|
|
|
242
|
-
|
|
244
|
+
// Sole selection only — hole outlines are click-to-edit chrome, and a
|
|
245
|
+
// multi-selection shows highlight only (the group transforms as one piece).
|
|
246
|
+
if (selectedIds.length !== 1) return null
|
|
243
247
|
|
|
244
248
|
return (
|
|
245
249
|
<>
|
|
@@ -254,7 +258,7 @@ function SelectedSlabHoleHighlights({ slabId }: { slabId: string }) {
|
|
|
254
258
|
const node = useScene((state) => state.nodes[slabId as AnyNodeId])
|
|
255
259
|
const override = useLiveNodeOverrides((state) => state.overrides.get(slabId))
|
|
256
260
|
const hoveredHole = useEditor((state) => state.hoveredHole)
|
|
257
|
-
const editingHole =
|
|
261
|
+
const editingHole = useEditingHole()
|
|
258
262
|
const setHoveredHole = useEditor((state) => state.setHoveredHole)
|
|
259
263
|
|
|
260
264
|
const slab = node?.type === 'slab' ? (node as SlabNode) : null
|
|
@@ -389,21 +393,25 @@ function SlabHoleHighlight({
|
|
|
389
393
|
// user edits the source rather than the synced hole. Everything else —
|
|
390
394
|
// manual holes and holes that predate holeMetadata — opens the editor.
|
|
391
395
|
if (metadata?.source === 'stair' && metadata.stairId) {
|
|
392
|
-
|
|
396
|
+
useInteractionScope
|
|
397
|
+
.getState()
|
|
398
|
+
.endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
|
|
393
399
|
useEditor.getState().setHoveredHole(null)
|
|
394
400
|
resetPointerCursor()
|
|
395
401
|
selectOwnedNode(metadata.stairId, 'stair')
|
|
396
402
|
return
|
|
397
403
|
}
|
|
398
404
|
if (metadata?.source === 'elevator' && metadata.elevatorId) {
|
|
399
|
-
|
|
405
|
+
useInteractionScope
|
|
406
|
+
.getState()
|
|
407
|
+
.endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
|
|
400
408
|
useEditor.getState().setHoveredHole(null)
|
|
401
409
|
resetPointerCursor()
|
|
402
410
|
selectOwnedNode(metadata.elevatorId, 'elevator')
|
|
403
411
|
return
|
|
404
412
|
}
|
|
405
413
|
|
|
406
|
-
|
|
414
|
+
useInteractionScope.getState().begin(holeEditScope({ nodeId: slabId, holeIndex }))
|
|
407
415
|
useViewer.getState().setSelection({ selectedIds: [slabId as AnyNodeId] })
|
|
408
416
|
},
|
|
409
417
|
[holeIndex, metadata, slabId],
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { emitter } from '@pascal-app/core'
|
|
4
|
-
import {
|
|
4
|
+
import { Check, Crop, Loader2, Maximize2, Monitor, X } from 'lucide-react'
|
|
5
5
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
6
6
|
import { useIsMobile } from '../../hooks/use-mobile'
|
|
7
7
|
import { triggerSFX } from '../../lib/sfx-bus'
|
|
8
|
-
import useEditor
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
8
|
+
import useEditor, {
|
|
9
|
+
type SnapshotCropMode,
|
|
10
|
+
type SnapshotStandardAspect,
|
|
11
|
+
} from '../../store/use-editor'
|
|
12
|
+
|
|
13
|
+
// Local alias — distinct from `useEditor.captureMode` (which describes *why*
|
|
14
|
+
// a capture is happening, e.g. `preset`). This one says HOW the captured
|
|
15
|
+
// pixels are cropped: full-frame 16:9 (`standard`), raw canvas viewport, or
|
|
16
|
+
// user-dragged area. Hosts can preselect it via `captureMode.crop`.
|
|
17
|
+
type CropMode = SnapshotCropMode
|
|
15
18
|
type CaptureState = 'idle' | 'capturing' | 'saved'
|
|
16
19
|
|
|
17
20
|
interface DragPoint {
|
|
@@ -24,12 +27,23 @@ interface Drag {
|
|
|
24
27
|
end: DragPoint
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// Output presets for `standard` captures — long edge stays near 1920.
|
|
31
|
+
const STANDARD_SIZES: Record<SnapshotStandardAspect, { w: number; h: number }> = {
|
|
32
|
+
'16:9': { w: 1920, h: 1080 },
|
|
33
|
+
'9:16': { w: 1080, h: 1920 },
|
|
34
|
+
'4:3': { w: 1920, h: 1440 },
|
|
35
|
+
'3:4': { w: 1440, h: 1920 },
|
|
36
|
+
'1:1': { w: 1440, h: 1440 },
|
|
37
|
+
}
|
|
38
|
+
type StandardAspect = SnapshotStandardAspect
|
|
39
|
+
|
|
27
40
|
function getResolution(
|
|
28
41
|
mode: CropMode,
|
|
29
42
|
overlayEl: HTMLDivElement | null,
|
|
30
43
|
drag: Drag | null,
|
|
44
|
+
standardAspect: StandardAspect,
|
|
31
45
|
): { w: number; h: number } | null {
|
|
32
|
-
if (mode === 'standard') return
|
|
46
|
+
if (mode === 'standard') return STANDARD_SIZES[standardAspect]
|
|
33
47
|
|
|
34
48
|
if (!overlayEl) return null
|
|
35
49
|
const rect = overlayEl.getBoundingClientRect()
|
|
@@ -49,6 +63,41 @@ function getResolution(
|
|
|
49
63
|
return null
|
|
50
64
|
}
|
|
51
65
|
|
|
66
|
+
/** Rule-of-thirds guide rendered inside a framing surface. */
|
|
67
|
+
function ThirdsGrid() {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className="pointer-events-none absolute inset-0"
|
|
71
|
+
style={{
|
|
72
|
+
background: `
|
|
73
|
+
linear-gradient(to right, transparent calc(33.33% - 0.5px), rgba(255,255,255,0.16) 33.33%, transparent calc(33.33% + 0.5px)),
|
|
74
|
+
linear-gradient(to right, transparent calc(66.66% - 0.5px), rgba(255,255,255,0.16) 66.66%, transparent calc(66.66% + 0.5px)),
|
|
75
|
+
linear-gradient(to bottom, transparent calc(33.33% - 0.5px), rgba(255,255,255,0.16) 33.33%, transparent calc(33.33% + 0.5px)),
|
|
76
|
+
linear-gradient(to bottom, transparent calc(66.66% - 0.5px), rgba(255,255,255,0.16) 66.66%, transparent calc(66.66% + 0.5px))`,
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Accented corner brackets on the framing rect. */
|
|
83
|
+
function CornerAccents() {
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<span className="pointer-events-none absolute -top-0.5 -left-0.5 h-5 w-5 rounded-tl-md border-primary border-t-[2.5px] border-l-[2.5px]" />
|
|
87
|
+
<span className="pointer-events-none absolute -right-0.5 -bottom-0.5 h-5 w-5 rounded-br-md border-primary border-r-[2.5px] border-b-[2.5px]" />
|
|
88
|
+
</>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const HUD_CHIP_CLASS =
|
|
93
|
+
'flex flex-col gap-px rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 backdrop-blur-md'
|
|
94
|
+
|
|
95
|
+
const CROP_LABELS: Record<CropMode, string> = {
|
|
96
|
+
standard: 'Standard',
|
|
97
|
+
viewport: 'Viewport',
|
|
98
|
+
area: 'Area',
|
|
99
|
+
}
|
|
100
|
+
|
|
52
101
|
export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
53
102
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
54
103
|
const captureMode = useEditor((s) => s.captureMode)
|
|
@@ -58,12 +107,32 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
58
107
|
// a transparent background — the user picks framing but not the
|
|
59
108
|
// crop shape. Matches the unified preset-thumbnail capture flow.
|
|
60
109
|
const isPreset = captureMode.mode === 'preset'
|
|
110
|
+
const requestedCrop = captureMode.mode === 'standard' ? captureMode.crop : undefined
|
|
111
|
+
const requestedAspect = captureMode.mode === 'standard' ? captureMode.standardAspect : undefined
|
|
112
|
+
// A host-preselected crop means the host needs that exact output shape
|
|
113
|
+
// (e.g. the publish-cover capture) — hide the crop/aspect switcher.
|
|
114
|
+
const isCropLocked = isPreset || requestedCrop !== undefined
|
|
61
115
|
|
|
62
116
|
const [mode, setMode] = useState<CropMode>('standard')
|
|
117
|
+
const [standardAspect, setStandardAspect] = useState<StandardAspect>('16:9')
|
|
118
|
+
const [aspectMenuOpen, setAspectMenuOpen] = useState(false)
|
|
63
119
|
const [drag, setDrag] = useState<Drag | null>(null)
|
|
64
120
|
const [isDragging, setIsDragging] = useState(false)
|
|
65
121
|
const [captureState, setCaptureState] = useState<CaptureState>('idle')
|
|
66
122
|
const overlayRef = useRef<HTMLDivElement>(null)
|
|
123
|
+
// Overlay size drives the computed standard-frame rect + resolution HUD.
|
|
124
|
+
const [overlaySize, setOverlaySize] = useState<{ w: number; h: number } | null>(null)
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!isCaptureMode) return
|
|
128
|
+
const el = overlayRef.current
|
|
129
|
+
if (!el) return
|
|
130
|
+
const update = () => setOverlaySize({ w: el.clientWidth, h: el.clientHeight })
|
|
131
|
+
update()
|
|
132
|
+
const observer = new ResizeObserver(update)
|
|
133
|
+
observer.observe(el)
|
|
134
|
+
return () => observer.disconnect()
|
|
135
|
+
}, [isCaptureMode])
|
|
67
136
|
|
|
68
137
|
// Dismiss on Esc
|
|
69
138
|
useEffect(() => {
|
|
@@ -82,7 +151,9 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
82
151
|
// tweak the framing, but they don't have to draw the rect first.
|
|
83
152
|
useEffect(() => {
|
|
84
153
|
if (!isCaptureMode) return
|
|
85
|
-
setMode(isPreset ? 'area' : 'standard')
|
|
154
|
+
setMode(isPreset ? 'area' : (requestedCrop ?? 'standard'))
|
|
155
|
+
setStandardAspect(requestedAspect ?? '16:9')
|
|
156
|
+
setAspectMenuOpen(false)
|
|
86
157
|
setIsDragging(false)
|
|
87
158
|
setCaptureState('idle')
|
|
88
159
|
if (isPreset && overlayRef.current) {
|
|
@@ -97,7 +168,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
97
168
|
} else {
|
|
98
169
|
setDrag(null)
|
|
99
170
|
}
|
|
100
|
-
}, [isCaptureMode, isPreset])
|
|
171
|
+
}, [isCaptureMode, isPreset, requestedCrop, requestedAspect])
|
|
101
172
|
|
|
102
173
|
// Listen for snapshot saved to show feedback then exit
|
|
103
174
|
useEffect(() => {
|
|
@@ -265,16 +336,36 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
265
336
|
projectId,
|
|
266
337
|
captureMode: mode,
|
|
267
338
|
cropRegion,
|
|
339
|
+
standardSize: mode === 'standard' ? STANDARD_SIZES[standardAspect] : undefined,
|
|
268
340
|
// In preset mode, the ThumbnailGenerator should keep the alpha
|
|
269
341
|
// channel transparent so the saved preset thumbnail composes
|
|
270
342
|
// cleanly onto any palette background.
|
|
271
343
|
transparent: isPreset,
|
|
272
344
|
})
|
|
273
|
-
}, [captureState, mode, drag, projectId, isPreset])
|
|
345
|
+
}, [captureState, mode, drag, projectId, isPreset, standardAspect])
|
|
274
346
|
|
|
275
347
|
if (!isCaptureMode) return null
|
|
276
348
|
|
|
277
|
-
const resolution = getResolution(mode, overlayRef.current, drag)
|
|
349
|
+
const resolution = getResolution(mode, overlayRef.current, drag, standardAspect)
|
|
350
|
+
|
|
351
|
+
// Standard mode framing: the output is a center-crop of the canvas to the
|
|
352
|
+
// chosen aspect (see ThumbnailGenerator) — show exactly that region as a
|
|
353
|
+
// letterboxed frame.
|
|
354
|
+
const standardFrame =
|
|
355
|
+
mode === 'standard' && overlaySize
|
|
356
|
+
? (() => {
|
|
357
|
+
const size = STANDARD_SIZES[standardAspect]
|
|
358
|
+
const targetRatio = size.w / size.h
|
|
359
|
+
const w = Math.min(overlaySize.w, overlaySize.h * targetRatio)
|
|
360
|
+
const h = w / targetRatio
|
|
361
|
+
return {
|
|
362
|
+
left: (overlaySize.w - w) / 2,
|
|
363
|
+
top: (overlaySize.h - h) / 2,
|
|
364
|
+
width: w,
|
|
365
|
+
height: h,
|
|
366
|
+
}
|
|
367
|
+
})()
|
|
368
|
+
: null
|
|
278
369
|
|
|
279
370
|
// Area selection rect (CSS px, relative to overlay)
|
|
280
371
|
const selectionStyle =
|
|
@@ -294,6 +385,23 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
294
385
|
|
|
295
386
|
return (
|
|
296
387
|
<div className="pointer-events-none absolute inset-0 z-40" ref={overlayRef}>
|
|
388
|
+
{/* Standard mode: letterboxed 16:9 frame with thirds + corner accents */}
|
|
389
|
+
{standardFrame && (
|
|
390
|
+
<div
|
|
391
|
+
className="pointer-events-none absolute rounded-md border-[1.5px] border-white/85"
|
|
392
|
+
style={{
|
|
393
|
+
...standardFrame,
|
|
394
|
+
boxShadow: '0 0 0 9999px rgba(10,10,14,0.4)',
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
<ThirdsGrid />
|
|
398
|
+
<CornerAccents />
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{/* Viewport mode: the whole canvas is the frame — thirds only */}
|
|
403
|
+
{mode === 'viewport' && <ThirdsGrid />}
|
|
404
|
+
|
|
297
405
|
{/* Area mode: dim layer + crosshair cursor + drag-to-select.
|
|
298
406
|
*
|
|
299
407
|
* Preset mode reuses the same DOM but stays click-through: the
|
|
@@ -336,7 +444,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
336
444
|
has a pre-staged square, so we never show it there. */}
|
|
337
445
|
{!selectionStyle && !isPreset && (
|
|
338
446
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
339
|
-
<span className="rounded-full bg-
|
|
447
|
+
<span className="rounded-full border border-white/10 bg-neutral-950/80 px-4 py-2 text-sm text-white backdrop-blur-md">
|
|
340
448
|
Drag the area you want to capture
|
|
341
449
|
</span>
|
|
342
450
|
</div>
|
|
@@ -345,6 +453,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
345
453
|
{/* Selection rect */}
|
|
346
454
|
{selectionStyle && (
|
|
347
455
|
<div
|
|
456
|
+
className="rounded-sm border-[1.5px] border-white/85"
|
|
348
457
|
style={{
|
|
349
458
|
position: 'absolute',
|
|
350
459
|
left: selectionStyle.left,
|
|
@@ -352,11 +461,12 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
352
461
|
width: selectionStyle.width,
|
|
353
462
|
height: selectionStyle.height,
|
|
354
463
|
pointerEvents: 'none',
|
|
355
|
-
boxShadow: '0 0 0 9999px rgba(
|
|
356
|
-
|
|
357
|
-
background: 'rgba(255,255,255,0.04)',
|
|
464
|
+
boxShadow: '0 0 0 9999px rgba(10,10,14,0.4)',
|
|
465
|
+
background: 'rgba(255,255,255,0.03)',
|
|
358
466
|
}}
|
|
359
467
|
>
|
|
468
|
+
{hasSelection && <ThirdsGrid />}
|
|
469
|
+
<CornerAccents />
|
|
360
470
|
{/* Corner handles — preset mode locks the frame to the
|
|
361
471
|
auto-staged centered square; the user adjusts the
|
|
362
472
|
camera instead. */}
|
|
@@ -390,11 +500,34 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
390
500
|
</div>
|
|
391
501
|
)}
|
|
392
502
|
|
|
503
|
+
{/* Top-center HUD — what the shot will be. Preset captures are a fixed
|
|
504
|
+
square and carry their own "Frame your item" banner up there. */}
|
|
505
|
+
{!isMobile && !isPreset && (
|
|
506
|
+
<div className="pointer-events-none absolute top-4 left-1/2 flex -translate-x-1/2 gap-2">
|
|
507
|
+
<div className={HUD_CHIP_CLASS}>
|
|
508
|
+
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
|
509
|
+
Crop
|
|
510
|
+
</span>
|
|
511
|
+
<span className="font-semibold text-white text-xs">
|
|
512
|
+
{isPreset ? 'Preset · square' : CROP_LABELS[mode]}
|
|
513
|
+
</span>
|
|
514
|
+
</div>
|
|
515
|
+
<div className={HUD_CHIP_CLASS}>
|
|
516
|
+
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
|
517
|
+
Format
|
|
518
|
+
</span>
|
|
519
|
+
<span className="font-semibold text-white text-xs tabular-nums">
|
|
520
|
+
{resolution ? `${resolution.w} × ${resolution.h}` : '—'}
|
|
521
|
+
</span>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
|
|
393
526
|
{/* Top-right dismiss button (icon-only on mobile) */}
|
|
394
527
|
<div className="pointer-events-auto absolute top-4 right-4">
|
|
395
528
|
<button
|
|
396
529
|
aria-label="Close capture mode"
|
|
397
|
-
className="flex items-center gap-1.5 rounded-
|
|
530
|
+
className="flex items-center gap-1.5 rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 text-white/80 text-xs backdrop-blur-md transition-colors hover:bg-neutral-950 hover:text-white"
|
|
398
531
|
onClick={dismiss}
|
|
399
532
|
type="button"
|
|
400
533
|
>
|
|
@@ -403,104 +536,114 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
403
536
|
</button>
|
|
404
537
|
</div>
|
|
405
538
|
|
|
406
|
-
{/*
|
|
407
|
-
<div className="pointer-events-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
</span>
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
const captureButton = (
|
|
449
|
-
<button
|
|
450
|
-
className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-1.5 font-medium text-primary-foreground text-xs transition-opacity disabled:opacity-50"
|
|
451
|
-
disabled={captureDisabled}
|
|
452
|
-
onClick={handleCapture}
|
|
453
|
-
type="button"
|
|
454
|
-
>
|
|
455
|
-
{captureState === 'capturing' ? (
|
|
456
|
-
<>
|
|
457
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
458
|
-
Capturing
|
|
459
|
-
</>
|
|
460
|
-
) : captureState === 'saved' ? (
|
|
461
|
-
<>
|
|
462
|
-
<Check className="h-3.5 w-3.5" />
|
|
463
|
-
Saved
|
|
464
|
-
</>
|
|
465
|
-
) : (
|
|
466
|
-
<>
|
|
467
|
-
<Camera className="h-3.5 w-3.5" />
|
|
468
|
-
Capture
|
|
469
|
-
</>
|
|
470
|
-
)}
|
|
471
|
-
</button>
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
if (isMobile) {
|
|
475
|
-
return (
|
|
476
|
-
<div className="flex flex-col items-stretch gap-2 rounded-2xl border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
|
|
477
|
-
{modeButtons && (
|
|
478
|
-
<div className="flex items-center justify-center gap-1">{modeButtons}</div>
|
|
479
|
-
)}
|
|
480
|
-
<div
|
|
481
|
-
className={
|
|
482
|
-
modeButtons
|
|
483
|
-
? 'flex items-center justify-center gap-2 border-white/10 border-t pt-2'
|
|
484
|
-
: 'flex items-center justify-center gap-2'
|
|
485
|
-
}
|
|
486
|
-
>
|
|
487
|
-
{resolutionDisplay}
|
|
488
|
-
{captureButton}
|
|
489
|
-
</div>
|
|
539
|
+
{/* Subtle scrim so the bottom controls stay readable on bright scenes */}
|
|
540
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/45 via-black/15 to-transparent" />
|
|
541
|
+
|
|
542
|
+
{/* Bottom-center: crop switcher, caption + shutter */}
|
|
543
|
+
<div className="pointer-events-none absolute right-0 bottom-5 left-0 flex flex-col items-center gap-2.5">
|
|
544
|
+
{!isCropLocked && (
|
|
545
|
+
<div className="pointer-events-auto relative flex items-center gap-1 rounded-full border border-white/10 bg-neutral-950/85 px-1.5 py-1.5 shadow-xl backdrop-blur-md">
|
|
546
|
+
{/* Clicking Standard while it's active opens the aspect picker */}
|
|
547
|
+
<ModeButton
|
|
548
|
+
active={mode === 'standard'}
|
|
549
|
+
badge={standardAspect}
|
|
550
|
+
icon={<Monitor className="h-3.5 w-3.5" />}
|
|
551
|
+
label={isMobile ? undefined : 'Standard'}
|
|
552
|
+
onClick={() => {
|
|
553
|
+
if (mode === 'standard') setAspectMenuOpen((v) => !v)
|
|
554
|
+
else setAspectMenuOpen(false)
|
|
555
|
+
setMode('standard')
|
|
556
|
+
setDrag(null)
|
|
557
|
+
}}
|
|
558
|
+
/>
|
|
559
|
+
{aspectMenuOpen && mode === 'standard' && (
|
|
560
|
+
<div className="absolute bottom-[calc(100%+8px)] left-0 flex gap-1 rounded-full border border-white/10 bg-neutral-950/90 p-1.5 shadow-xl backdrop-blur-md">
|
|
561
|
+
{(Object.keys(STANDARD_SIZES) as StandardAspect[]).map((aspect) => (
|
|
562
|
+
<button
|
|
563
|
+
className={`rounded-full px-2.5 py-1 font-mono text-[11px] transition-colors ${
|
|
564
|
+
aspect === standardAspect
|
|
565
|
+
? 'bg-white/15 text-white ring-1 ring-white/20'
|
|
566
|
+
: 'text-white/55 hover:text-white/90'
|
|
567
|
+
}`}
|
|
568
|
+
key={aspect}
|
|
569
|
+
onClick={() => {
|
|
570
|
+
setStandardAspect(aspect)
|
|
571
|
+
setAspectMenuOpen(false)
|
|
572
|
+
}}
|
|
573
|
+
type="button"
|
|
574
|
+
>
|
|
575
|
+
{aspect}
|
|
576
|
+
</button>
|
|
577
|
+
))}
|
|
490
578
|
</div>
|
|
491
|
-
)
|
|
492
|
-
|
|
579
|
+
)}
|
|
580
|
+
<ModeButton
|
|
581
|
+
active={mode === 'viewport'}
|
|
582
|
+
icon={<Maximize2 className="h-3.5 w-3.5" />}
|
|
583
|
+
label={isMobile ? undefined : 'Viewport'}
|
|
584
|
+
onClick={() => {
|
|
585
|
+
setMode('viewport')
|
|
586
|
+
setDrag(null)
|
|
587
|
+
setAspectMenuOpen(false)
|
|
588
|
+
}}
|
|
589
|
+
/>
|
|
590
|
+
<ModeButton
|
|
591
|
+
active={mode === 'area'}
|
|
592
|
+
icon={<Crop className="h-3.5 w-3.5" />}
|
|
593
|
+
label={isMobile ? undefined : 'Area'}
|
|
594
|
+
onClick={() => {
|
|
595
|
+
setMode('area')
|
|
596
|
+
setAspectMenuOpen(false)
|
|
597
|
+
}}
|
|
598
|
+
/>
|
|
599
|
+
{isMobile && (
|
|
600
|
+
<span className="px-2 text-white/50 text-xs tabular-nums">
|
|
601
|
+
{resolution ? `${resolution.w} × ${resolution.h}` : '—'}
|
|
602
|
+
</span>
|
|
603
|
+
)}
|
|
604
|
+
</div>
|
|
605
|
+
)}
|
|
606
|
+
|
|
607
|
+
{/* Preset captures carry their own "Frame your item" banner — the
|
|
608
|
+
snapshot pitch only applies to the studio/reference flow. */}
|
|
609
|
+
{!isMobile && !isPreset && (
|
|
610
|
+
<span className="pointer-events-none max-w-90 rounded-lg border border-white/10 bg-neutral-950/85 px-3.5 py-1.5 text-center text-[11.5px] text-white/85 leading-relaxed backdrop-blur-md">
|
|
611
|
+
A <b className="font-semibold text-white">snapshot</b>
|
|
612
|
+
{' freezes this exact camera angle as a reusable reference for renders & videos.'}
|
|
613
|
+
</span>
|
|
614
|
+
)}
|
|
493
615
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
616
|
+
<button
|
|
617
|
+
aria-label={isPreset ? 'Capture' : 'Take snapshot'}
|
|
618
|
+
className="group pointer-events-auto relative grid h-14 w-14 place-items-center rounded-full disabled:opacity-50"
|
|
619
|
+
disabled={captureDisabled}
|
|
620
|
+
onClick={handleCapture}
|
|
621
|
+
type="button"
|
|
622
|
+
>
|
|
623
|
+
<span className="absolute inset-0 rounded-full border-[3px] border-white shadow-lg" />
|
|
624
|
+
<span
|
|
625
|
+
className={`h-10 w-10 rounded-full transition-transform duration-100 ${
|
|
626
|
+
captureState === 'capturing'
|
|
627
|
+
? 'scale-[0.55] bg-primary'
|
|
628
|
+
: 'bg-white group-hover:scale-90 group-active:scale-75'
|
|
629
|
+
}`}
|
|
630
|
+
/>
|
|
631
|
+
{captureState === 'capturing' && (
|
|
632
|
+
<Loader2 className="absolute h-4 w-4 animate-spin text-white" />
|
|
633
|
+
)}
|
|
634
|
+
{captureState === 'saved' && (
|
|
635
|
+
<Check className="absolute h-5 w-5 stroke-3 text-neutral-900" />
|
|
636
|
+
)}
|
|
637
|
+
</button>
|
|
638
|
+
<span className="pointer-events-none font-mono text-[10.5px] text-white uppercase tracking-[0.12em] drop-shadow">
|
|
639
|
+
{captureState === 'capturing'
|
|
640
|
+
? 'Capturing…'
|
|
641
|
+
: captureState === 'saved'
|
|
642
|
+
? 'Saved'
|
|
643
|
+
: isPreset
|
|
644
|
+
? 'Capture'
|
|
645
|
+
: 'Take snapshot'}
|
|
646
|
+
</span>
|
|
504
647
|
</div>
|
|
505
648
|
</div>
|
|
506
649
|
)
|
|
@@ -515,7 +658,7 @@ function ModeButton({
|
|
|
515
658
|
}: {
|
|
516
659
|
active: boolean
|
|
517
660
|
icon: React.ReactNode
|
|
518
|
-
label
|
|
661
|
+
label?: string
|
|
519
662
|
badge?: string
|
|
520
663
|
onClick: () => void
|
|
521
664
|
}) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Camera, Raycaster } from 'three'
|
|
2
|
+
|
|
3
|
+
// Escape hatch for DOM-level interaction sessions (group pick-up move) that
|
|
4
|
+
// need to raycast the 3D view without living inside the R3F tree. The
|
|
5
|
+
// editor's SelectionManager (always mounted with the canvas) publishes the
|
|
6
|
+
// live camera / raycaster / canvas here; consumers must handle `null`
|
|
7
|
+
// (canvas not mounted yet, or torn down).
|
|
8
|
+
export type EditorThreeContext = {
|
|
9
|
+
camera: Camera
|
|
10
|
+
raycaster: Raycaster
|
|
11
|
+
domElement: HTMLCanvasElement
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let current: EditorThreeContext | null = null
|
|
15
|
+
|
|
16
|
+
export function setEditorThreeContext(ctx: EditorThreeContext | null) {
|
|
17
|
+
current = ctx
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getEditorThreeContext(): EditorThreeContext | null {
|
|
21
|
+
return current
|
|
22
|
+
}
|