Skip to content

Commit 50879c3

Browse files
committed
refactor: drop legacy 'native' AspectRatio via v6 schema migration
(1) Bumped axcutSchemaVersion 5 -> 6 and added upgradeV5DocumentToV6, which rewrites every stored legacyEditor.aspectRatio === "native" to a concrete "W:H" token (the timeline's largest clip dims, falling back to "16:9"). The upgrader is wired into the existing z.preprocess chain alongside v3->v4 and v4->v5. (2) Dropped the "native" union arm from AspectRatio and the per-helper branches in getAspectRatioValue / getAspectRatioLabel / isAspectRatio / formatAspectRatioForCSS. Renamed NATIVE_ASPECT_RATIO_FALLBACK -> ASPECT_RATIO_FALLBACK (the file-private fallback). getNativeAspectRatioValue is kept. (3) Removed the runtime bridge in outputFormat.resolveAspectRatioValue (it was document-aware to resolve "native") and the self-migration useMemo in V4Timeline (the activeToken that re-mapped settings.aspectRatio to the largest clip's token). Both are dead now that v6 documents cannot contain "native". Updated the three preview/export call sites to the new one-arg signature. (4) Tests: bumped every existing schemaVersion === 5 assertion to 6 (schema, migrate, document-service), added a 7-test v5->v6 migration block in schema/index.test.ts, and stripped the "native" assertions from aspectRatioUtils and outputFormat tests (pointing at the v5->v6 upgrader tests as the new home). Intentionally untouched: lastBackgroundColor. The original audit recommended dropping it as dead code, but the picker swatch in src/lib/ai-edition/annotations/background.ts:25-29 reads it for the swatch when bg is off, and toggleTextBackground writes it on every toggle — it's the fix for the "background toggle always reverts to black" regression.
1 parent 9e586bd commit 50879c3

11 files changed

Lines changed: 299 additions & 111 deletions

File tree

electron/ai-edition/document-service.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ describe("DocumentService", () => {
2424
});
2525

