camera for zones and levels

This commit is contained in:
wass08
2026-02-02 10:24:57 +09:00
parent ff4e2fc2fe
commit f98ead4585
4 changed files with 760 additions and 27 deletions
@@ -1,5 +1,6 @@
import { import {
type BuildingNode, type BuildingNode,
emitter,
LevelNode, LevelNode,
useScene, useScene,
type ZoneNode, type ZoneNode,
@@ -7,6 +8,7 @@ import {
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import { import {
Building2, Building2,
Camera,
ChevronDown, ChevronDown,
Layers, Layers,
MoreHorizontal, MoreHorizontal,
@@ -155,11 +157,13 @@ function BuildingSelector() {
function LevelsSection() { function LevelsSection() {
const nodes = useScene((state) => state.nodes); const nodes = useScene((state) => state.nodes);
const createNode = useScene((state) => state.createNode); const createNode = useScene((state) => state.createNode);
const updateNode = useScene((state) => state.updateNode);
const selectedBuildingId = useViewer((state) => state.selection.buildingId); const selectedBuildingId = useViewer((state) => state.selection.buildingId);
const selectedLevelId = useViewer((state) => state.selection.levelId); const selectedLevelId = useViewer((state) => state.selection.levelId);
const setSelection = useViewer((state) => state.setSelection); const setSelection = useViewer((state) => state.setSelection);
const [referencesLevelId, setReferencesLevelId] = useState<string | null>(null); const [referencesLevelId, setReferencesLevelId] = useState<string | null>(null);
const [cameraPopoverOpen, setCameraPopoverOpen] = useState<string | null>(null);
const building = selectedBuildingId const building = selectedBuildingId
? (nodes[selectedBuildingId] as BuildingNode) ? (nodes[selectedBuildingId] as BuildingNode)
@@ -215,6 +219,72 @@ function LevelsSection() {
<Layers className="w-3.5 h-3.5 shrink-0" /> <Layers className="w-3.5 h-3.5 shrink-0" />
<span className="truncate">{level.name || `Level ${level.level}`}</span> <span className="truncate">{level.name || `Level ${level.level}`}</span>
</button> </button>
{/* Camera snapshot button */}
<Popover open={cameraPopoverOpen === level.id} onOpenChange={(open) => setCameraPopoverOpen(open ? level.id : null)}>
<PopoverTrigger asChild>
<button
className={cn(
"relative opacity-0 group-hover/level:opacity-100 w-6 h-6 mr-1 flex items-center justify-center rounded cursor-pointer shrink-0",
selectedLevelId === level.id
? "hover:bg-primary-foreground/20"
: "hover:bg-accent"
)}
onClick={(e) => e.stopPropagation()}
title="Camera snapshot"
>
<Camera className="w-3.5 h-3.5" />
{level.camera && (
<span className="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-primary" />
)}
</button>
</PopoverTrigger>
<PopoverContent
side="right"
align="start"
className="w-auto p-1"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-0.5">
{level.camera && (
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
onClick={(e) => {
e.stopPropagation();
emitter.emit("camera-controls:view", { nodeId: level.id });
setCameraPopoverOpen(null);
}}
>
<Camera className="w-3.5 h-3.5" />
View snapshot
</button>
)}
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
onClick={(e) => {
e.stopPropagation();
emitter.emit("camera-controls:capture", { nodeId: level.id });
setCameraPopoverOpen(null);
}}
>
<Camera className="w-3.5 h-3.5" />
{level.camera ? "Update snapshot" : "Take snapshot"}
</button>
{level.camera && (
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-destructive hover:text-destructive-foreground text-left w-full"
onClick={(e) => {
e.stopPropagation();
updateNode(level.id, { camera: undefined });
setCameraPopoverOpen(null);
}}
>
<Trash2 className="w-3.5 h-3.5" />
Clear snapshot
</button>
)}
</div>
</PopoverContent>
</Popover>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<button <button
@@ -307,6 +377,7 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
function ZoneItem({ zone }: { zone: ZoneNode }) { function ZoneItem({ zone }: { zone: ZoneNode }) {
const [renameOpen, setRenameOpen] = useState(false); const [renameOpen, setRenameOpen] = useState(false);
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false);
const deleteNode = useScene((state) => state.deleteNode); const deleteNode = useScene((state) => state.deleteNode);
const updateNode = useScene((state) => state.updateNode); const updateNode = useScene((state) => state.updateNode);
const selectedZoneId = useViewer((state) => state.selection.zoneId); const selectedZoneId = useViewer((state) => state.selection.zoneId);
@@ -394,6 +465,67 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<span className="truncate flex-1">{zone.name || defaultName}</span> <span className="truncate flex-1">{zone.name || defaultName}</span>
{/* Camera snapshot button */}
<Popover open={cameraPopoverOpen} onOpenChange={setCameraPopoverOpen}>
<PopoverTrigger asChild>
<button
className="relative opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
onClick={(e) => e.stopPropagation()}
title="Camera snapshot"
>
<Camera className="w-3 h-3" />
{zone.camera && (
<span className="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-primary" />
)}
</button>
</PopoverTrigger>
<PopoverContent
side="right"
align="start"
className="w-auto p-1"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-0.5">
{zone.camera && (
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
onClick={(e) => {
e.stopPropagation();
emitter.emit("camera-controls:view", { nodeId: zone.id });
setCameraPopoverOpen(false);
}}
>
<Camera className="w-3.5 h-3.5" />
View snapshot
</button>
)}
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
onClick={(e) => {
e.stopPropagation();
emitter.emit("camera-controls:capture", { nodeId: zone.id });
setCameraPopoverOpen(false);
}}
>
<Camera className="w-3.5 h-3.5" />
{zone.camera ? "Update snapshot" : "Take snapshot"}
</button>
{zone.camera && (
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-destructive hover:text-destructive-foreground text-left w-full"
onClick={(e) => {
e.stopPropagation();
updateNode(zone.id, { camera: undefined });
setCameraPopoverOpen(false);
}}
>
<Trash2 className="w-3.5 h-3.5" />
Clear snapshot
</button>
)}
</div>
</PopoverContent>
</Popover>
<button <button
className="opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20" className="opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
onClick={handleDelete} onClick={handleDelete}
@@ -1,6 +1,7 @@
import { useScene, type ZoneNode } from "@pascal-app/core"; import { emitter, useScene, type ZoneNode } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import { Hexagon, Trash2 } from "lucide-react"; import { Camera, Hexagon, Trash2 } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import useEditor from "@/store/use-editor"; import useEditor from "@/store/use-editor";
import { import {
@@ -22,6 +23,7 @@ const PRESET_COLORS = [
]; ];
function ZoneItem({ zone }: { zone: ZoneNode }) { function ZoneItem({ zone }: { zone: ZoneNode }) {
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false);
const deleteNode = useScene((state) => state.deleteNode); const deleteNode = useScene((state) => state.deleteNode);
const updateNode = useScene((state) => state.updateNode); const updateNode = useScene((state) => state.updateNode);
const selectedZoneId = useViewer((state) => state.selection.zoneId); const selectedZoneId = useViewer((state) => state.selection.zoneId);
@@ -85,6 +87,67 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
</Popover> </Popover>
<Hexagon className="w-3.5 h-3.5 mr-1.5 shrink-0" /> <Hexagon className="w-3.5 h-3.5 mr-1.5 shrink-0" />
<span className="truncate flex-1">{zone.name}</span> <span className="truncate flex-1">{zone.name}</span>
{/* Camera snapshot button */}
<Popover open={cameraPopoverOpen} onOpenChange={setCameraPopoverOpen}>
<PopoverTrigger asChild>
<button
className="relative opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
onClick={(e) => e.stopPropagation()}
title="Camera snapshot"
>
<Camera className="w-3 h-3" />
{zone.camera && (
<span className="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-primary" />
)}
</button>
</PopoverTrigger>
<PopoverContent
side="right"
align="start"
className="w-auto p-1"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-0.5">
{zone.camera && (
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
onClick={(e) => {
e.stopPropagation();
emitter.emit("camera-controls:view", { nodeId: zone.id });
setCameraPopoverOpen(false);
}}
>
<Camera className="w-3.5 h-3.5" />
View snapshot
</button>
)}
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
onClick={(e) => {
e.stopPropagation();
emitter.emit("camera-controls:capture", { nodeId: zone.id });
setCameraPopoverOpen(false);
}}
>
<Camera className="w-3.5 h-3.5" />
{zone.camera ? "Update snapshot" : "Take snapshot"}
</button>
{zone.camera && (
<button
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-destructive hover:text-destructive-foreground text-left w-full"
onClick={(e) => {
e.stopPropagation();
updateNode(zone.id, { camera: undefined });
setCameraPopoverOpen(false);
}}
>
<Trash2 className="w-3.5 h-3.5" />
Clear snapshot
</button>
)}
</div>
</PopoverContent>
</Popover>
<button <button
className="opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20" className="opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
onClick={handleDelete} onClick={handleDelete}
+563 -20
View File
@@ -58,9 +58,34 @@
"item_0e9paq67kdbm5ux0", "item_0e9paq67kdbm5ux0",
"item_iyu7knyxqe82c3yg", "item_iyu7knyxqe82c3yg",
"item_nwe34qk8vzs1vag7", "item_nwe34qk8vzs1vag7",
"guide_vh96481lk2oqzf4d" "item_oay55zmjjo76s1fs",
"item_g060bhthvcra992w",
"item_38kt7s45alt2vrjg",
"item_n10cp9ke7n9hxl96",
"item_nt5fxip4a03cmaoi",
"item_1quoil3ytsuuarni",
"item_e1w89kkg2pql1v45",
"item_3akotmiffzdr8ule",
"item_1b3tinfswueb6gr8",
"item_y164oe3lxfx9qefg",
"item_dsalxofuqf96h8t4",
"roof_ui8zhim41alg6lq4",
"guide_acs9nzz19rm4vl2c"
], ],
"level": 0 "level": 0,
"camera": {
"position": [
32.19770918094574,
13.355189178183336,
32.63027548275616
],
"target": [
4.8501257048479305,
-6.656412040461838e-16,
7.3634421472590255
],
"mode": "perspective"
}
}, },
"slab_gr6zxi4915gqwjbn": { "slab_gr6zxi4915gqwjbn": {
"object": "node", "object": "node",
@@ -100,7 +125,20 @@
"children": [ "children": [
"roof_jxd8tc6rcuaujl25" "roof_jxd8tc6rcuaujl25"
], ],
"level": 1 "level": 1,
"camera": {
"position": [
11.709072311989358,
25.635955613557638,
50.59653427090403
],
"target": [
6.9995635873796855,
2.4999999999999996,
0.6911898255966076
],
"mode": "perspective"
}
}, },
"roof_jxd8tc6rcuaujl25": { "roof_jxd8tc6rcuaujl25": {
"object": "node", "object": "node",
@@ -117,9 +155,9 @@
], ],
"rotation": 0, "rotation": 0,
"length": 5.5, "length": 5.5,
"height": 1.5, "height": 1.6,
"leftWidth": 2.5, "leftWidth": 4.7,
"rightWidth": 2.5 "rightWidth": 2.7
}, },
"zone_iozx54yy1hmmoads": { "zone_iozx54yy1hmmoads": {
"object": "node", "object": "node",
@@ -147,7 +185,20 @@
6 6
] ]
], ],
"color": "#3b82f6" "color": "#3b82f6",
"camera": {
"position": [
18.715003971902778,
18.8254836683251,
12.086656976691291
],
"target": [
6.9995635873796855,
1.6971845387795204e-17,
0.6911898255966076
],
"mode": "perspective"
}
}, },
"item_137wje66gax2c6bc": { "item_137wje66gax2c6bc": {
"object": "node", "object": "node",
@@ -757,7 +808,7 @@
"visible": true, "visible": true,
"metadata": {}, "metadata": {},
"position": [ "position": [
3.5, 4.5,
0, 0,
0 0
], ],
@@ -1185,9 +1236,9 @@
"metadata": {}, "metadata": {},
"children": [ "children": [
"item_s9fs055u0ilri7pi", "item_s9fs055u0ilri7pi",
"item_4dehdkm4vx6r4ghv",
"item_0173tdywhm704hah", "item_0173tdywhm704hah",
"item_7ykyzy2yre3761dq" "item_7ykyzy2yre3761dq",
"item_4dehdkm4vx6r4ghv"
], ],
"start": [ "start": [
1, 1,
@@ -1255,7 +1306,7 @@
"visible": true, "visible": true,
"metadata": {}, "metadata": {},
"position": [ "position": [
9, 8.5,
0.5, 0.5,
0 0
], ],
@@ -1303,7 +1354,7 @@
"visible": true, "visible": true,
"metadata": {}, "metadata": {},
"position": [ "position": [
6, 6.5,
0, 0,
0 0
], ],
@@ -2053,7 +2104,20 @@
15.5 15.5
] ]
], ],
"color": "#22c55e" "color": "#22c55e",
"camera": {
"position": [
-6.654332076100337,
17.996846152830106,
22.501743052051737
],
"target": [
5.317880378107912,
-2.779128022959309e-17,
10.080522989431769
],
"mode": "perspective"
}
}, },
"item_0e9paq67kdbm5ux0": { "item_0e9paq67kdbm5ux0": {
"object": "node", "object": "node",
@@ -2193,18 +2257,497 @@
] ]
} }
}, },
"guide_vh96481lk2oqzf4d": { "item_oay55zmjjo76s1fs": {
"object": "node", "object": "node",
"id": "guide_vh96481lk2oqzf4d", "id": "item_oay55zmjjo76s1fs",
"type": "guide", "type": "item",
"name": "elestio-postgis-thumbnail.png", "name": "High Fence",
"parentId": "level_pojp0mw3qssu110w", "parentId": "level_pojp0mw3qssu110w",
"visible": true, "visible": true,
"metadata": {}, "metadata": {},
"url": "asset://129b9095-60c3-4239-92b6-3cef391721fb", "position": [
23,
0,
4.25
],
"rotation": [
0,
1.5707963267948966,
0
],
"asset": {
"id": "high-fence",
"category": "outdoor",
"name": "High Fence",
"thumbnail": "/items/high-fence/thumbnail.webp",
"src": "/items/high-fence/model.glb",
"dimensions": [
4,
4.1,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
}
},
"item_g060bhthvcra992w": {
"object": "node",
"id": "item_g060bhthvcra992w",
"type": "item",
"name": "Medium Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
23,
0,
-1.25
],
"rotation": [
0,
0,
0
],
"asset": {
"id": "medium-fence",
"category": "outdoor",
"name": "Medium Fence",
"thumbnail": "/items/medium-fence/thumbnail.webp",
"src": "/items/medium-fence/model.glb",
"dimensions": [
2,
2,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
0.49,
0.49,
0.49
]
}
},
"item_38kt7s45alt2vrjg": {
"object": "node",
"id": "item_38kt7s45alt2vrjg",
"type": "item",
"name": "Medium Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
22,
0,
-2.25
],
"rotation": [
0,
1.5707963267948966,
0
],
"asset": {
"id": "medium-fence",
"category": "outdoor",
"name": "Medium Fence",
"thumbnail": "/items/medium-fence/thumbnail.webp",
"src": "/items/medium-fence/model.glb",
"dimensions": [
2,
2,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
0.49,
0.49,
0.49
]
}
},
"item_n10cp9ke7n9hxl96": {
"object": "node",
"id": "item_n10cp9ke7n9hxl96",
"type": "item",
"name": "Low Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
23,
0,
-4.75
],
"rotation": [
0,
0,
0
],
"asset": {
"id": "low-fence",
"category": "outdoor",
"name": "Low Fence",
"thumbnail": "/items/low-fence/thumbnail.webp",
"src": "/items/low-fence/model.glb",
"dimensions": [
2,
0.8,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
}
},
"item_nt5fxip4a03cmaoi": {
"object": "node",
"id": "item_nt5fxip4a03cmaoi",
"type": "item",
"name": "Low Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
22,
0,
-5.75
],
"rotation": [
0,
1.5707963267948966,
0
],
"asset": {
"id": "low-fence",
"category": "outdoor",
"name": "Low Fence",
"thumbnail": "/items/low-fence/thumbnail.webp",
"src": "/items/low-fence/model.glb",
"dimensions": [
2,
0.8,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
}
},
"item_1quoil3ytsuuarni": {
"object": "node",
"id": "item_1quoil3ytsuuarni",
"type": "item",
"name": "High Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
15,
0,
4.25
],
"rotation": [
0,
1.5707963267948966,
0
],
"asset": {
"id": "high-fence",
"category": "outdoor",
"name": "High Fence",
"thumbnail": "/items/high-fence/thumbnail.webp",
"src": "/items/high-fence/model.glb",
"dimensions": [
4,
4.1,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
}
},
"item_e1w89kkg2pql1v45": {
"object": "node",
"id": "item_e1w89kkg2pql1v45",
"type": "item",
"name": "Medium Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
17,
0,
0.25
],
"rotation": [
0,
1.5707963267948966,
0
],
"asset": {
"id": "medium-fence",
"category": "outdoor",
"name": "Medium Fence",
"thumbnail": "/items/medium-fence/thumbnail.webp",
"src": "/items/medium-fence/model.glb",
"dimensions": [
2,
2,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
0.49,
0.49,
0.49
]
}
},
"item_3akotmiffzdr8ule": {
"object": "node",
"id": "item_3akotmiffzdr8ule",
"type": "item",
"name": "Medium Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
17,
0,
-1.75
],
"rotation": [
0,
1.5707963267948966,
0
],
"asset": {
"id": "medium-fence",
"category": "outdoor",
"name": "Medium Fence",
"thumbnail": "/items/medium-fence/thumbnail.webp",
"src": "/items/medium-fence/model.glb",
"dimensions": [
2,
2,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
0.49,
0.49,
0.49
]
}
},
"item_1b3tinfswueb6gr8": {
"object": "node",
"id": "item_1b3tinfswueb6gr8",
"type": "item",
"name": "Medium Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
17,
0,
-4.75
],
"rotation": [
0,
4.71238898038469,
0
],
"asset": {
"id": "medium-fence",
"category": "outdoor",
"name": "Medium Fence",
"thumbnail": "/items/medium-fence/thumbnail.webp",
"src": "/items/medium-fence/model.glb",
"dimensions": [
2,
2,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
0.49,
0.49,
0.49
]
}
},
"item_y164oe3lxfx9qefg": {
"object": "node",
"id": "item_y164oe3lxfx9qefg",
"type": "item",
"name": "Medium Fence",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
17,
0,
-6.75
],
"rotation": [
0,
4.71238898038469,
0
],
"asset": {
"id": "medium-fence",
"category": "outdoor",
"name": "Medium Fence",
"thumbnail": "/items/medium-fence/thumbnail.webp",
"src": "/items/medium-fence/model.glb",
"dimensions": [
2,
2,
0.5
],
"offset": [
0,
0.01,
0
],
"rotation": [
0,
0,
0
],
"scale": [
0.49,
0.49,
0.49
]
}
},
"roof_ui8zhim41alg6lq4": {
"object": "node",
"id": "roof_ui8zhim41alg6lq4",
"type": "roof",
"name": "Roof 2",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"position": [
1,
0,
-5.5
],
"rotation": 0,
"length": 0.5,
"height": 1.5,
"leftWidth": 12.1,
"rightWidth": 1
},
"guide_acs9nzz19rm4vl2c": {
"object": "node",
"id": "guide_acs9nzz19rm4vl2c",
"type": "guide",
"name": "FaceIt0703_FaceItProductPage.png",
"parentId": "level_pojp0mw3qssu110w",
"visible": true,
"metadata": {},
"url": "asset://1e66ba17-99d2-4c5c-ad2b-00dff438b6a7",
"position": [ "position": [
0, 0,
1.2, 0,
0 0
], ],
"rotation": [ "rotation": [
@@ -2213,7 +2756,7 @@
0 0
], ],
"scale": 1, "scale": 1,
"opacity": 100 "opacity": 51
} }
}, },
"rootNodeIds": [ "rootNodeIds": [
-5
View File
@@ -179,17 +179,12 @@ useScene.temporal.subscribe((state) => {
const currentPastLength = state.pastStates.length const currentPastLength = state.pastStates.length
const currentFutureLength = state.futureStates.length const currentFutureLength = state.futureStates.length
console.log('Temporal state changed:', {
pastStates: { prev: prevPastLength, current: currentPastLength },
futureStates: { prev: prevFutureLength, current: currentFutureLength },
})
// Undo: futureStates increases (state moved from past to future) // Undo: futureStates increases (state moved from past to future)
// Redo: pastStates increases while futureStates decreases (state moved from future to past) // Redo: pastStates increases while futureStates decreases (state moved from future to past)
const didUndo = currentFutureLength > prevFutureLength const didUndo = currentFutureLength > prevFutureLength
const didRedo = currentPastLength > prevPastLength && currentFutureLength < prevFutureLength const didRedo = currentPastLength > prevPastLength && currentFutureLength < prevFutureLength
console.log('Detection:', { didUndo, didRedo })
if (didUndo || didRedo) { if (didUndo || didRedo) {
// Use RAF to ensure all middleware and store updates are complete // Use RAF to ensure all middleware and store updates are complete