Coalesce background Root notifications without delaying input updates

This commit is contained in:
2026-11-10 17:56:00 +08:00
parent 0abb7c96ba
commit b7fe214d97
5 changed files with 56 additions and 5 deletions
@@ -15,6 +15,7 @@ import {nativeRectVisible} from "@/lib/canvas-bindings/visibility";
import {createOverlayIndex} from "@/lib/canvas-bindings/overlays";
import {CanvasOverlays} from "./canvas-overlays";
import {createProjectedPaintIndex} from "@/lib/canvas-bindings/projected-paint";
import {createFrameSource} from '@/lib/canvas-bindings/frame-queue';
import {createSessionFrameSource} from "@/lib/canvas-bindings/session-frames";
import {routeCanvasKey,canvasKeyContext} from "@/lib/canvas-bindings/input-router";
import {hitProjectedCanvas} from "@/lib/canvas-bindings/hit-query";
@@ -42,11 +43,17 @@ import styles from "./canvas.module.css";
const json = (value: unknown): Data => JSON.parse(JSON.stringify(value));
function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engine: ProjectEngine; board: Board; viewId: Id; renderers:ReactRenderers;fitLoading?:boolean }) {
const sceneCache=useMemo(()=>createSceneProjection(),[engine,viewId]);
const subscribe = useCallback((notify: () => void) => {
const subscribe = useCallback((notify: (immediate?:boolean) => void) => {
let before=engine.getSnapshot().document;
return engine.editor.subscribe((commit,next)=>{sceneCache.advance(before,next.document,commit.changes);before=next.document;notify();});
return engine.editor.subscribe((commit,next)=>{sceneCache.advance(before,next.document,commit.changes);before=next.document;
// Controlled inputs must publish their new value before the native event
// finishes. Batch creation/background work may share a presentation frame.
const immediate=commit.origin==='undo'||commit.origin==='redo'||commit.origin==='user'&&commit.changes.some(change=>change.kind==='field'||change.before!==null);
notify(immediate);
});
}, [engine,sceneCache]);
const root = useSyncExternalStore(subscribe, engine.getSnapshot, engine.getSnapshot);
const rootFrames=useMemo(()=>createFrameSource({subscribe,getSnapshot:engine.getSnapshot},{request:callback=>requestAnimationFrame(callback),cancel:handle=>cancelAnimationFrame(handle as number)}),[engine,subscribe]);
const root = useSyncExternalStore(rootFrames.subscribe, rootFrames.getSnapshot, rootFrames.getSnapshot);
const sessionFrames=useMemo(()=>createSessionFrameSource(engine.sessions,{request:callback=>requestAnimationFrame(callback),cancel:handle=>cancelAnimationFrame(handle as number)}),[engine]);
const sessions = useSyncExternalStore(sessionFrames.subscribe, sessionFrames.getSnapshot, sessionFrames.getSnapshot);
const liveView = sessions.views.get(viewId), lastView = useRef(liveView);