@pascal-app/editor 0.8.0 → 0.9.2
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 +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -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 +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- 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-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- 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 +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- 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 +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- 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/floating-level-selector.tsx +47 -10
- 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 +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- 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 +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- 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/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- 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 +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- 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 +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -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 +112 -39
- 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/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- 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-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- 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 +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- 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 +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- 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 +183 -0
- package/src/lib/measurements.ts +199 -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/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- 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 +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -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 +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -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 +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- 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 +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,13 +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
|
-
type
|
|
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
|
|
11
18
|
type CaptureState = 'idle' | 'capturing' | 'saved'
|
|
12
19
|
|
|
13
20
|
interface DragPoint {
|
|
@@ -20,12 +27,23 @@ interface Drag {
|
|
|
20
27
|
end: DragPoint
|
|
21
28
|
}
|
|
22
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
|
+
|
|
23
40
|
function getResolution(
|
|
24
|
-
mode:
|
|
41
|
+
mode: CropMode,
|
|
25
42
|
overlayEl: HTMLDivElement | null,
|
|
26
43
|
drag: Drag | null,
|
|
44
|
+
standardAspect: StandardAspect,
|
|
27
45
|
): { w: number; h: number } | null {
|
|
28
|
-
if (mode === 'standard') return
|
|
46
|
+
if (mode === 'standard') return STANDARD_SIZES[standardAspect]
|
|
29
47
|
|
|
30
48
|
if (!overlayEl) return null
|
|
31
49
|
const rect = overlayEl.getBoundingClientRect()
|
|
@@ -45,16 +63,73 @@ function getResolution(
|
|
|
45
63
|
return null
|
|
46
64
|
}
|
|
47
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
|
+
|
|
48
101
|
export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
49
102
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
103
|
+
const captureMode = useEditor((s) => s.captureMode)
|
|
50
104
|
const setCaptureMode = useEditor((s) => s.setCaptureMode)
|
|
51
105
|
const isMobile = useIsMobile()
|
|
52
|
-
|
|
53
|
-
|
|
106
|
+
// `preset` capture mode locks the overlay to a square area crop with
|
|
107
|
+
// a transparent background — the user picks framing but not the
|
|
108
|
+
// crop shape. Matches the unified preset-thumbnail capture flow.
|
|
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
|
+
|
|
113
|
+
const [mode, setMode] = useState<CropMode>('standard')
|
|
114
|
+
const [standardAspect, setStandardAspect] = useState<StandardAspect>('16:9')
|
|
115
|
+
const [aspectMenuOpen, setAspectMenuOpen] = useState(false)
|
|
54
116
|
const [drag, setDrag] = useState<Drag | null>(null)
|
|
55
117
|
const [isDragging, setIsDragging] = useState(false)
|
|
56
118
|
const [captureState, setCaptureState] = useState<CaptureState>('idle')
|
|
57
119
|
const overlayRef = useRef<HTMLDivElement>(null)
|
|
120
|
+
// Overlay size drives the computed standard-frame rect + resolution HUD.
|
|
121
|
+
const [overlaySize, setOverlaySize] = useState<{ w: number; h: number } | null>(null)
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!isCaptureMode) return
|
|
125
|
+
const el = overlayRef.current
|
|
126
|
+
if (!el) return
|
|
127
|
+
const update = () => setOverlaySize({ w: el.clientWidth, h: el.clientHeight })
|
|
128
|
+
update()
|
|
129
|
+
const observer = new ResizeObserver(update)
|
|
130
|
+
observer.observe(el)
|
|
131
|
+
return () => observer.disconnect()
|
|
132
|
+
}, [isCaptureMode])
|
|
58
133
|
|
|
59
134
|
// Dismiss on Esc
|
|
60
135
|
useEffect(() => {
|
|
@@ -66,15 +141,31 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
66
141
|
return () => window.removeEventListener('keydown', onKey)
|
|
67
142
|
}, [isCaptureMode, setCaptureMode])
|
|
68
143
|
|
|
69
|
-
// Reset local state when entering capture mode
|
|
144
|
+
// Reset local state when entering capture mode. Preset mode also
|
|
145
|
+
// auto-stages a centered square crop sized to ~75% of the shorter
|
|
146
|
+
// viewport dimension so the user can capture immediately — the
|
|
147
|
+
// overlay's pan/move/resize handles still apply if they want to
|
|
148
|
+
// tweak the framing, but they don't have to draw the rect first.
|
|
70
149
|
useEffect(() => {
|
|
71
|
-
if (isCaptureMode)
|
|
72
|
-
|
|
150
|
+
if (!isCaptureMode) return
|
|
151
|
+
setMode(isPreset ? 'area' : (requestedCrop ?? 'standard'))
|
|
152
|
+
setStandardAspect(requestedAspect ?? '16:9')
|
|
153
|
+
setAspectMenuOpen(false)
|
|
154
|
+
setIsDragging(false)
|
|
155
|
+
setCaptureState('idle')
|
|
156
|
+
if (isPreset && overlayRef.current) {
|
|
157
|
+
const rect = overlayRef.current.getBoundingClientRect()
|
|
158
|
+
const side = Math.min(rect.width, rect.height) * 0.75
|
|
159
|
+
const cx = rect.width / 2
|
|
160
|
+
const cy = rect.height / 2
|
|
161
|
+
setDrag({
|
|
162
|
+
start: { x: cx - side / 2, y: cy - side / 2 },
|
|
163
|
+
end: { x: cx + side / 2, y: cy + side / 2 },
|
|
164
|
+
})
|
|
165
|
+
} else {
|
|
73
166
|
setDrag(null)
|
|
74
|
-
setIsDragging(false)
|
|
75
|
-
setCaptureState('idle')
|
|
76
167
|
}
|
|
77
|
-
}, [isCaptureMode])
|
|
168
|
+
}, [isCaptureMode, isPreset, requestedCrop, requestedAspect])
|
|
78
169
|
|
|
79
170
|
// Listen for snapshot saved to show feedback then exit
|
|
80
171
|
useEffect(() => {
|
|
@@ -142,11 +233,28 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
142
233
|
start: { x: snapshot.start.x + dx, y: snapshot.start.y + dy },
|
|
143
234
|
end: { x: snapshot.end.x + dx, y: snapshot.end.y + dy },
|
|
144
235
|
})
|
|
236
|
+
} else if (isPreset) {
|
|
237
|
+
// Preset mode locks the rect to a square — use the smaller
|
|
238
|
+
// axis to keep the drag predictable, sign-correct so the user
|
|
239
|
+
// can still drag in any quadrant.
|
|
240
|
+
setDrag((d) => {
|
|
241
|
+
if (!d) return null
|
|
242
|
+
const dx = pt.x - d.start.x
|
|
243
|
+
const dy = pt.y - d.start.y
|
|
244
|
+
const side = Math.min(Math.abs(dx), Math.abs(dy))
|
|
245
|
+
return {
|
|
246
|
+
start: d.start,
|
|
247
|
+
end: {
|
|
248
|
+
x: d.start.x + Math.sign(dx || 1) * side,
|
|
249
|
+
y: d.start.y + Math.sign(dy || 1) * side,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
})
|
|
145
253
|
} else {
|
|
146
254
|
setDrag((d) => (d ? { start: d.start, end: pt } : null))
|
|
147
255
|
}
|
|
148
256
|
},
|
|
149
|
-
[isDragging],
|
|
257
|
+
[isDragging, isPreset],
|
|
150
258
|
)
|
|
151
259
|
|
|
152
260
|
const onPointerUp = useCallback(() => {
|
|
@@ -225,12 +333,36 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
225
333
|
projectId,
|
|
226
334
|
captureMode: mode,
|
|
227
335
|
cropRegion,
|
|
336
|
+
standardSize: mode === 'standard' ? STANDARD_SIZES[standardAspect] : undefined,
|
|
337
|
+
// In preset mode, the ThumbnailGenerator should keep the alpha
|
|
338
|
+
// channel transparent so the saved preset thumbnail composes
|
|
339
|
+
// cleanly onto any palette background.
|
|
340
|
+
transparent: isPreset,
|
|
228
341
|
})
|
|
229
|
-
}, [captureState, mode, drag, projectId])
|
|
342
|
+
}, [captureState, mode, drag, projectId, isPreset, standardAspect])
|
|
230
343
|
|
|
231
344
|
if (!isCaptureMode) return null
|
|
232
345
|
|
|
233
|
-
const resolution = getResolution(mode, overlayRef.current, drag)
|
|
346
|
+
const resolution = getResolution(mode, overlayRef.current, drag, standardAspect)
|
|
347
|
+
|
|
348
|
+
// Standard mode framing: the output is a center-crop of the canvas to the
|
|
349
|
+
// chosen aspect (see ThumbnailGenerator) — show exactly that region as a
|
|
350
|
+
// letterboxed frame.
|
|
351
|
+
const standardFrame =
|
|
352
|
+
mode === 'standard' && overlaySize
|
|
353
|
+
? (() => {
|
|
354
|
+
const size = STANDARD_SIZES[standardAspect]
|
|
355
|
+
const targetRatio = size.w / size.h
|
|
356
|
+
const w = Math.min(overlaySize.w, overlaySize.h * targetRatio)
|
|
357
|
+
const h = w / targetRatio
|
|
358
|
+
return {
|
|
359
|
+
left: (overlaySize.w - w) / 2,
|
|
360
|
+
top: (overlaySize.h - h) / 2,
|
|
361
|
+
width: w,
|
|
362
|
+
height: h,
|
|
363
|
+
}
|
|
364
|
+
})()
|
|
365
|
+
: null
|
|
234
366
|
|
|
235
367
|
// Area selection rect (CSS px, relative to overlay)
|
|
236
368
|
const selectionStyle =
|
|
@@ -250,33 +382,66 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
250
382
|
|
|
251
383
|
return (
|
|
252
384
|
<div className="pointer-events-none absolute inset-0 z-40" ref={overlayRef}>
|
|
253
|
-
{/*
|
|
254
|
-
{
|
|
385
|
+
{/* Standard mode: letterboxed 16:9 frame with thirds + corner accents */}
|
|
386
|
+
{standardFrame && (
|
|
255
387
|
<div
|
|
256
|
-
className="pointer-events-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Update cursor: 'move' when hovering inside an existing selection
|
|
261
|
-
if (!isDragging && drag && overlayRef.current) {
|
|
262
|
-
const rect = overlayRef.current.getBoundingClientRect()
|
|
263
|
-
const px = e.clientX - rect.left
|
|
264
|
-
const py = e.clientY - rect.top
|
|
265
|
-
const x0 = Math.min(drag.start.x, drag.end.x)
|
|
266
|
-
const y0 = Math.min(drag.start.y, drag.end.y)
|
|
267
|
-
const x1 = Math.max(drag.start.x, drag.end.x)
|
|
268
|
-
const y1 = Math.max(drag.start.y, drag.end.y)
|
|
269
|
-
e.currentTarget.style.cursor =
|
|
270
|
-
px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair'
|
|
271
|
-
}
|
|
388
|
+
className="pointer-events-none absolute rounded-md border-[1.5px] border-white/85"
|
|
389
|
+
style={{
|
|
390
|
+
...standardFrame,
|
|
391
|
+
boxShadow: '0 0 0 9999px rgba(10,10,14,0.4)',
|
|
272
392
|
}}
|
|
273
|
-
onPointerUp={onPointerUp}
|
|
274
|
-
style={{ cursor: 'crosshair' }}
|
|
275
393
|
>
|
|
276
|
-
|
|
277
|
-
|
|
394
|
+
<ThirdsGrid />
|
|
395
|
+
<CornerAccents />
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* Viewport mode: the whole canvas is the frame — thirds only */}
|
|
400
|
+
{mode === 'viewport' && <ThirdsGrid />}
|
|
401
|
+
|
|
402
|
+
{/* Area mode: dim layer + crosshair cursor + drag-to-select.
|
|
403
|
+
*
|
|
404
|
+
* Preset mode reuses the same DOM but stays click-through: the
|
|
405
|
+
* crop frame is auto-staged and locked, so the user adjusts the
|
|
406
|
+
* camera (orbit / pan / zoom) instead of dragging the rect. The
|
|
407
|
+
* dim letterbox + dashed border still render via the inline
|
|
408
|
+
* `box-shadow` on the selection rect — they're cosmetic. */}
|
|
409
|
+
{mode === 'area' && (
|
|
410
|
+
<div
|
|
411
|
+
className={
|
|
412
|
+
isPreset
|
|
413
|
+
? 'pointer-events-none absolute inset-0'
|
|
414
|
+
: 'pointer-events-auto absolute inset-0 bg-black/30'
|
|
415
|
+
}
|
|
416
|
+
onPointerDown={isPreset ? undefined : onPointerDown}
|
|
417
|
+
onPointerMove={
|
|
418
|
+
isPreset
|
|
419
|
+
? undefined
|
|
420
|
+
: (e) => {
|
|
421
|
+
onPointerMove(e)
|
|
422
|
+
// Update cursor: 'move' when hovering inside an existing selection
|
|
423
|
+
if (!isDragging && drag && overlayRef.current) {
|
|
424
|
+
const rect = overlayRef.current.getBoundingClientRect()
|
|
425
|
+
const px = e.clientX - rect.left
|
|
426
|
+
const py = e.clientY - rect.top
|
|
427
|
+
const x0 = Math.min(drag.start.x, drag.end.x)
|
|
428
|
+
const y0 = Math.min(drag.start.y, drag.end.y)
|
|
429
|
+
const x1 = Math.max(drag.start.x, drag.end.x)
|
|
430
|
+
const y1 = Math.max(drag.start.y, drag.end.y)
|
|
431
|
+
e.currentTarget.style.cursor =
|
|
432
|
+
px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair'
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
onPointerUp={isPreset ? undefined : onPointerUp}
|
|
437
|
+
style={isPreset ? undefined : { cursor: 'crosshair' }}
|
|
438
|
+
>
|
|
439
|
+
{/* "No selection" hint — only when the user has to draw the
|
|
440
|
+
area themselves (`standard` capture). Preset mode always
|
|
441
|
+
has a pre-staged square, so we never show it there. */}
|
|
442
|
+
{!selectionStyle && !isPreset && (
|
|
278
443
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
279
|
-
<span className="rounded-full bg-
|
|
444
|
+
<span className="rounded-full border border-white/10 bg-neutral-950/80 px-4 py-2 text-sm text-white backdrop-blur-md">
|
|
280
445
|
Drag the area you want to capture
|
|
281
446
|
</span>
|
|
282
447
|
</div>
|
|
@@ -285,6 +450,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
285
450
|
{/* Selection rect */}
|
|
286
451
|
{selectionStyle && (
|
|
287
452
|
<div
|
|
453
|
+
className="rounded-sm border-[1.5px] border-white/85"
|
|
288
454
|
style={{
|
|
289
455
|
position: 'absolute',
|
|
290
456
|
left: selectionStyle.left,
|
|
@@ -292,46 +458,73 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
292
458
|
width: selectionStyle.width,
|
|
293
459
|
height: selectionStyle.height,
|
|
294
460
|
pointerEvents: 'none',
|
|
295
|
-
boxShadow: '0 0 0 9999px rgba(
|
|
296
|
-
|
|
297
|
-
background: 'rgba(255,255,255,0.04)',
|
|
461
|
+
boxShadow: '0 0 0 9999px rgba(10,10,14,0.4)',
|
|
462
|
+
background: 'rgba(255,255,255,0.03)',
|
|
298
463
|
}}
|
|
299
464
|
>
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
465
|
+
{hasSelection && <ThirdsGrid />}
|
|
466
|
+
<CornerAccents />
|
|
467
|
+
{/* Corner handles — preset mode locks the frame to the
|
|
468
|
+
auto-staged centered square; the user adjusts the
|
|
469
|
+
camera instead. */}
|
|
470
|
+
{!isPreset &&
|
|
471
|
+
(
|
|
472
|
+
[
|
|
473
|
+
{ pos: { top: -5, left: -5 }, cursor: 'nwse-resize' },
|
|
474
|
+
{ pos: { top: -5, right: -5 }, cursor: 'nesw-resize' },
|
|
475
|
+
{ pos: { bottom: -5, left: -5 }, cursor: 'nesw-resize' },
|
|
476
|
+
{ pos: { bottom: -5, right: -5 }, cursor: 'nwse-resize' },
|
|
477
|
+
] as const
|
|
478
|
+
).map(({ pos, cursor }, i) => (
|
|
479
|
+
<div
|
|
480
|
+
key={i}
|
|
481
|
+
onPointerDown={(e) => onCornerPointerDown(e, i)}
|
|
482
|
+
style={{
|
|
483
|
+
position: 'absolute',
|
|
484
|
+
width: 10,
|
|
485
|
+
height: 10,
|
|
486
|
+
borderRadius: '50%',
|
|
487
|
+
background: 'white',
|
|
488
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
|
|
489
|
+
pointerEvents: 'auto',
|
|
490
|
+
cursor,
|
|
491
|
+
...pos,
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
))}
|
|
325
495
|
</div>
|
|
326
496
|
)}
|
|
327
497
|
</div>
|
|
328
498
|
)}
|
|
329
499
|
|
|
500
|
+
{/* Top-center HUD — what the shot will be. Preset captures are a fixed
|
|
501
|
+
square and carry their own "Frame your item" banner up there. */}
|
|
502
|
+
{!isMobile && !isPreset && (
|
|
503
|
+
<div className="pointer-events-none absolute top-4 left-1/2 flex -translate-x-1/2 gap-2">
|
|
504
|
+
<div className={HUD_CHIP_CLASS}>
|
|
505
|
+
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
|
506
|
+
Crop
|
|
507
|
+
</span>
|
|
508
|
+
<span className="font-semibold text-white text-xs">
|
|
509
|
+
{isPreset ? 'Preset · square' : CROP_LABELS[mode]}
|
|
510
|
+
</span>
|
|
511
|
+
</div>
|
|
512
|
+
<div className={HUD_CHIP_CLASS}>
|
|
513
|
+
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
|
514
|
+
Format
|
|
515
|
+
</span>
|
|
516
|
+
<span className="font-semibold text-white text-xs tabular-nums">
|
|
517
|
+
{resolution ? `${resolution.w} × ${resolution.h}` : '—'}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
|
|
330
523
|
{/* Top-right dismiss button (icon-only on mobile) */}
|
|
331
524
|
<div className="pointer-events-auto absolute top-4 right-4">
|
|
332
525
|
<button
|
|
333
526
|
aria-label="Close capture mode"
|
|
334
|
-
className="flex items-center gap-1.5 rounded-
|
|
527
|
+
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"
|
|
335
528
|
onClick={dismiss}
|
|
336
529
|
type="button"
|
|
337
530
|
>
|
|
@@ -340,93 +533,114 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
340
533
|
</button>
|
|
341
534
|
</div>
|
|
342
535
|
|
|
343
|
-
{/*
|
|
344
|
-
<div className="pointer-events-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const captureButton = (
|
|
383
|
-
<button
|
|
384
|
-
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"
|
|
385
|
-
disabled={captureDisabled}
|
|
386
|
-
onClick={handleCapture}
|
|
387
|
-
type="button"
|
|
388
|
-
>
|
|
389
|
-
{captureState === 'capturing' ? (
|
|
390
|
-
<>
|
|
391
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
392
|
-
Capturing
|
|
393
|
-
</>
|
|
394
|
-
) : captureState === 'saved' ? (
|
|
395
|
-
<>
|
|
396
|
-
<Check className="h-3.5 w-3.5" />
|
|
397
|
-
Saved
|
|
398
|
-
</>
|
|
399
|
-
) : (
|
|
400
|
-
<>
|
|
401
|
-
<Camera className="h-3.5 w-3.5" />
|
|
402
|
-
Capture
|
|
403
|
-
</>
|
|
404
|
-
)}
|
|
405
|
-
</button>
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
if (isMobile) {
|
|
409
|
-
return (
|
|
410
|
-
<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">
|
|
411
|
-
<div className="flex items-center justify-center gap-1">{modeButtons}</div>
|
|
412
|
-
<div className="flex items-center justify-center gap-2 border-white/10 border-t pt-2">
|
|
413
|
-
{resolutionDisplay}
|
|
414
|
-
{captureButton}
|
|
415
|
-
</div>
|
|
536
|
+
{/* Subtle scrim so the bottom controls stay readable on bright scenes */}
|
|
537
|
+
<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" />
|
|
538
|
+
|
|
539
|
+
{/* Bottom-center: crop switcher, caption + shutter */}
|
|
540
|
+
<div className="pointer-events-none absolute right-0 bottom-5 left-0 flex flex-col items-center gap-2.5">
|
|
541
|
+
{!isPreset && (
|
|
542
|
+
<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">
|
|
543
|
+
{/* Clicking Standard while it's active opens the aspect picker */}
|
|
544
|
+
<ModeButton
|
|
545
|
+
active={mode === 'standard'}
|
|
546
|
+
badge={standardAspect}
|
|
547
|
+
icon={<Monitor className="h-3.5 w-3.5" />}
|
|
548
|
+
label={isMobile ? undefined : 'Standard'}
|
|
549
|
+
onClick={() => {
|
|
550
|
+
if (mode === 'standard') setAspectMenuOpen((v) => !v)
|
|
551
|
+
else setAspectMenuOpen(false)
|
|
552
|
+
setMode('standard')
|
|
553
|
+
setDrag(null)
|
|
554
|
+
}}
|
|
555
|
+
/>
|
|
556
|
+
{aspectMenuOpen && mode === 'standard' && (
|
|
557
|
+
<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">
|
|
558
|
+
{(Object.keys(STANDARD_SIZES) as StandardAspect[]).map((aspect) => (
|
|
559
|
+
<button
|
|
560
|
+
className={`rounded-full px-2.5 py-1 font-mono text-[11px] transition-colors ${
|
|
561
|
+
aspect === standardAspect
|
|
562
|
+
? 'bg-white/15 text-white ring-1 ring-white/20'
|
|
563
|
+
: 'text-white/55 hover:text-white/90'
|
|
564
|
+
}`}
|
|
565
|
+
key={aspect}
|
|
566
|
+
onClick={() => {
|
|
567
|
+
setStandardAspect(aspect)
|
|
568
|
+
setAspectMenuOpen(false)
|
|
569
|
+
}}
|
|
570
|
+
type="button"
|
|
571
|
+
>
|
|
572
|
+
{aspect}
|
|
573
|
+
</button>
|
|
574
|
+
))}
|
|
416
575
|
</div>
|
|
417
|
-
)
|
|
418
|
-
|
|
576
|
+
)}
|
|
577
|
+
<ModeButton
|
|
578
|
+
active={mode === 'viewport'}
|
|
579
|
+
icon={<Maximize2 className="h-3.5 w-3.5" />}
|
|
580
|
+
label={isMobile ? undefined : 'Viewport'}
|
|
581
|
+
onClick={() => {
|
|
582
|
+
setMode('viewport')
|
|
583
|
+
setDrag(null)
|
|
584
|
+
setAspectMenuOpen(false)
|
|
585
|
+
}}
|
|
586
|
+
/>
|
|
587
|
+
<ModeButton
|
|
588
|
+
active={mode === 'area'}
|
|
589
|
+
icon={<Crop className="h-3.5 w-3.5" />}
|
|
590
|
+
label={isMobile ? undefined : 'Area'}
|
|
591
|
+
onClick={() => {
|
|
592
|
+
setMode('area')
|
|
593
|
+
setAspectMenuOpen(false)
|
|
594
|
+
}}
|
|
595
|
+
/>
|
|
596
|
+
{isMobile && (
|
|
597
|
+
<span className="px-2 text-white/50 text-xs tabular-nums">
|
|
598
|
+
{resolution ? `${resolution.w} × ${resolution.h}` : '—'}
|
|
599
|
+
</span>
|
|
600
|
+
)}
|
|
601
|
+
</div>
|
|
602
|
+
)}
|
|
603
|
+
|
|
604
|
+
{/* Preset captures carry their own "Frame your item" banner — the
|
|
605
|
+
snapshot pitch only applies to the studio/reference flow. */}
|
|
606
|
+
{!isMobile && !isPreset && (
|
|
607
|
+
<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">
|
|
608
|
+
A <b className="font-semibold text-white">snapshot</b>
|
|
609
|
+
{' freezes this exact camera angle as a reusable reference for renders & videos.'}
|
|
610
|
+
</span>
|
|
611
|
+
)}
|
|
419
612
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
613
|
+
<button
|
|
614
|
+
aria-label={isPreset ? 'Capture' : 'Take snapshot'}
|
|
615
|
+
className="group pointer-events-auto relative grid h-14 w-14 place-items-center rounded-full disabled:opacity-50"
|
|
616
|
+
disabled={captureDisabled}
|
|
617
|
+
onClick={handleCapture}
|
|
618
|
+
type="button"
|
|
619
|
+
>
|
|
620
|
+
<span className="absolute inset-0 rounded-full border-[3px] border-white shadow-lg" />
|
|
621
|
+
<span
|
|
622
|
+
className={`h-10 w-10 rounded-full transition-transform duration-100 ${
|
|
623
|
+
captureState === 'capturing'
|
|
624
|
+
? 'scale-[0.55] bg-primary'
|
|
625
|
+
: 'bg-white group-hover:scale-90 group-active:scale-75'
|
|
626
|
+
}`}
|
|
627
|
+
/>
|
|
628
|
+
{captureState === 'capturing' && (
|
|
629
|
+
<Loader2 className="absolute h-4 w-4 animate-spin text-white" />
|
|
630
|
+
)}
|
|
631
|
+
{captureState === 'saved' && (
|
|
632
|
+
<Check className="absolute h-5 w-5 stroke-3 text-neutral-900" />
|
|
633
|
+
)}
|
|
634
|
+
</button>
|
|
635
|
+
<span className="pointer-events-none font-mono text-[10.5px] text-white uppercase tracking-[0.12em] drop-shadow">
|
|
636
|
+
{captureState === 'capturing'
|
|
637
|
+
? 'Capturing…'
|
|
638
|
+
: captureState === 'saved'
|
|
639
|
+
? 'Saved'
|
|
640
|
+
: isPreset
|
|
641
|
+
? 'Capture'
|
|
642
|
+
: 'Take snapshot'}
|
|
643
|
+
</span>
|
|
430
644
|
</div>
|
|
431
645
|
</div>
|
|
432
646
|
)
|
|
@@ -441,7 +655,7 @@ function ModeButton({
|
|
|
441
655
|
}: {
|
|
442
656
|
active: boolean
|
|
443
657
|
icon: React.ReactNode
|
|
444
|
-
label
|
|
658
|
+
label?: string
|
|
445
659
|
badge?: string
|
|
446
660
|
onClick: () => void
|
|
447
661
|
}) {
|
|
@@ -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
|
+
}
|