2626
describe("createProject", () => {
27-
it("creates a v5 doc with the given title and writes it to disk", async () => {
27+
it("creates a v6 doc with the given title and writes it to disk", async () => {
2828
const doc = await service.createProject("Demo Project");
29-
expect(doc.schemaVersion).toBe(5);
29+
expect(doc.schemaVersion).toBe(6);
3030
expect(doc.project.title).toBe("Demo Project");
3131
expect(doc.project.id).toMatch(/^proj_/);
3232
expect(doc.assets).toEqual([]);
3333

3434
const filePath = path.join(tempDir, `${doc.project.id}.openscreen`);
3535
const raw = await fs.readFile(filePath, "utf8");
3636
expect(JSON.parse(raw)).toMatchObject({
37-
schemaVersion: 5,
37+
schemaVersion: 6,
3838
project: { title: "Demo Project" },
3939
});
4040
});

src/components/ai-edition/ExportDialog.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) {
183183
// so the sizes shown match what the export produces. Read through `getEditorSettings` — the
184184
// same typed façade the ratio dropdown writes through and `buildSceneDescription` reads — so
185185
// this dialog can't drift from the compositor if the storage ever moves. `resolveAspectRatioValue`
186-
// owns the legacy "native" case (uncropped reference asset), previously hand-rolled here.
186+
// is the single funnel preview and export agree on; the v5→v6 upgrader retires the legacy
187+
// "native" AspectRatio so no document-context argument is needed here.
187188
const EXPORT_ASPECT = useMemo(
188-
() => resolveAspectRatioValue(document, getEditorSettings(document).aspectRatio),
189+
() => resolveAspectRatioValue(getEditorSettings(document).aspectRatio),
189190
[document],
190191
);
191192
// Output dimensions the export will produce for a given tier, from the (crop-aware)

src/components/ai-edition/PreviewCanvas.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,12 @@ export function PreviewCanvas(props: PreviewCanvasProps) {
188188
// exactly what the frame is styled to (`width: frameSize.width` a few lines below), so it's
189189
// used directly everywhere `canvasSize` used to be — one source of truth, no separate
190190
// observer that can desync from it.
191-
// `resolveAspectRatioValue`, not bare `getAspectRatioValue`: the latter has no document to
192-
// resolve the legacy "native" selection against and answers 16/9 for it, so a project saved
193-
// with "native" over portrait footage framed the preview 16:9 while `pickOutputDims` handed
194-
// the compositor a portrait `output` — preview and export disagreed on the frame's shape.
191+
// `resolveAspectRatioValue`, not bare `getAspectRatioValue`: keeps preview and export
192+
// routing through the same funnel — the v5→v6 upgrader rewrites the legacy "native"
193+
// selection to a concrete `"W:H"` token, so both paths agree on the resolved shape
194+
// without an extra document argument.
195195
const frameSize = useMemo(() => {
196-
const ratio = resolveAspectRatioValue(document, settings.aspectRatio);
196+
const ratio = resolveAspectRatioValue(settings.aspectRatio);
197197
const { width: containerWidth, height: containerHeight } = containerSize;
198198
if (containerWidth <= 0 || containerHeight <= 0)
199199
return { width: containerWidth, height: containerHeight };
@@ -203,7 +203,7 @@ export function PreviewCanvas(props: PreviewCanvasProps) {
203203
}
204204
const width = containerWidth;
205205
return { width: Math.round(width), height: Math.round(width / ratio) };
206-
}, [containerSize, settings.aspectRatio, document]);
206+
}, [containerSize, settings.aspectRatio]);
207207

208208
// Crop is per-clip (see clipSchema.cropRegion) — resolve it from whichever
209209
// clip the playhead is currently inside, the same lookup VirtualPreview

src/components/ai-edition/v4/V4Timeline.tsx

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { ZOOM_DEPTH_SCALES } from "@/components/video-editor/types";
3030
import { useScopedT } from "@/contexts/I18nContext";
3131
import { useAudioPeaks } from "@/hooks/useAudioPeaks";
3232
import { createId } from "@/lib/ai-edition/document/ids";
33-
import { collectNativeFormats, referenceClipDims } from "@/lib/ai-edition/document/outputFormat";
33+
import { collectNativeFormats } from "@/lib/ai-edition/document/outputFormat";
3434
import type { AxcutClip } from "@/lib/ai-edition/schema";
3535
import { useProjectStore } from "@/lib/ai-edition/store/projectStore";
3636
import { useChatPromptBus } from "@/lib/ai-edition/store/useChatPromptBus";
@@ -46,11 +46,7 @@ import {
4646
} from "@/lib/ai-edition/timeline/trim-mapping";
4747
import { buildAutoZoomSuggestions } from "@/lib/ai-edition/timeline/zoom-suggestions";
4848
import { nativeBridgeClient } from "@/native/client";
49-
import {
50-
ASPECT_RATIO_PRESETS,
51-
getAspectRatioLabel,
52-
toAspectRatioToken,
53-
} from "@/utils/aspectRatioUtils";
49+
import { ASPECT_RATIO_PRESETS, getAspectRatioLabel } from "@/utils/aspectRatioUtils";
5450
import { TransportBar } from "../TransportBar";
5551
import type { VideoSource } from "../VirtualPreview";
5652
import styles from "./EditorShellV4.module.css";
@@ -271,16 +267,6 @@ export function V4Timeline({
271267
// stays a pure list of fixed choices, and "which shapes are my clips" lives solely in ORIGINAL
272268
// (no more per-preset badge, which split that one answer across two places).
273269
const timelineIsMixed = nativeFormats.length > 1;
274-
// A project saved before the shapes were enumerated still stores "native". Resolve it to the
275-
// shape it currently means so the menu highlights a real row (and the button names a real
276-
// ratio) instead of showing a selection that matches nothing. Picking that row rewrites the
277-
// document to the concrete token — which is how those projects self-migrate off the value
278-
// that silently moved with the clip list.
279-
const activeToken = useMemo(() => {
280-
if (settings.aspectRatio !== "native" || !document) return settings.aspectRatio;
281-
const reference = referenceClipDims(document);
282-
return toAspectRatioToken(reference.width, reference.height) ?? settings.aspectRatio;
283-
}, [settings.aspectRatio, document]);
284270

285271
const [aspectMenuOpen, setAspectMenuOpen] = useState(false);
286272
const [autoEnhanceOpen, setAutoEnhanceOpen] = useState(false);
@@ -1115,7 +1101,7 @@ export function V4Timeline({
11151101
title={t("toolbar.aspectRatio")}
11161102
aria-label={t("toolbar.aspectRatio")}
11171103
>
1118-
{getAspectRatioLabel(activeToken)}
1104+
{getAspectRatioLabel(settings.aspectRatio)}
11191105
<ChevronDown size={10} />
11201106
</button>
11211107
</PopoverTrigger>
@@ -1134,7 +1120,7 @@ export function V4Timeline({
11341120
type="button"
11351121
key={ratio}
11361122
className={`${styles.recMenuRow}${
1137-
ratio === activeToken ? ` ${styles.active}` : ""
1123+
ratio === settings.aspectRatio ? ` ${styles.active}` : ""
11381124
}`}
11391125
onClick={() => {
11401126
void setSettings({ aspectRatio: ratio });
@@ -1152,7 +1138,7 @@ export function V4Timeline({
11521138
type="button"
11531139
key={format.token}
11541140
className={`${styles.recMenuRow}${
1155-
format.token === activeToken ? ` ${styles.active}` : ""
1141+
format.token === settings.aspectRatio ? ` ${styles.active}` : ""
11561142
}`}
11571143
onClick={() => {
11581144
void setSettings({ aspectRatio: format.token });

src/lib/ai-edition/document/migrate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe("migrateProjectDataToAxcutDocument", () => {
4343
it("produces a v3 document with one asset and one clip from a v2 single-recording project", () => {
4444
const doc = migrateProjectDataToAxcutDocument(makeV2Project());
4545

46-
expect(doc.schemaVersion).toBe(5);
46+
expect(doc.schemaVersion).toBe(6);
4747
expect(doc.assets).toHaveLength(1);
4848
const asset = doc.assets[0];
4949
expect(asset.kind).toBe("video");

src/lib/ai-edition/document/outputFormat.test.ts

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,7 @@ describe("pickOutputDims", () => {
225225
[asset("a1", 1366, 768), asset("a2", 3840, 2160)],
226226
[clip("c1", "a1"), clip("c2", "a2")],
227227
);
228-
const tokens: AspectRatio[] = [
229-
...ASPECT_RATIO_PRESETS,
230-
"683:384",
231-
"64:27",
232-
"1023:767",
233-
"native",
234-
];
228+
const tokens: AspectRatio[] = [...ASPECT_RATIO_PRESETS, "683:384", "64:27", "1023:767"];
235229
for (const token of tokens) {
236230
const out = pickOutputDims(d, token);
237231
expect(out.width % 2, `width for ${token}`).toBe(0);
@@ -256,14 +250,6 @@ describe("pickOutputDims", () => {
256250
expect(pickOutputDims(after, "16:9")).toEqual({ width: 3840, height: 2160 });
257251
});
258252

259-
it('legacy "native" still resolves to the reference clip, drift included', () => {
260-
const portraitWins = doc(
261-
[asset("a1", 1920, 1080), asset("a2", 2160, 3840)],
262-
[clip("c1", "a1"), clip("c2", "a2")],
263-
);
264-
expect(pickOutputDims(portraitWins, "native")).toEqual({ width: 2160, height: 3840 });
265-
});
266-
267253
it("sizes the output off the cropped footprint, not the raw 4K asset", () => {
268254
// A single 4K clip cropped to a 16:9 half-width strip: the output must rasterise at the
269255
// crop's real size (1920x1080), not the asset's 3840x2160. Before the reference went
@@ -277,18 +263,13 @@ describe("pickOutputDims", () => {
277263
});
278264

279265
describe("resolveAspectRatioValue", () => {
280-
it('resolves legacy "native" against the document instead of the 16/9 fallback', () => {
281-
const d = doc([asset("a1", 1080, 1920)], [clip("c1", "a1")]);
282-
expect(resolveAspectRatioValue(d, "native")).toBeCloseTo(1080 / 1920, 6);
283-
});
284-
285-
it('falls back to 16/9 for "native" with no document (preview before load)', () => {
286-
expect(resolveAspectRatioValue(null, "native")).toBeCloseTo(16 / 9, 6);
287-
});
288-
266+
// The legacy `"native"` AspectRatio is retired — the v5→v6 upgrader (see
267+
// `src/lib/ai-edition/schema/index.test.ts` — "v5 -> v6 native AspectRatio
268+
// migration") rewrites every stored "native" to a concrete `"W:H"` token at load
269+
// time, so by the time this function runs there is no document context to thread
270+
// through. What remains is a thin wrapper around `getAspectRatioValue`.
289271
it("passes concrete tokens straight through", () => {
290-
const d = doc([asset("a1", 1080, 1920)], [clip("c1", "a1")]);
291-
expect(resolveAspectRatioValue(d, "4:5")).toBeCloseTo(0.8, 6);
292-
expect(resolveAspectRatioValue(d, "64:27")).toBeCloseTo(64 / 27, 6);
272+
expect(resolveAspectRatioValue("4:5")).toBeCloseTo(0.8, 6);
273+
expect(resolveAspectRatioValue("64:27")).toBeCloseTo(64 / 27, 6);
293274
});
294275
});

src/lib/ai-edition/document/outputFormat.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { calculateEffectiveSourceDimensions } from "@/lib/exporter/mp4ExportSett
1818
import {
1919
type AspectRatio,
2020
getAspectRatioValue,
21-
getNativeAspectRatioValue,
2221
toAspectRatioToken,
2322
} from "@/utils/aspectRatioUtils";
2423
import type { AxcutAsset, AxcutClip, AxcutDocument } from "../schema";
@@ -225,19 +224,13 @@ export function collectNativeFormats(
225224
}
226225

227226
/**
228-
* Numeric ratio for a stored selection, with the document available to resolve the legacy
229-
* `"native"` value. Every consumer that frames or sizes the output must go through this rather
230-
* than bare `getAspectRatioValue`, which has no document and falls back to 16/9.
227+
* Numeric ratio for a stored selection. v6 documents never carry the legacy `"native"`
228+
* AspectRatio (the v5→v6 upgrader rewrites it to a concrete `"W:H"` token), so resolution
229+
* is a single token lookup. Kept as a function so the preview / export path still has a
230+
* single funnel — if storage ever moves, this is the one place to update.
231231
*/
232-
export function resolveAspectRatioValue(
233-
document: AxcutDocument | null | undefined,
234-
aspectRatio: AspectRatio,
235-
probedAssetDims: Record<string, Dims> = {},
236-
): number {
237-
if (aspectRatio !== "native") return getAspectRatioValue(aspectRatio);
238-
if (!document) return getAspectRatioValue("native");
239-
const reference = referenceClipDims(document, probedAssetDims);
240-
return getNativeAspectRatioValue(reference.width, reference.height);
232+
export function resolveAspectRatioValue(aspectRatio: AspectRatio): number {
233+
return getAspectRatioValue(aspectRatio);
241234
}
242235

243236
/**
@@ -261,7 +254,7 @@ export function pickOutputDims(
261254
probedAssetDims: Record<string, Dims> = {},
262255
): Dims {
263256
const reference = referenceClipDims(document, probedAssetDims);
264-
const ratio = resolveAspectRatioValue(document, aspectRatio, probedAssetDims);
257+
const ratio = resolveAspectRatioValue(aspectRatio);
265258
const longSide = toEvenPx(Math.max(reference.width, reference.height));
266259
if (ratio >= 1) {
267260
return { width: longSide, height: toEvenPx(longSide / ratio) };

0 commit comments

Comments
 (0)