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)
|
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', () => {
|
test('rejects an invalid structural operation before mutating any field or material', () => {
|
||||||
const missingParentId = 'building_missing' as AnyNodeId
|
const missingParentId = 'building_missing' as AnyNodeId
|
||||||
const orphan = LevelNode.parse({
|
const orphan = LevelNode.parse({
|
||||||
|
|||||||
@@ -1682,11 +1682,13 @@ export function applySceneOperationPatch(changes: SceneOperationPatch): boolean
|
|||||||
if (!next) return false
|
if (!next) return false
|
||||||
|
|
||||||
const before = sceneHistorySnapshotFromState(beforeState)
|
const before = sceneHistorySnapshotFromState(beforeState)
|
||||||
pauseSceneHistory(useScene)
|
const shouldScopeHistoryPause =
|
||||||
|
useScene.temporal.getState().isTracking || getSceneHistoryPauseDepth() > 0
|
||||||
|
if (shouldScopeHistoryPause) pauseSceneHistory(useScene)
|
||||||
try {
|
try {
|
||||||
useScene.setState(next)
|
useScene.setState(next)
|
||||||
} finally {
|
} finally {
|
||||||
resumeSceneHistory(useScene)
|
if (shouldScopeHistoryPause) resumeSceneHistory(useScene)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentState = useScene.getState()
|
const currentState = useScene.getState()
|
||||||
@@ -1710,6 +1712,18 @@ export function applySceneOperationPatch(changes: SceneOperationPatch): boolean
|
|||||||
if (beforeParentId) currentState.markDirty(beforeParentId)
|
if (beforeParentId) currentState.markDirty(beforeParentId)
|
||||||
if (currentParentId) currentState.markDirty(currentParentId)
|
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) {
|
if (changes.materialChanges.length > 0) {
|
||||||
const materialRefs = new Set(changes.materialChanges.map(({ id }) => toSceneMaterialRef(id)))
|
const materialRefs = new Set(changes.materialChanges.map(({ id }) => toSceneMaterialRef(id)))
|
||||||
for (const node of Object.values(current.nodes)) {
|
for (const node of Object.values(current.nodes)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user