/** * 交互会话通知的显示帧合并。每个订阅拥有一条 frame queue,读快照不等帧。 * 这里只发现视图和手势的显示失效;选择、工具及预览事实继续保存在原 session。 */ import type {SessionSnapshot} from '../canvas-core/gestures.ts'; import type {ExternalSource} from './selectors.ts'; import {createFrameQueue,type FrameClock} from './frame-queue.ts'; /** One subscription owns one disposable frame queue. Reads always see current sessions. */ /** 每个订阅保留上一份 session 引用,按不可变 view/gesture 身份发现失效,任意 dirty 最终只触发 notify()。 * key 集合是帧内去重依据,不向消费者发布逐元素差分;getSnapshot 直接借用源方法,须不依赖 this 绑定。 * 此包装不使用 immediate 标志,也不对上游 subscribe/stop 抛错作 finally 清理,宿主须保证正常订阅生命周期。 */ export function createSessionFrameSource(source:ExternalSource,clock:FrameClock):ExternalSource{ return Object.freeze({getSnapshot:source.getSnapshot,subscribe(notify:()=>void){ let previous=source.getSnapshot();const queue=createFrameQueue(clock,()=>notify()); // 两次遍历覆盖视图新增/删除;手势结束要同时刷新旧目标,才能去掉它们的预览。 const stop=source.subscribe(()=>{const next=source.getSnapshot(),dirty=new Set(); for(const [id,view]of previous.views)if(next.views.get(id)!==view)dirty.add(`view:${id}`); for(const [id,view]of next.views)if(previous.views.get(id)!==view)dirty.add(`view:${id}`); if(previous.gesture!==next.gesture)for(const state of [previous.gesture,next.gesture])for(const target of state?.targets??[])dirty.add(`element:${target.id}`); // 连线手势与普通几何手势分别跟踪,改变或取消时都让旧/新目标失效。 if(previous.connectionGesture!==next.connectionGesture)for(const state of [previous.connectionGesture,next.connectionGesture])if(state)dirty.add(`element:${state.target.id}`); // 先推进 previous 再排队,因此一帧内多次变化相对每次上游快照计算,不只比较帧起点与终点。 previous=next;queue.invalidate(dirty); });return()=>{stop();queue.dispose();}; }}); }