From 964d1c1b4b73751bc952755071a39c9342ab9eb6 Mon Sep 17 00:00:00 2001 From: murich Date: Tue, 1 Sep 2026 15:34:15 +0700 Subject: [PATCH] fix: normalize clip.effects to prevent crash on missing params Effect.params is typed as required, but nothing enforced that at runtime. A patch that sets clip.effects from outside the effects UI (kadr_eval, a hand-edited or foreign project file) can hand an effect object with no params. BlurControls and GlowControls in Inspector.tsx both read fx.params directly with no guard, so selecting a clip carrying such an effect throws during render: TypeError: Cannot read properties of undefined (reading 'size') at BlurControls React has no error boundary around this tree, so the uncaught error unmounts the whole app to a blank/black window with no console output the user can see and no crash report anywhere. Fix in the two places clip.effects can enter live state: - updateClip (the single merge point every patch, including kadr_eval-issued ones, goes through) now filters out entries missing id/type and defaults params to {} after merging. - sanitizeProject (run on project load) gets the same treatment, so a foreign/hand-edited project file is healed on open too, not just a live patch. --- src/state/store.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/state/store.ts b/src/state/store.ts index 4f09b3b..6f46272 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -158,7 +158,16 @@ export function sanitizeProject(p: Project): Project { if (!Number.isFinite(c.speed) || c.speed <= 0) c.speed = 1 c.transform = { ...newClipDefaults().transform, ...(c.transform ?? {}) } c.gain = anim(c.gain, 1) - c.effects ??= [] + // same invariant updateClip enforces on a live patch: a hand-edited or + // foreign project file can carry an effect with no params, and every + // reader (BlurControls, GlowControls, …) reads fx.params directly + c.effects = (c.effects ?? []) + .filter((e) => e && typeof e.id === 'string' && typeof e.type === 'string') + .map((e) => ({ + ...e, + enabled: typeof e.enabled === 'boolean' ? e.enabled : true, + params: e.params && typeof e.params === 'object' ? e.params : {} + })) forEachAnim(c, (a) => anim(a, Number.isFinite((a as Anim)?.value) ? (a as Anim).value : 0)) } } @@ -1178,7 +1187,22 @@ export const useEditor = create((set, get) => ({ set((s) => { const p = clone(s.project) const f = findClip(p, clipId) - if (f) Object.assign(f.clip, patch) + if (f) { + Object.assign(f.clip, patch) + // a patch built outside the effects UI (kadr_eval, foreign project + // data) can hand an effect with no params — every reader downstream + // (BlurControls, GlowControls, and any future consumer) assumes the + // Effect shape and reads fx.params directly, so an id/type-less or + // params-less entry here becomes an uncaught render crash the moment + // the clip is selected, not a contained failure + f.clip.effects = (f.clip.effects ?? []) + .filter((e) => e && typeof e.id === 'string' && typeof e.type === 'string') + .map((e) => ({ + ...e, + enabled: typeof e.enabled === 'boolean' ? e.enabled : true, + params: e.params && typeof e.params === 'object' ? e.params : {} + })) + } return { project: p } }),