@pascal-app/editor 0.9.1 → 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 +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- 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 +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- 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 +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- 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 +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- 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 +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- 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-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- 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 +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +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/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- 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 +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- 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/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 +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -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 +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -6,6 +6,7 @@ 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,
|
|
@@ -24,19 +25,24 @@ import {
|
|
|
24
25
|
Check,
|
|
25
26
|
ChevronRight,
|
|
26
27
|
Diamond,
|
|
28
|
+
Footprints,
|
|
27
29
|
Layers,
|
|
30
|
+
Palette,
|
|
28
31
|
PenLine,
|
|
29
32
|
Sparkles,
|
|
33
|
+
Square,
|
|
30
34
|
} from 'lucide-react'
|
|
31
35
|
import Link from 'next/link'
|
|
36
|
+
import { flushSync } from 'react-dom'
|
|
32
37
|
import { useShallow } from 'zustand/react/shallow'
|
|
33
|
-
import { getLevelDisplayName } from '../lib/level-name'
|
|
34
38
|
import { cn } from '../lib/utils'
|
|
39
|
+
import useEditor from '../store/use-editor'
|
|
35
40
|
import { ActionButton } from './ui/action-menu/action-button'
|
|
36
41
|
import {
|
|
37
42
|
DropdownMenu,
|
|
38
43
|
DropdownMenuContent,
|
|
39
44
|
DropdownMenuItem,
|
|
45
|
+
DropdownMenuSeparator,
|
|
40
46
|
DropdownMenuTrigger,
|
|
41
47
|
} from './ui/primitives/dropdown-menu'
|
|
42
48
|
import { TooltipProvider } from './ui/primitives/tooltip'
|
|
@@ -48,6 +54,24 @@ type ProjectOwner = {
|
|
|
48
54
|
image: string | null
|
|
49
55
|
}
|
|
50
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
|
+
|
|
51
75
|
const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
|
|
52
76
|
stacked: 'Stacked',
|
|
53
77
|
exploded: 'Exploded',
|
|
@@ -64,22 +88,28 @@ const levelModeBadgeLabels: Record<'manual' | 'stacked' | 'exploded' | 'solo', s
|
|
|
64
88
|
const wallModeConfig = {
|
|
65
89
|
up: {
|
|
66
90
|
icon: (props: any) => (
|
|
67
|
-
<img alt="Full Height" height={28} src="/icons/room.
|
|
91
|
+
<img alt="Full Height" height={28} src="/icons/room.webp" width={28} {...props} />
|
|
68
92
|
),
|
|
69
93
|
label: 'Full Height',
|
|
70
94
|
},
|
|
71
95
|
cutaway: {
|
|
72
96
|
icon: (props: any) => (
|
|
73
|
-
<img alt="Cutaway" height={28} src="/icons/wallcut.
|
|
97
|
+
<img alt="Cutaway" height={28} src="/icons/wallcut.webp" width={28} {...props} />
|
|
74
98
|
),
|
|
75
99
|
label: 'Cutaway',
|
|
76
100
|
},
|
|
77
101
|
down: {
|
|
78
102
|
icon: (props: any) => (
|
|
79
|
-
<img alt="Low" height={28} src="/icons/walllow.
|
|
103
|
+
<img alt="Low" height={28} src="/icons/walllow.webp" width={28} {...props} />
|
|
80
104
|
),
|
|
81
105
|
label: 'Low',
|
|
82
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
|
+
},
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
const SHADING_OPTIONS = [
|
|
@@ -87,8 +117,14 @@ const SHADING_OPTIONS = [
|
|
|
87
117
|
{ id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
|
|
88
118
|
] as const
|
|
89
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
|
+
|
|
90
125
|
function RenderModeMenu() {
|
|
91
126
|
const shading = useViewer((s) => s.shading)
|
|
127
|
+
const textures = useViewer((s) => s.textures)
|
|
92
128
|
const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
|
|
93
129
|
const ActiveIcon = active.icon
|
|
94
130
|
return (
|
|
@@ -121,6 +157,23 @@ function RenderModeMenu() {
|
|
|
121
157
|
</DropdownMenuItem>
|
|
122
158
|
)
|
|
123
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
|
+
})}
|
|
124
177
|
</DropdownMenuContent>
|
|
125
178
|
</DropdownMenu>
|
|
126
179
|
)
|
|
@@ -455,7 +508,7 @@ export const ViewerOverlay = ({
|
|
|
455
508
|
<img
|
|
456
509
|
alt="Scans"
|
|
457
510
|
className="h-[28px] w-[28px] object-contain"
|
|
458
|
-
src="/icons/mesh.
|
|
511
|
+
src="/icons/mesh.webp"
|
|
459
512
|
/>
|
|
460
513
|
</ActionButton>
|
|
461
514
|
)}
|
|
@@ -476,7 +529,7 @@ export const ViewerOverlay = ({
|
|
|
476
529
|
<img
|
|
477
530
|
alt="Guides"
|
|
478
531
|
className="h-[28px] w-[28px] object-contain"
|
|
479
|
-
src="/icons/floorplan.
|
|
532
|
+
src="/icons/floorplan.webp"
|
|
480
533
|
/>
|
|
481
534
|
</ActionButton>
|
|
482
535
|
)}
|
|
@@ -554,7 +607,12 @@ export const ViewerOverlay = ({
|
|
|
554
607
|
}
|
|
555
608
|
label={`Walls: ${wallModeConfig[wallMode as keyof typeof wallModeConfig].label}`}
|
|
556
609
|
onClick={() => {
|
|
557
|
-
const modes: ('cutaway' | 'up' | 'down')[] = [
|
|
610
|
+
const modes: ('cutaway' | 'up' | 'down' | 'translucent')[] = [
|
|
611
|
+
'cutaway',
|
|
612
|
+
'up',
|
|
613
|
+
'down',
|
|
614
|
+
'translucent',
|
|
615
|
+
]
|
|
558
616
|
const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length
|
|
559
617
|
useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
|
|
560
618
|
}}
|
|
@@ -582,7 +640,7 @@ export const ViewerOverlay = ({
|
|
|
582
640
|
<img
|
|
583
641
|
alt="Orbit Left"
|
|
584
642
|
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
585
|
-
src="/icons/rotate.
|
|
643
|
+
src="/icons/rotate.webp"
|
|
586
644
|
/>
|
|
587
645
|
</ActionButton>
|
|
588
646
|
|
|
@@ -597,7 +655,7 @@ export const ViewerOverlay = ({
|
|
|
597
655
|
<img
|
|
598
656
|
alt="Orbit Right"
|
|
599
657
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
600
|
-
src="/icons/rotate.
|
|
658
|
+
src="/icons/rotate.webp"
|
|
601
659
|
/>
|
|
602
660
|
</ActionButton>
|
|
603
661
|
|
|
@@ -612,9 +670,26 @@ export const ViewerOverlay = ({
|
|
|
612
670
|
<img
|
|
613
671
|
alt="Top View"
|
|
614
672
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
615
|
-
src="/icons/topview.
|
|
673
|
+
src="/icons/topview.webp"
|
|
616
674
|
/>
|
|
617
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>
|
|
618
693
|
</div>
|
|
619
694
|
</TooltipProvider>
|
|
620
695
|
</div>
|
|
@@ -4,13 +4,18 @@ 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
20
|
sceneRegistry.byType.zone!.forEach((id) => {
|
|
16
21
|
const obj = sceneRegistry.nodes.get(id)
|
|
@@ -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,6 +5,11 @@ 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
|
|
|
@@ -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')
|
|
@@ -155,9 +186,25 @@ export function useAutoSave({
|
|
|
155
186
|
// (mobile Safari, bfcache) where `beforeunload` does not.
|
|
156
187
|
function flushOnExit() {
|
|
157
188
|
if (!hasDirtyChangesRef.current) return
|
|
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
|
+
|
|
158
199
|
hasDirtyChangesRef.current = false
|
|
159
|
-
|
|
160
|
-
const sceneGraph = {
|
|
200
|
+
lastNodeCount = currentNodeCount
|
|
201
|
+
const sceneGraph = {
|
|
202
|
+
nodes,
|
|
203
|
+
rootNodeIds,
|
|
204
|
+
collections,
|
|
205
|
+
materials,
|
|
206
|
+
installedPlugins,
|
|
207
|
+
} as SceneGraph
|
|
161
208
|
if (onSaveRef.current) {
|
|
162
209
|
onSaveRef.current(sceneGraph, { keepalive: true }).catch(() => {})
|
|
163
210
|
} else {
|
|
@@ -12,6 +12,7 @@ import { useThree } from '@react-three/fiber'
|
|
|
12
12
|
import { useEffect, useRef } from 'react'
|
|
13
13
|
import { type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
|
|
14
14
|
import useEditor from '../store/use-editor'
|
|
15
|
+
import { getMovingNode } from '../store/use-interaction-scope'
|
|
15
16
|
|
|
16
17
|
const UP = new Vector3(0, 1, 0)
|
|
17
18
|
|
|
@@ -59,7 +60,7 @@ export function useCeilingEvents() {
|
|
|
59
60
|
const isActive = (): boolean => {
|
|
60
61
|
const ed = useEditor.getState()
|
|
61
62
|
if (ed.selectedItem?.attachTo === 'ceiling') return true
|
|
62
|
-
const moving =
|
|
63
|
+
const moving = getMovingNode()
|
|
63
64
|
return moving?.type === 'item' && moving.asset?.attachTo === 'ceiling'
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -85,7 +86,7 @@ export function useCeilingEvents() {
|
|
|
85
86
|
|
|
86
87
|
for (const id of ceilingIds) {
|
|
87
88
|
const node = nodes[id as AnyNodeId] as CeilingNode | undefined
|
|
88
|
-
if (
|
|
89
|
+
if (node?.type !== 'ceiling' || node.polygon.length < 3) continue
|
|
89
90
|
if (resolveLevelId(node, nodes) !== activeLevelId) continue
|
|
90
91
|
const mesh = sceneRegistry.nodes.get(id)
|
|
91
92
|
if (!mesh) continue
|
|
@@ -19,7 +19,10 @@ import { useEffect, useRef } from 'react'
|
|
|
19
19
|
const sceneApi = createSceneApi(useScene)
|
|
20
20
|
|
|
21
21
|
function modifiersFromGridEvent(event: GridEvent): Modifiers {
|
|
22
|
-
|
|
22
|
+
// Both grid-event emit paths (use-grid-events.ts and the floorplan panel)
|
|
23
|
+
// store the raw DOM event directly at `.nativeEvent` — there is no second
|
|
24
|
+
// `.nativeEvent` hop.
|
|
25
|
+
const ne = event.nativeEvent as Partial<MouseEvent> | undefined
|
|
23
26
|
return {
|
|
24
27
|
shift: ne?.shiftKey ?? false,
|
|
25
28
|
alt: ne?.altKey ?? false,
|