feat: unified composition renderer for clipboard/export

Replace the split snapshot/native renderer paths with a single
composition pipeline (compositionRenderer.ts) that:

- Loads actual source images and uses naturalWidth/Height for
  correct full-resolution sampling (fixes top-left-corner-only bug
  caused by data.w/h being capped to 600px display dimensions)
- Routes image-only selections through native Canvas 2D composition
- Falls back to viewport snapshot for mixed/unsupported selections
  with explicit warnings instead of silent degradation
- Resolves group children via itemResolver for proper group export
- Rejects group children from native composition (local coords
  incompatible with world-space drawing)
- Adds canvas size safety limits with auto-downscale
- Guards VideoSprite._drawFrame against null texture source race
  condition during zoom-triggered culling

New files:
- compositionRenderer.ts — unified composition module
- compositionRenderer.test.ts — 16 tests for entry flattening,
  bounds, dimensions, group handling

Modified:
- clipboard.ts — uses composeSelection() instead of direct renderers
- export.ts — uses composeSelection() + getCompositionDimensions()
- Editor.tsx, useShortcutHandler.ts — pass scene for group resolution
- VideoSprite.ts — null guard on texture source in frame loop
This commit is contained in:
Hiren Kangad
2026-03-13 16:59:11 +05:30
parent 1ceafec67f
commit 05118330d6
7 changed files with 805 additions and 72 deletions
+4 -2
View File
@@ -586,7 +586,8 @@ export default function Editor({ isPublicView }: EditorProps) {
try {
const app = canvasRef.current?.getApp() ?? null;
const viewport = canvasRef.current?.getViewport() ?? null;
await writeClipboard(app, viewport, items);
const scene = canvasRef.current?.getScene() ?? undefined;
await writeClipboard(app, viewport, items, scene);
} catch (err: any) {
showToast('Copy failed: ' + (err.message || 'clipboard not available'));
}
@@ -1622,7 +1623,8 @@ export default function Editor({ isPublicView }: EditorProps) {
try {
const app = canvasRef.current?.getApp() ?? null;
const viewport = canvasRef.current?.getViewport() ?? null;
await exportAsImage(app, viewport, items, options);
const scene = canvasRef.current?.getScene() ?? undefined;
await exportAsImage(app, viewport, items, options, scene);
showToast('Exported successfully');
} catch (err: any) {
showToast('Export failed: ' + (err.message || 'unknown error'));