@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
|
@@ -6,17 +6,45 @@ import {
|
|
|
6
6
|
type AnyNodeId,
|
|
7
7
|
type BuildingNode,
|
|
8
8
|
emitter,
|
|
9
|
+
getLevelDisplayName,
|
|
9
10
|
type LevelNode,
|
|
10
11
|
useScene,
|
|
11
12
|
type ZoneNode,
|
|
12
13
|
} from '@pascal-app/core'
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
CLAY_PALETTE,
|
|
16
|
+
type EdgeMode,
|
|
17
|
+
getSceneTheme,
|
|
18
|
+
SCENE_THEMES,
|
|
19
|
+
useViewer,
|
|
20
|
+
} from '@pascal-app/viewer'
|
|
21
|
+
import {
|
|
22
|
+
ArrowLeft,
|
|
23
|
+
Box,
|
|
24
|
+
Camera,
|
|
25
|
+
Check,
|
|
26
|
+
ChevronRight,
|
|
27
|
+
Diamond,
|
|
28
|
+
Footprints,
|
|
29
|
+
Layers,
|
|
30
|
+
Palette,
|
|
31
|
+
PenLine,
|
|
32
|
+
Sparkles,
|
|
33
|
+
Square,
|
|
34
|
+
} from 'lucide-react'
|
|
16
35
|
import Link from 'next/link'
|
|
36
|
+
import { flushSync } from 'react-dom'
|
|
17
37
|
import { useShallow } from 'zustand/react/shallow'
|
|
18
38
|
import { cn } from '../lib/utils'
|
|
39
|
+
import useEditor from '../store/use-editor'
|
|
19
40
|
import { ActionButton } from './ui/action-menu/action-button'
|
|
41
|
+
import {
|
|
42
|
+
DropdownMenu,
|
|
43
|
+
DropdownMenuContent,
|
|
44
|
+
DropdownMenuItem,
|
|
45
|
+
DropdownMenuSeparator,
|
|
46
|
+
DropdownMenuTrigger,
|
|
47
|
+
} from './ui/primitives/dropdown-menu'
|
|
20
48
|
import { TooltipProvider } from './ui/primitives/tooltip'
|
|
21
49
|
|
|
22
50
|
type ProjectOwner = {
|
|
@@ -26,6 +54,24 @@ type ProjectOwner = {
|
|
|
26
54
|
image: string | null
|
|
27
55
|
}
|
|
28
56
|
|
|
57
|
+
function requestWalkthroughPointerLock() {
|
|
58
|
+
const canvas = document.querySelector<HTMLCanvasElement>('[data-pascal-viewer-3d] canvas')
|
|
59
|
+
if (!canvas) return
|
|
60
|
+
|
|
61
|
+
if (!canvas.hasAttribute('tabindex')) {
|
|
62
|
+
canvas.tabIndex = -1
|
|
63
|
+
}
|
|
64
|
+
canvas.focus({ preventScroll: true })
|
|
65
|
+
|
|
66
|
+
if (document.pointerLockElement === canvas) return
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
canvas.requestPointerLock?.()
|
|
70
|
+
} catch {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
|
|
30
76
|
stacked: 'Stacked',
|
|
31
77
|
exploded: 'Exploded',
|
|
@@ -42,22 +88,188 @@ const levelModeBadgeLabels: Record<'manual' | 'stacked' | 'exploded' | 'solo', s
|
|
|
42
88
|
const wallModeConfig = {
|
|
43
89
|
up: {
|
|
44
90
|
icon: (props: any) => (
|
|
45
|
-
<img alt="Full Height" height={28} src="/icons/room.
|
|
91
|
+
<img alt="Full Height" height={28} src="/icons/room.webp" width={28} {...props} />
|
|
46
92
|
),
|
|
47
93
|
label: 'Full Height',
|
|
48
94
|
},
|
|
49
95
|
cutaway: {
|
|
50
96
|
icon: (props: any) => (
|
|
51
|
-
<img alt="Cutaway" height={28} src="/icons/wallcut.
|
|
97
|
+
<img alt="Cutaway" height={28} src="/icons/wallcut.webp" width={28} {...props} />
|
|
52
98
|
),
|
|
53
99
|
label: 'Cutaway',
|
|
54
100
|
},
|
|
55
101
|
down: {
|
|
56
102
|
icon: (props: any) => (
|
|
57
|
-
<img alt="Low" height={28} src="/icons/walllow.
|
|
103
|
+
<img alt="Low" height={28} src="/icons/walllow.webp" width={28} {...props} />
|
|
58
104
|
),
|
|
59
105
|
label: 'Low',
|
|
60
106
|
},
|
|
107
|
+
translucent: {
|
|
108
|
+
icon: (props: any) => (
|
|
109
|
+
<img alt="Translucent" height={28} src="/icons/wall.png" width={28} {...props} />
|
|
110
|
+
),
|
|
111
|
+
label: 'Translucent',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const SHADING_OPTIONS = [
|
|
116
|
+
{ id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box },
|
|
117
|
+
{ id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
|
|
118
|
+
] as const
|
|
119
|
+
|
|
120
|
+
const TEXTURE_OPTIONS = [
|
|
121
|
+
{ id: true, name: 'Colored', detail: 'Show materials, textures & colors', icon: Palette },
|
|
122
|
+
{ id: false, name: 'Monochrome', detail: 'Flat clay surfaces by role', icon: Square },
|
|
123
|
+
] as const
|
|
124
|
+
|
|
125
|
+
function RenderModeMenu() {
|
|
126
|
+
const shading = useViewer((s) => s.shading)
|
|
127
|
+
const textures = useViewer((s) => s.textures)
|
|
128
|
+
const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
|
|
129
|
+
const ActiveIcon = active.icon
|
|
130
|
+
return (
|
|
131
|
+
<DropdownMenu>
|
|
132
|
+
<DropdownMenuTrigger asChild>
|
|
133
|
+
<ActionButton
|
|
134
|
+
className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
|
|
135
|
+
label={`Render: ${active.name}`}
|
|
136
|
+
size="icon"
|
|
137
|
+
tooltipSide="top"
|
|
138
|
+
variant="ghost"
|
|
139
|
+
>
|
|
140
|
+
<ActiveIcon className="h-6 w-6" />
|
|
141
|
+
</ActionButton>
|
|
142
|
+
</DropdownMenuTrigger>
|
|
143
|
+
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
144
|
+
{SHADING_OPTIONS.map((option) => {
|
|
145
|
+
const OptionIcon = option.icon
|
|
146
|
+
return (
|
|
147
|
+
<DropdownMenuItem
|
|
148
|
+
key={option.id}
|
|
149
|
+
onSelect={() => useViewer.getState().setShading(option.id)}
|
|
150
|
+
>
|
|
151
|
+
<OptionIcon />
|
|
152
|
+
<div className="flex flex-col">
|
|
153
|
+
<span className="text-foreground">{option.name}</span>
|
|
154
|
+
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
155
|
+
</div>
|
|
156
|
+
{shading === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
157
|
+
</DropdownMenuItem>
|
|
158
|
+
)
|
|
159
|
+
})}
|
|
160
|
+
<DropdownMenuSeparator />
|
|
161
|
+
{TEXTURE_OPTIONS.map((option) => {
|
|
162
|
+
const OptionIcon = option.icon
|
|
163
|
+
return (
|
|
164
|
+
<DropdownMenuItem
|
|
165
|
+
key={option.name}
|
|
166
|
+
onSelect={() => useViewer.getState().setTextures(option.id)}
|
|
167
|
+
>
|
|
168
|
+
<OptionIcon />
|
|
169
|
+
<div className="flex flex-col">
|
|
170
|
+
<span className="text-foreground">{option.name}</span>
|
|
171
|
+
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
172
|
+
</div>
|
|
173
|
+
{textures === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
174
|
+
</DropdownMenuItem>
|
|
175
|
+
)
|
|
176
|
+
})}
|
|
177
|
+
</DropdownMenuContent>
|
|
178
|
+
</DropdownMenu>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function SceneThemeMenu() {
|
|
183
|
+
const sceneTheme = useViewer((s) => s.sceneTheme)
|
|
184
|
+
const active = getSceneTheme(sceneTheme)
|
|
185
|
+
return (
|
|
186
|
+
<DropdownMenu>
|
|
187
|
+
<DropdownMenuTrigger asChild>
|
|
188
|
+
<ActionButton
|
|
189
|
+
className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
|
|
190
|
+
label={`Theme: ${active.name}`}
|
|
191
|
+
size="icon"
|
|
192
|
+
tooltipSide="top"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
>
|
|
195
|
+
<Icon color="currentColor" height={24} icon="lucide:swatch-book" width={24} />
|
|
196
|
+
</ActionButton>
|
|
197
|
+
</DropdownMenuTrigger>
|
|
198
|
+
<DropdownMenuContent align="center" className="min-w-48" side="top">
|
|
199
|
+
{SCENE_THEMES.map((sceneThemeOption) => {
|
|
200
|
+
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
|
201
|
+
(role) => sceneThemeOption.clayTints?.[role] ?? CLAY_PALETTE[role],
|
|
202
|
+
)
|
|
203
|
+
return (
|
|
204
|
+
<DropdownMenuItem
|
|
205
|
+
key={sceneThemeOption.id}
|
|
206
|
+
onSelect={() => useViewer.getState().setSceneTheme(sceneThemeOption.id)}
|
|
207
|
+
>
|
|
208
|
+
<span
|
|
209
|
+
className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
|
|
210
|
+
style={{ backgroundColor: sceneThemeOption.background }}
|
|
211
|
+
>
|
|
212
|
+
{swatches.map((color, index) => (
|
|
213
|
+
<span
|
|
214
|
+
key={`${sceneThemeOption.id}-${index}`}
|
|
215
|
+
style={{ backgroundColor: color }}
|
|
216
|
+
/>
|
|
217
|
+
))}
|
|
218
|
+
</span>
|
|
219
|
+
<span className="text-foreground">{sceneThemeOption.name}</span>
|
|
220
|
+
{sceneTheme === sceneThemeOption.id ? (
|
|
221
|
+
<Check className="ml-auto text-foreground" />
|
|
222
|
+
) : null}
|
|
223
|
+
</DropdownMenuItem>
|
|
224
|
+
)
|
|
225
|
+
})}
|
|
226
|
+
</DropdownMenuContent>
|
|
227
|
+
</DropdownMenu>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const EDGE_OPTIONS = [
|
|
232
|
+
{ id: 'off', name: 'Off', detail: 'No edge lines' },
|
|
233
|
+
{ id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' },
|
|
234
|
+
{ id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
|
|
235
|
+
] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
|
|
236
|
+
|
|
237
|
+
function EdgesMenu() {
|
|
238
|
+
const edges = useViewer((s) => s.edges)
|
|
239
|
+
const active = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0]
|
|
240
|
+
return (
|
|
241
|
+
<DropdownMenu>
|
|
242
|
+
<DropdownMenuTrigger asChild>
|
|
243
|
+
<ActionButton
|
|
244
|
+
className={
|
|
245
|
+
edges === 'off'
|
|
246
|
+
? 'text-muted-foreground/80 hover:bg-white/5 hover:text-foreground'
|
|
247
|
+
: 'bg-white/10 text-foreground'
|
|
248
|
+
}
|
|
249
|
+
label={`Edges: ${active.name}`}
|
|
250
|
+
size="icon"
|
|
251
|
+
tooltipSide="top"
|
|
252
|
+
variant="ghost"
|
|
253
|
+
>
|
|
254
|
+
<PenLine className="h-6 w-6" />
|
|
255
|
+
</ActionButton>
|
|
256
|
+
</DropdownMenuTrigger>
|
|
257
|
+
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
258
|
+
{EDGE_OPTIONS.map((option) => (
|
|
259
|
+
<DropdownMenuItem
|
|
260
|
+
key={option.id}
|
|
261
|
+
onSelect={() => useViewer.getState().setEdges(option.id)}
|
|
262
|
+
>
|
|
263
|
+
<div className="flex flex-col">
|
|
264
|
+
<span className="text-foreground">{option.name}</span>
|
|
265
|
+
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
266
|
+
</div>
|
|
267
|
+
{edges === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
268
|
+
</DropdownMenuItem>
|
|
269
|
+
))}
|
|
270
|
+
</DropdownMenuContent>
|
|
271
|
+
</DropdownMenu>
|
|
272
|
+
)
|
|
61
273
|
}
|
|
62
274
|
|
|
63
275
|
const getNodeName = (node: AnyNode): string => {
|
|
@@ -93,7 +305,6 @@ export const ViewerOverlay = ({
|
|
|
93
305
|
const cameraMode = useViewer((s) => s.cameraMode)
|
|
94
306
|
const levelMode = useViewer((s) => s.levelMode)
|
|
95
307
|
const wallMode = useViewer((s) => s.wallMode)
|
|
96
|
-
const theme = useViewer((s) => s.theme)
|
|
97
308
|
|
|
98
309
|
// Subscribe only to the specific nodes we read so that creating an unrelated
|
|
99
310
|
// node elsewhere in the scene doesn't re-render this overlay.
|
|
@@ -206,7 +417,7 @@ export const ViewerOverlay = ({
|
|
|
206
417
|
className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
|
|
207
418
|
onClick={() => handleBreadcrumbClick('level')}
|
|
208
419
|
>
|
|
209
|
-
{level
|
|
420
|
+
{getLevelDisplayName(level)}
|
|
210
421
|
</button>
|
|
211
422
|
</>
|
|
212
423
|
)}
|
|
@@ -280,53 +491,6 @@ export const ViewerOverlay = ({
|
|
|
280
491
|
<div className="dark absolute bottom-6 left-1/2 z-20 -translate-x-1/2 text-foreground">
|
|
281
492
|
<TooltipProvider delayDuration={0}>
|
|
282
493
|
<div className="pointer-events-auto flex h-14 flex-row items-center justify-center gap-1.5 rounded-2xl border border-border/40 bg-background/95 p-1.5 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
283
|
-
{/* Theme Toggle */}
|
|
284
|
-
<button
|
|
285
|
-
aria-label="Toggle theme"
|
|
286
|
-
className="flex h-[36px] shrink-0 cursor-pointer items-center rounded-full border border-border/50 bg-accent/50 p-1"
|
|
287
|
-
onClick={() => useViewer.getState().setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
288
|
-
type="button"
|
|
289
|
-
>
|
|
290
|
-
<div className="relative flex">
|
|
291
|
-
{/* Sliding Background */}
|
|
292
|
-
<motion.div
|
|
293
|
-
animate={{
|
|
294
|
-
x: theme === 'light' ? '100%' : '0%',
|
|
295
|
-
}}
|
|
296
|
-
className="absolute inset-0 rounded-full bg-white shadow-sm dark:bg-white/20"
|
|
297
|
-
initial={false}
|
|
298
|
-
style={{ width: '50%' }}
|
|
299
|
-
transition={{
|
|
300
|
-
type: 'spring',
|
|
301
|
-
stiffness: 500,
|
|
302
|
-
damping: 35,
|
|
303
|
-
}}
|
|
304
|
-
/>
|
|
305
|
-
|
|
306
|
-
{/* Dark Mode Icon */}
|
|
307
|
-
<div
|
|
308
|
-
className={cn(
|
|
309
|
-
'pointer-events-none relative z-10 flex h-7 w-9 items-center justify-center rounded-full transition-colors duration-200',
|
|
310
|
-
theme === 'dark' ? 'text-foreground' : 'text-muted-foreground',
|
|
311
|
-
)}
|
|
312
|
-
>
|
|
313
|
-
<Moon className="h-4 w-4" />
|
|
314
|
-
</div>
|
|
315
|
-
|
|
316
|
-
{/* Light Mode Icon */}
|
|
317
|
-
<div
|
|
318
|
-
className={cn(
|
|
319
|
-
'pointer-events-none relative z-10 flex h-7 w-9 items-center justify-center rounded-full transition-colors duration-200',
|
|
320
|
-
theme === 'light' ? 'text-foreground' : 'text-muted-foreground',
|
|
321
|
-
)}
|
|
322
|
-
>
|
|
323
|
-
<Sun className="h-4 w-4" />
|
|
324
|
-
</div>
|
|
325
|
-
</div>
|
|
326
|
-
</button>
|
|
327
|
-
|
|
328
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
329
|
-
|
|
330
494
|
{/* Scans and Guides Visibility */}
|
|
331
495
|
{canShowScans && (
|
|
332
496
|
<ActionButton
|
|
@@ -344,7 +508,7 @@ export const ViewerOverlay = ({
|
|
|
344
508
|
<img
|
|
345
509
|
alt="Scans"
|
|
346
510
|
className="h-[28px] w-[28px] object-contain"
|
|
347
|
-
src="/icons/mesh.
|
|
511
|
+
src="/icons/mesh.webp"
|
|
348
512
|
/>
|
|
349
513
|
</ActionButton>
|
|
350
514
|
)}
|
|
@@ -365,7 +529,7 @@ export const ViewerOverlay = ({
|
|
|
365
529
|
<img
|
|
366
530
|
alt="Guides"
|
|
367
531
|
className="h-[28px] w-[28px] object-contain"
|
|
368
|
-
src="/icons/floorplan.
|
|
532
|
+
src="/icons/floorplan.webp"
|
|
369
533
|
/>
|
|
370
534
|
</ActionButton>
|
|
371
535
|
)}
|
|
@@ -392,6 +556,12 @@ export const ViewerOverlay = ({
|
|
|
392
556
|
<Camera className="h-6 w-6" />
|
|
393
557
|
</ActionButton>
|
|
394
558
|
|
|
559
|
+
<RenderModeMenu />
|
|
560
|
+
|
|
561
|
+
<SceneThemeMenu />
|
|
562
|
+
|
|
563
|
+
<EdgesMenu />
|
|
564
|
+
|
|
395
565
|
{/* Level Mode */}
|
|
396
566
|
<ActionButton
|
|
397
567
|
className={cn(
|
|
@@ -437,7 +607,12 @@ export const ViewerOverlay = ({
|
|
|
437
607
|
}
|
|
438
608
|
label={`Walls: ${wallModeConfig[wallMode as keyof typeof wallModeConfig].label}`}
|
|
439
609
|
onClick={() => {
|
|
440
|
-
const modes: ('cutaway' | 'up' | 'down')[] = [
|
|
610
|
+
const modes: ('cutaway' | 'up' | 'down' | 'translucent')[] = [
|
|
611
|
+
'cutaway',
|
|
612
|
+
'up',
|
|
613
|
+
'down',
|
|
614
|
+
'translucent',
|
|
615
|
+
]
|
|
441
616
|
const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length
|
|
442
617
|
useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
|
|
443
618
|
}}
|
|
@@ -465,7 +640,7 @@ export const ViewerOverlay = ({
|
|
|
465
640
|
<img
|
|
466
641
|
alt="Orbit Left"
|
|
467
642
|
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
468
|
-
src="/icons/rotate.
|
|
643
|
+
src="/icons/rotate.webp"
|
|
469
644
|
/>
|
|
470
645
|
</ActionButton>
|
|
471
646
|
|
|
@@ -480,7 +655,7 @@ export const ViewerOverlay = ({
|
|
|
480
655
|
<img
|
|
481
656
|
alt="Orbit Right"
|
|
482
657
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
483
|
-
src="/icons/rotate.
|
|
658
|
+
src="/icons/rotate.webp"
|
|
484
659
|
/>
|
|
485
660
|
</ActionButton>
|
|
486
661
|
|
|
@@ -495,9 +670,26 @@ export const ViewerOverlay = ({
|
|
|
495
670
|
<img
|
|
496
671
|
alt="Top View"
|
|
497
672
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
498
|
-
src="/icons/topview.
|
|
673
|
+
src="/icons/topview.webp"
|
|
499
674
|
/>
|
|
500
675
|
</ActionButton>
|
|
676
|
+
|
|
677
|
+
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
678
|
+
|
|
679
|
+
{/* First-person walkthrough */}
|
|
680
|
+
<ActionButton
|
|
681
|
+
className="hover:bg-white/5 hover:text-emerald-400"
|
|
682
|
+
label="Walkthrough"
|
|
683
|
+
onClick={() => {
|
|
684
|
+
flushSync(() => useEditor.getState().setFirstPersonMode(true))
|
|
685
|
+
requestWalkthroughPointerLock()
|
|
686
|
+
}}
|
|
687
|
+
size="icon"
|
|
688
|
+
tooltipSide="top"
|
|
689
|
+
variant="ghost"
|
|
690
|
+
>
|
|
691
|
+
<Footprints className="h-6 w-6" />
|
|
692
|
+
</ActionButton>
|
|
501
693
|
</div>
|
|
502
694
|
</TooltipProvider>
|
|
503
695
|
</div>
|
|
@@ -4,15 +4,20 @@ import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
|
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { useFrame } from '@react-three/fiber'
|
|
6
6
|
import type { Mesh } from 'three'
|
|
7
|
+
import { resolveOverlayPolicy } from '../lib/interaction/overlay-policy'
|
|
7
8
|
import useEditor from '../store/use-editor'
|
|
9
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
8
10
|
|
|
9
11
|
export const ViewerZoneSystem = () => {
|
|
10
12
|
useFrame(() => {
|
|
11
13
|
const { levelId, zoneId } = useViewer.getState().selection
|
|
12
14
|
const structureLayer = useEditor.getState().structureLayer
|
|
13
15
|
const nodes = useScene.getState().nodes
|
|
16
|
+
// During any active interaction zone labels step back entirely (Sims-light).
|
|
17
|
+
const zoneLabelsHidden =
|
|
18
|
+
resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
|
|
14
19
|
|
|
15
|
-
sceneRegistry.byType.zone
|
|
20
|
+
sceneRegistry.byType.zone!.forEach((id) => {
|
|
16
21
|
const obj = sceneRegistry.nodes.get(id)
|
|
17
22
|
if (!obj) return
|
|
18
23
|
|
|
@@ -35,7 +40,7 @@ export const ViewerZoneSystem = () => {
|
|
|
35
40
|
})
|
|
36
41
|
|
|
37
42
|
// Labels: always visible on the current level (regardless of mode or zone selection)
|
|
38
|
-
const showLabel = !!levelId && isOnSelectedLevel
|
|
43
|
+
const showLabel = !zoneLabelsHidden && !!levelId && isOnSelectedLevel
|
|
39
44
|
const targetOpacity = showLabel ? '1' : '0'
|
|
40
45
|
const labelEl = document.getElementById(`${id}-label`)
|
|
41
46
|
if (labelEl && labelEl.style.opacity !== targetOpacity) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { isSuspiciousNodeDrop } from './use-auto-save'
|
|
3
|
+
|
|
4
|
+
describe('isSuspiciousNodeDrop', () => {
|
|
5
|
+
test('blocks populated scenes from being flushed as empty skeletons', () => {
|
|
6
|
+
expect(isSuspiciousNodeDrop(12, 0)).toBe(true)
|
|
7
|
+
expect(isSuspiciousNodeDrop(12, 4)).toBe(true)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('allows ordinary edits and intentionally empty starting scenes', () => {
|
|
11
|
+
expect(isSuspiciousNodeDrop(12, 11)).toBe(false)
|
|
12
|
+
expect(isSuspiciousNodeDrop(4, 0)).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -5,11 +5,16 @@ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
|
|
|
5
5
|
import { type SceneGraph, saveSceneToLocalStorage } from '../lib/scene'
|
|
6
6
|
|
|
7
7
|
const AUTOSAVE_DEBOUNCE_MS = 1000
|
|
8
|
+
const STRUCTURAL_NODE_COUNT = 4
|
|
9
|
+
|
|
10
|
+
export function isSuspiciousNodeDrop(previousNodeCount: number, currentNodeCount: number) {
|
|
11
|
+
return previousNodeCount > STRUCTURAL_NODE_COUNT && currentNodeCount <= STRUCTURAL_NODE_COUNT
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
export type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'paused' | 'error'
|
|
10
15
|
|
|
11
16
|
interface UseAutoSaveOptions {
|
|
12
|
-
onSave?: (scene: SceneGraph) => Promise<void>
|
|
17
|
+
onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
|
|
13
18
|
onDirty?: () => void
|
|
14
19
|
onSaveStatusChange?: (status: SaveStatus) => void
|
|
15
20
|
isVersionPreviewMode?: boolean
|
|
@@ -61,6 +66,13 @@ export function useAutoSave({
|
|
|
61
66
|
useEffect(() => {
|
|
62
67
|
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
|
|
63
68
|
let lastNodeCount = Object.keys(useScene.getState().nodes).length
|
|
69
|
+
// Collections + scene materials are document-level state that persists with
|
|
70
|
+
// the graph but lives outside `nodes`. Track them by reference (zustand
|
|
71
|
+
// hands out a new object on every mutation) so a material edit or a
|
|
72
|
+
// collection change still triggers a save.
|
|
73
|
+
let lastCollectionsRef = useScene.getState().collections
|
|
74
|
+
let lastMaterialsRef = useScene.getState().materials
|
|
75
|
+
let lastInstalledPluginsRef = useScene.getState().installedPlugins
|
|
64
76
|
|
|
65
77
|
async function executeSave() {
|
|
66
78
|
if (isLoadingSceneRef.current || isVersionPreviewModeRef.current) {
|
|
@@ -69,14 +81,19 @@ export function useAutoSave({
|
|
|
69
81
|
return
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
const { nodes, rootNodeIds } = useScene.getState()
|
|
73
|
-
const sceneGraph = {
|
|
84
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } = useScene.getState()
|
|
85
|
+
const sceneGraph = {
|
|
86
|
+
nodes,
|
|
87
|
+
rootNodeIds,
|
|
88
|
+
collections,
|
|
89
|
+
materials,
|
|
90
|
+
installedPlugins,
|
|
91
|
+
} as SceneGraph
|
|
74
92
|
|
|
75
93
|
// Guard: refuse to autosave if the scene went from populated to nearly empty.
|
|
76
94
|
// This catches accidental full deletions before they're persisted.
|
|
77
95
|
const currentNodeCount = Object.keys(nodes).length
|
|
78
|
-
|
|
79
|
-
if (lastNodeCount > STRUCTURAL_NODE_COUNT && currentNodeCount <= STRUCTURAL_NODE_COUNT) {
|
|
96
|
+
if (isSuspiciousNodeDrop(lastNodeCount, currentNodeCount)) {
|
|
80
97
|
console.warn(
|
|
81
98
|
`[autosave] Blocked: scene dropped from ${lastNodeCount} to ${currentNodeCount} nodes. Likely accidental deletion.`,
|
|
82
99
|
)
|
|
@@ -118,19 +135,33 @@ export function useAutoSave({
|
|
|
118
135
|
const unsubscribe = useScene.subscribe((state) => {
|
|
119
136
|
if (isLoadingSceneRef.current) {
|
|
120
137
|
lastNodesSnapshot = JSON.stringify(state.nodes)
|
|
138
|
+
lastCollectionsRef = state.collections
|
|
139
|
+
lastMaterialsRef = state.materials
|
|
140
|
+
lastInstalledPluginsRef = state.installedPlugins
|
|
121
141
|
return
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
if (isVersionPreviewModeRef.current) {
|
|
125
145
|
setSaveStatus('paused')
|
|
126
146
|
lastNodesSnapshot = JSON.stringify(state.nodes)
|
|
147
|
+
lastCollectionsRef = state.collections
|
|
148
|
+
lastMaterialsRef = state.materials
|
|
149
|
+
lastInstalledPluginsRef = state.installedPlugins
|
|
127
150
|
return
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
|
131
|
-
|
|
154
|
+
const changed =
|
|
155
|
+
currentNodesSnapshot !== lastNodesSnapshot ||
|
|
156
|
+
state.collections !== lastCollectionsRef ||
|
|
157
|
+
state.materials !== lastMaterialsRef ||
|
|
158
|
+
state.installedPlugins !== lastInstalledPluginsRef
|
|
159
|
+
if (!changed) return
|
|
132
160
|
|
|
133
161
|
lastNodesSnapshot = currentNodesSnapshot
|
|
162
|
+
lastCollectionsRef = state.collections
|
|
163
|
+
lastMaterialsRef = state.materials
|
|
164
|
+
lastInstalledPluginsRef = state.installedPlugins
|
|
134
165
|
hasDirtyChangesRef.current = true
|
|
135
166
|
onDirtyRef.current?.()
|
|
136
167
|
setSaveStatus('pending')
|
|
@@ -148,23 +179,46 @@ export function useAutoSave({
|
|
|
148
179
|
}, AUTOSAVE_DEBOUNCE_MS)
|
|
149
180
|
})
|
|
150
181
|
|
|
182
|
+
// Flush any unsaved change while the page is going away. The network
|
|
183
|
+
// save MUST set `keepalive` — a normal fetch is cancelled by the browser
|
|
184
|
+
// the moment the page unloads, so a quick refresh right after an edit
|
|
185
|
+
// would otherwise drop the change entirely. `pagehide` fires in cases
|
|
186
|
+
// (mobile Safari, bfcache) where `beforeunload` does not.
|
|
151
187
|
function flushOnExit() {
|
|
152
188
|
if (!hasDirtyChangesRef.current) return
|
|
153
|
-
const { nodes, rootNodeIds } = useScene.getState()
|
|
154
|
-
const
|
|
189
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } = useScene.getState()
|
|
190
|
+
const currentNodeCount = Object.keys(nodes).length
|
|
191
|
+
if (isSuspiciousNodeDrop(lastNodeCount, currentNodeCount)) {
|
|
192
|
+
console.warn(
|
|
193
|
+
`[autosave] Blocked unload flush: scene dropped from ${lastNodeCount} to ${currentNodeCount} nodes. Likely accidental deletion.`,
|
|
194
|
+
)
|
|
195
|
+
setSaveStatus('error')
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
hasDirtyChangesRef.current = false
|
|
200
|
+
lastNodeCount = currentNodeCount
|
|
201
|
+
const sceneGraph = {
|
|
202
|
+
nodes,
|
|
203
|
+
rootNodeIds,
|
|
204
|
+
collections,
|
|
205
|
+
materials,
|
|
206
|
+
installedPlugins,
|
|
207
|
+
} as SceneGraph
|
|
155
208
|
if (onSaveRef.current) {
|
|
156
|
-
onSaveRef.current(sceneGraph).catch(() => {})
|
|
209
|
+
onSaveRef.current(sceneGraph, { keepalive: true }).catch(() => {})
|
|
157
210
|
} else {
|
|
158
211
|
saveSceneToLocalStorage(sceneGraph)
|
|
159
212
|
}
|
|
160
|
-
hasDirtyChangesRef.current = false
|
|
161
213
|
}
|
|
162
214
|
|
|
163
215
|
window.addEventListener('beforeunload', flushOnExit)
|
|
216
|
+
window.addEventListener('pagehide', flushOnExit)
|
|
164
217
|
|
|
165
218
|
return () => {
|
|
166
219
|
executeSaveRef.current = null
|
|
167
220
|
window.removeEventListener('beforeunload', flushOnExit)
|
|
221
|
+
window.removeEventListener('pagehide', flushOnExit)
|
|
168
222
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
|
169
223
|
flushOnExit()
|
|
170
224
|
unsubscribe()
|