Fix realtime undo updates for wall and fence moves
This commit is contained in:
@@ -54,6 +54,12 @@ export {
|
|||||||
type ItemInteractiveState,
|
type ItemInteractiveState,
|
||||||
useInteractive,
|
useInteractive,
|
||||||
} from './store/use-interactive'
|
} from './store/use-interactive'
|
||||||
|
export {
|
||||||
|
getSceneHistoryPauseDepth,
|
||||||
|
pauseSceneHistory,
|
||||||
|
resetSceneHistoryPauseDepth,
|
||||||
|
resumeSceneHistory,
|
||||||
|
} from './store/history-control'
|
||||||
export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms'
|
export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms'
|
||||||
export { FenceSystem } from './systems/fence/fence-system'
|
export { FenceSystem } from './systems/fence/fence-system'
|
||||||
export { clearSceneHistory, default as useScene } from './store/use-scene'
|
export { clearSceneHistory, default as useScene } from './store/use-scene'
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import {
|
|||||||
isCurvedWall,
|
isCurvedWall,
|
||||||
} from '../systems/wall/wall-curve'
|
} from '../systems/wall/wall-curve'
|
||||||
import { CeilingNode, SlabNode, type CeilingNode as CeilingNodeType, type SlabNode as SlabNodeType, type WallNode } from '../schema'
|
import { CeilingNode, SlabNode, type CeilingNode as CeilingNodeType, type SlabNode as SlabNodeType, type WallNode } from '../schema'
|
||||||
|
import {
|
||||||
|
getSceneHistoryPauseDepth,
|
||||||
|
pauseSceneHistory,
|
||||||
|
resumeSceneHistory,
|
||||||
|
} from '../store/history-control'
|
||||||
import { simplifyClosedPolygon } from './polygon-geometry'
|
import { simplifyClosedPolygon } from './polygon-geometry'
|
||||||
|
|
||||||
type Point2D = { x: number; y: number }
|
type Point2D = { x: number; y: number }
|
||||||
@@ -855,6 +860,7 @@ export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () =>
|
|||||||
|
|
||||||
const unsubscribe = sceneStore.subscribe((state: any) => {
|
const unsubscribe = sceneStore.subscribe((state: any) => {
|
||||||
if (isProcessing) return
|
if (isProcessing) return
|
||||||
|
if (getSceneHistoryPauseDepth() > 0) return
|
||||||
|
|
||||||
const nodes = state.nodes
|
const nodes = state.nodes
|
||||||
const wallsByLevel = new Map<string, WallNode[]>()
|
const wallsByLevel = new Map<string, WallNode[]>()
|
||||||
@@ -889,11 +895,11 @@ export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
isProcessing = true
|
isProcessing = true
|
||||||
sceneStore.temporal.getState().pause()
|
pauseSceneHistory(sceneStore)
|
||||||
try {
|
try {
|
||||||
runSpaceDetection([...levelsToUpdate], sceneStore, editorStore, nodes)
|
runSpaceDetection([...levelsToUpdate], sceneStore, editorStore, nodes)
|
||||||
} finally {
|
} finally {
|
||||||
sceneStore.temporal.getState().resume()
|
resumeSceneHistory(sceneStore)
|
||||||
previousSnapshots.clear()
|
previousSnapshots.clear()
|
||||||
for (const [levelId, snapshot] of currentSnapshots.entries()) {
|
for (const [levelId, snapshot] of currentSnapshots.entries()) {
|
||||||
previousSnapshots.set(levelId, snapshot)
|
previousSnapshots.set(levelId, snapshot)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
let sceneHistoryPauseDepth = 0
|
||||||
|
|
||||||
|
type TemporalStoreLike = {
|
||||||
|
temporal: {
|
||||||
|
getState(): {
|
||||||
|
pause(): void
|
||||||
|
resume(): void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pauseSceneHistory(sceneStore: TemporalStoreLike): void {
|
||||||
|
if (sceneHistoryPauseDepth === 0) {
|
||||||
|
sceneStore.temporal.getState().pause()
|
||||||
|
}
|
||||||
|
sceneHistoryPauseDepth += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resumeSceneHistory(sceneStore: TemporalStoreLike): void {
|
||||||
|
if (sceneHistoryPauseDepth === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneHistoryPauseDepth -= 1
|
||||||
|
if (sceneHistoryPauseDepth === 0) {
|
||||||
|
sceneStore.temporal.getState().resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSceneHistoryPauseDepth(): number {
|
||||||
|
return sceneHistoryPauseDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetSceneHistoryPauseDepth(): void {
|
||||||
|
sceneHistoryPauseDepth = 0
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { SiteNode } from '../schema/nodes/site'
|
|||||||
import { StairNode as StairNodeSchema } from '../schema/nodes/stair'
|
import { StairNode as StairNodeSchema } from '../schema/nodes/stair'
|
||||||
import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment'
|
import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment'
|
||||||
import type { AnyNode, AnyNodeId } from '../schema/types'
|
import type { AnyNode, AnyNodeId } from '../schema/types'
|
||||||
|
import { resetSceneHistoryPauseDepth } from './history-control'
|
||||||
import * as nodeActions from './actions/node-actions'
|
import * as nodeActions from './actions/node-actions'
|
||||||
|
|
||||||
function getFiniteNumber(value: unknown, fallback: number) {
|
function getFiniteNumber(value: unknown, fallback: number) {
|
||||||
@@ -628,6 +629,7 @@ let prevNodesSnapshot: Record<AnyNodeId, AnyNode> | null = null
|
|||||||
|
|
||||||
export function clearSceneHistory() {
|
export function clearSceneHistory() {
|
||||||
useScene.temporal.getState().clear()
|
useScene.temporal.getState().clear()
|
||||||
|
resetSceneHistoryPauseDepth()
|
||||||
prevPastLength = 0
|
prevPastLength = 0
|
||||||
prevFutureLength = 0
|
prevFutureLength = 0
|
||||||
prevNodesSnapshot = null
|
prevNodesSnapshot = null
|
||||||
@@ -647,8 +649,9 @@ useScene.temporal.subscribe((state) => {
|
|||||||
// Capture the previous snapshot before RAF fires
|
// Capture the previous snapshot before RAF fires
|
||||||
const snapshotBefore = prevNodesSnapshot
|
const snapshotBefore = prevNodesSnapshot
|
||||||
|
|
||||||
// Use RAF to ensure all middleware and store updates are complete
|
// Defer to a microtask so the scene store has settled before we diff,
|
||||||
requestAnimationFrame(() => {
|
// but still mark walls/items dirty before the next paint.
|
||||||
|
queueMicrotask(() => {
|
||||||
const currentNodes = useScene.getState().nodes
|
const currentNodes = useScene.getState().nodes
|
||||||
const { markDirty } = useScene.getState()
|
const { markDirty } = useScene.getState()
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ type FloorplanViewport = {
|
|||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function floorplanViewportEquals(a: FloorplanViewport | null, b: FloorplanViewport | null) {
|
||||||
|
if (a === b) return true
|
||||||
|
if (!(a && b)) return false
|
||||||
|
return a.centerX === b.centerX && a.centerY === b.centerY && a.width === b.width
|
||||||
|
}
|
||||||
|
|
||||||
type SvgPoint = {
|
type SvgPoint = {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
@@ -6155,12 +6161,12 @@ export function FloorplanPanel() {
|
|||||||
if (levelChanged) {
|
if (levelChanged) {
|
||||||
previousLevelIdRef.current = levelId ?? null
|
previousLevelIdRef.current = levelId ?? null
|
||||||
hasUserAdjustedViewportRef.current = false
|
hasUserAdjustedViewportRef.current = false
|
||||||
setViewport(fittedViewport)
|
setViewport((current) => (floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasUserAdjustedViewportRef.current) {
|
if (!hasUserAdjustedViewportRef.current) {
|
||||||
setViewport(fittedViewport)
|
setViewport((current) => (floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport))
|
||||||
}
|
}
|
||||||
}, [fittedViewport, levelId])
|
}, [fittedViewport, levelId])
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,14 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|||||||
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
|
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
|
// Restore original baseline while paused so the next resume+update
|
||||||
|
// registers as a single tracked change (undo reverts to original).
|
||||||
|
applyNodePreview([
|
||||||
|
{ id: nodeId, start: originalStart, end: originalEnd },
|
||||||
|
...linkedOriginalsRef.current,
|
||||||
|
])
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: preview.start, end: preview.end },
|
{ id: nodeId, start: preview.start, end: preview.end },
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNodeId, emitter, type GridEvent, useScene, type WallNode } from '@pascal-app/core'
|
import {
|
||||||
|
type AnyNodeId,
|
||||||
|
emitter,
|
||||||
|
type GridEvent,
|
||||||
|
pauseSceneHistory,
|
||||||
|
resumeSceneHistory,
|
||||||
|
useScene,
|
||||||
|
type WallNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { Html } from '@react-three/drei'
|
import { Html } from '@react-three/drei'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
@@ -127,7 +135,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
node?.type === 'wall' && (node.parentId ?? null) === (target.wall.parentId ?? null),
|
node?.type === 'wall' && (node.parentId ?? null) === (target.wall.parentId ?? null),
|
||||||
)
|
)
|
||||||
|
|
||||||
useScene.temporal.getState().pause()
|
pauseSceneHistory(useScene)
|
||||||
let wasCommitted = false
|
let wasCommitted = false
|
||||||
|
|
||||||
const applyNodePreview = (
|
const applyNodePreview = (
|
||||||
@@ -209,7 +217,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
...linkedOriginalsRef.current,
|
...linkedOriginalsRef.current,
|
||||||
])
|
])
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
resumeSceneHistory(useScene)
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: preview.start, end: preview.end },
|
{ id: nodeId, start: preview.start, end: preview.end },
|
||||||
...(altPressedRef.current
|
...(altPressedRef.current
|
||||||
@@ -222,7 +230,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
preview.end,
|
preview.end,
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
useScene.temporal.getState().pause()
|
pauseSceneHistory(useScene)
|
||||||
sfxEmitter.emit('sfx:item-place')
|
sfxEmitter.emit('sfx:item-place')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +242,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
restoreOriginal()
|
restoreOriginal()
|
||||||
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
useScene.temporal.getState().resume()
|
resumeSceneHistory(useScene)
|
||||||
markToolCancelConsumed()
|
markToolCancelConsumed()
|
||||||
exitMoveMode()
|
exitMoveMode()
|
||||||
}
|
}
|
||||||
@@ -279,7 +287,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
if (!wasCommitted) {
|
if (!wasCommitted) {
|
||||||
restoreOriginal()
|
restoreOriginal()
|
||||||
}
|
}
|
||||||
useScene.temporal.getState().resume()
|
resumeSceneHistory(useScene)
|
||||||
emitter.off('grid:move', onGridMove)
|
emitter.off('grid:move', onGridMove)
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off('grid:click', onGridClick)
|
||||||
emitter.off('tool:cancel', onCancel)
|
emitter.off('tool:cancel', onCancel)
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNodeId, emitter, type GridEvent, useScene, type WallNode } from '@pascal-app/core'
|
import {
|
||||||
|
type AnyNodeId,
|
||||||
|
emitter,
|
||||||
|
type GridEvent,
|
||||||
|
pauseSceneHistory,
|
||||||
|
resumeSceneHistory,
|
||||||
|
useScene,
|
||||||
|
type WallNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
@@ -24,9 +32,9 @@ function stripWallIsNewMetadata(meta: WallNode['metadata']): WallNode['metadata'
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextMeta = { ...(meta as Record<string, unknown>) }
|
const nextMeta = { ...(meta as Record<string, unknown>) } as Record<string, unknown>
|
||||||
delete nextMeta.isNew
|
delete nextMeta.isNew
|
||||||
return nextMeta
|
return nextMeta as WallNode['metadata']
|
||||||
}
|
}
|
||||||
|
|
||||||
type LinkedWallSnapshot = {
|
type LinkedWallSnapshot = {
|
||||||
@@ -146,7 +154,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
const originalCenter = originalCenterRef.current
|
const originalCenter = originalCenterRef.current
|
||||||
const originalHalfVector = originalHalfVectorRef.current
|
const originalHalfVector = originalHalfVectorRef.current
|
||||||
|
|
||||||
useScene.temporal.getState().pause()
|
pauseSceneHistory(useScene)
|
||||||
let wasCommitted = false
|
let wasCommitted = false
|
||||||
|
|
||||||
const applyNodePreview = (
|
const applyNodePreview = (
|
||||||
@@ -237,7 +245,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
...linkedOriginalsRef.current,
|
...linkedOriginalsRef.current,
|
||||||
])
|
])
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
resumeSceneHistory(useScene)
|
||||||
|
|
||||||
const commitUpdates = [
|
const commitUpdates = [
|
||||||
{
|
{
|
||||||
@@ -266,7 +274,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
useScene.getState().markDirty(id)
|
useScene.getState().markDirty(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
useScene.temporal.getState().pause()
|
pauseSceneHistory(useScene)
|
||||||
|
|
||||||
sfxEmitter.emit('sfx:item-place')
|
sfxEmitter.emit('sfx:item-place')
|
||||||
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
@@ -315,7 +323,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
restoreOriginal()
|
restoreOriginal()
|
||||||
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
useScene.temporal.getState().resume()
|
resumeSceneHistory(useScene)
|
||||||
markToolCancelConsumed()
|
markToolCancelConsumed()
|
||||||
exitMoveMode()
|
exitMoveMode()
|
||||||
}
|
}
|
||||||
@@ -331,7 +339,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
restoreOriginal()
|
restoreOriginal()
|
||||||
}
|
}
|
||||||
shiftPressedRef.current = false
|
shiftPressedRef.current = false
|
||||||
useScene.temporal.getState().resume()
|
resumeSceneHistory(useScene)
|
||||||
emitter.off('grid:move', onGridMove)
|
emitter.off('grid:move', onGridMove)
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off('grid:click', onGridClick)
|
||||||
emitter.off('tool:cancel', onCancel)
|
emitter.off('tool:cancel', onCancel)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
||||||
|
import { runRedo, runUndo } from '../../../lib/history'
|
||||||
import { useCommandRegistry } from '../../../store/use-command-registry'
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
||||||
import type { StructureTool } from '../../../store/use-editor'
|
import type { StructureTool } from '../../../store/use-editor'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
@@ -313,7 +314,7 @@ export function EditorCommands() {
|
|||||||
group: 'History',
|
group: 'History',
|
||||||
icon: <Undo2 className="h-4 w-4" />,
|
icon: <Undo2 className="h-4 w-4" />,
|
||||||
keywords: ['undo', 'revert', 'back'],
|
keywords: ['undo', 'revert', 'back'],
|
||||||
execute: () => run(() => useScene.temporal.getState().undo()),
|
execute: () => run(() => runUndo()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'editor.history.redo',
|
id: 'editor.history.redo',
|
||||||
@@ -321,7 +322,7 @@ export function EditorCommands() {
|
|||||||
group: 'History',
|
group: 'History',
|
||||||
icon: <Redo2 className="h-4 w-4" />,
|
icon: <Redo2 className="h-4 w-4" />,
|
||||||
keywords: ['redo', 'forward', 'repeat'],
|
keywords: ['redo', 'forward', 'repeat'],
|
||||||
execute: () => run(() => useScene.temporal.getState().redo()),
|
execute: () => run(() => runRedo()),
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Export & Share ───────────────────────────────────────────────────
|
// ── Export & Share ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { runRedo, runUndo } from '../lib/history'
|
||||||
import { sfxEmitter } from '../lib/sfx-bus'
|
import { sfxEmitter } from '../lib/sfx-bus'
|
||||||
import useEditor from '../store/use-editor'
|
import useEditor from '../store/use-editor'
|
||||||
|
|
||||||
@@ -91,11 +92,11 @@ export const useKeyboard = ({ isVersionPreviewMode = false } = {}) => {
|
|||||||
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
||||||
if (isVersionPreviewMode) return
|
if (isVersionPreviewMode) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useScene.temporal.getState().undo()
|
runUndo()
|
||||||
} else if (e.key === 'Z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'Z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
||||||
if (isVersionPreviewMode) return
|
if (isVersionPreviewMode) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useScene.temporal.getState().redo()
|
runRedo()
|
||||||
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const { buildingId, levelId } = useViewer.getState().selection
|
const { buildingId, levelId } = useViewer.getState().selection
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useLiveTransforms, useScene } from '@pascal-app/core'
|
||||||
|
|
||||||
|
function refreshSceneAfterHistoryJump() {
|
||||||
|
useLiveTransforms.getState().clearAll()
|
||||||
|
|
||||||
|
const state = useScene.getState()
|
||||||
|
for (const node of Object.values(state.nodes)) {
|
||||||
|
state.markDirty(node.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runUndo() {
|
||||||
|
useScene.temporal.getState().undo()
|
||||||
|
refreshSceneAfterHistoryJump()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runRedo() {
|
||||||
|
useScene.temporal.getState().redo()
|
||||||
|
refreshSceneAfterHistoryJump()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user