fix(core): preserve remote patch interaction state
This commit is contained in:
@@ -529,6 +529,61 @@ describe('scene commit boundary', () => {
|
||||
expect(useScene.getState().dirtyNodes.has(LEVEL_ID)).toBe(false)
|
||||
})
|
||||
|
||||
test('dirties surviving siblings after a remote structural deletion', () => {
|
||||
const siblingId = 'level_surviving_sibling' as AnyNodeId
|
||||
const sibling = LevelNode.parse({
|
||||
id: siblingId,
|
||||
parentId: BUILDING_ID,
|
||||
children: [],
|
||||
level: 1,
|
||||
})
|
||||
const building = useScene.getState().nodes[BUILDING_ID] as AnyNode
|
||||
useScene.setState({
|
||||
nodes: {
|
||||
...useScene.getState().nodes,
|
||||
[BUILDING_ID]: { ...building, children: [LEVEL_ID, siblingId] },
|
||||
[siblingId]: sibling,
|
||||
},
|
||||
dirtyNodes: new Set<AnyNodeId>(),
|
||||
})
|
||||
|
||||
expect(
|
||||
applySceneOperationPatch({
|
||||
materialChanges: [],
|
||||
nodeCreates: [],
|
||||
nodeDeletes: [{ node: useScene.getState().nodes[LEVEL_ID] as AnyNode, position: 0 }],
|
||||
nodeUpdates: [],
|
||||
}),
|
||||
).toBe(true)
|
||||
|
||||
expect(useScene.getState().dirtyNodes.has(siblingId)).toBe(true)
|
||||
})
|
||||
|
||||
test('preserves an external tool history pause while applying a remote patch', () => {
|
||||
useScene.temporal.getState().pause()
|
||||
expect(useScene.temporal.getState().isTracking).toBe(false)
|
||||
|
||||
try {
|
||||
expect(
|
||||
applySceneOperationPatch({
|
||||
materialChanges: [],
|
||||
nodeCreates: [],
|
||||
nodeDeletes: [],
|
||||
nodeUpdates: [
|
||||
{
|
||||
data: { level: 2 } as Partial<AnyNode>,
|
||||
id: LEVEL_ID,
|
||||
removeFields: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(useScene.temporal.getState().isTracking).toBe(false)
|
||||
} finally {
|
||||
useScene.temporal.getState().resume()
|
||||
}
|
||||
})
|
||||
|
||||
test('rejects an invalid structural operation before mutating any field or material', () => {
|
||||
const missingParentId = 'building_missing' as AnyNodeId
|
||||
const orphan = LevelNode.parse({
|
||||
|
||||
@@ -1682,11 +1682,13 @@ export function applySceneOperationPatch(changes: SceneOperationPatch): boolean
|
||||
if (!next) return false
|
||||
|
||||
const before = sceneHistorySnapshotFromState(beforeState)
|
||||
pauseSceneHistory(useScene)
|
||||
const shouldScopeHistoryPause =
|
||||
useScene.temporal.getState().isTracking || getSceneHistoryPauseDepth() > 0
|
||||
if (shouldScopeHistoryPause) pauseSceneHistory(useScene)
|
||||
try {
|
||||
useScene.setState(next)
|
||||
} finally {
|
||||
resumeSceneHistory(useScene)
|
||||
if (shouldScopeHistoryPause) resumeSceneHistory(useScene)
|
||||
}
|
||||
|
||||
const currentState = useScene.getState()
|
||||
@@ -1710,6 +1712,18 @@ export function applySceneOperationPatch(changes: SceneOperationPatch): boolean
|
||||
if (beforeParentId) currentState.markDirty(beforeParentId)
|
||||
if (currentParentId) currentState.markDirty(currentParentId)
|
||||
}
|
||||
const structuralParentIds = new Set<AnyNodeId>()
|
||||
for (const { node } of changes.nodeCreates) {
|
||||
if (node.parentId) structuralParentIds.add(node.parentId as AnyNodeId)
|
||||
}
|
||||
for (const { node } of changes.nodeDeletes) {
|
||||
if (node.parentId) structuralParentIds.add(node.parentId as AnyNodeId)
|
||||
}
|
||||
for (const parentId of structuralParentIds) {
|
||||
const parent = current.nodes[parentId]
|
||||
if (!(parent && 'children' in parent && Array.isArray(parent.children))) continue
|
||||
for (const childId of parent.children) currentState.markDirty(childId as AnyNodeId)
|
||||
}
|
||||
if (changes.materialChanges.length > 0) {
|
||||
const materialRefs = new Set(changes.materialChanges.map(({ id }) => toSceneMaterialRef(id)))
|
||||
for (const node of Object.values(current.nodes)) {
|
||||
|
||||
Reference in New Issue
Block a user