diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index 82ac03f..28a84ee 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -220,6 +220,23 @@ type DevPanel = 'wb' | 'tone' | 'presence' | 'effects'; type StripKey = GroupKey | ToolStrip | DevPanel; const WM_SUBGROUPS: StripKey[] = ['wmColor', 'wmSize', 'wmFont', 'gpsColor', 'gpsName', 'gpsTime', 'gpsFont']; const isWmPanel = (g: StripKey | null) => g === 'wm' || (g !== null && WM_SUBGROUPS.includes(g)); + +// A look is colour and tone. HEAL's dust spots, FX's mosaic and the gradient +// masks are drawn by hand on the frame standing on the stage — they belong to +// that frame, not to whichever look is loaded next. A preset, a recent or a +// .rcp file carries none of them, so laying one down must not take the +// session's brushing with it: the marks already on the stage win, and a +// recipe's own marks only land when the stage has none (a frame reopened from +// the catalogue arrives with its own). UNDO is untouched by this — a snapshot +// holds the whole recipe, marks included. +function layLook(next: Recipe['adjustments'], prev: Recipe['adjustments']): Recipe['adjustments'] { + return { + ...next, + heal: prev.heal?.length ? prev.heal : next.heal, + mosaic: prev.mosaic?.length ? prev.mosaic : next.mosaic, + masks: prev.masks?.length ? prev.masks : next.masks, + }; +} // A slider row opened from a strip returns to it with "<" (COLOR TEMP -> TEMP). // Every knob LIGHT owns names the panel it was opened from, so its ruler's back // reopens that panel's strip rather than dropping the strip off the screen. @@ -1435,7 +1452,8 @@ export function Workspace() { const applyRecipe = useCallback( (next: Recipe) => { remember(); - setRecipe(next); + // The look arrives, the frame's own marks stay (see layLook). + setRecipe((prev) => ({ ...next, adjustments: layLook(next.adjustments, prev.adjustments) })); setSimId(next.isCustom ? null : next.id); // A recipe filed by SAVE RECENT carries the frame it was saved with; // a bundled preset carries none, and then the frame on the stage stays. @@ -1882,7 +1900,13 @@ export function Workspace() { // the recipe can be judged before saving. No undo entry — this is a draft, and // SAVE/EXPORT is what commits it. const previewCreatedRecipe = useCallback((draft: Pick) => { - setRecipe((prev) => ({ ...prev, ...draft, isCustom: true })); + setRecipe((prev) => ({ + ...prev, + ...draft, + // The draft is a look being judged; the frame's marks stay put (see layLook). + adjustments: layLook(draft.adjustments, prev.adjustments), + isCustom: true, + })); }, []); // The phone's encrypted .recipe envelope (`shared/utils/recipeShare` is the