diff --git a/docs/backlog.md b/docs/backlog.md index 38693d5..e260a25 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -9,8 +9,6 @@ Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) → D (pipeline). Делать по одной, сверху вниз. -- [ ] **C2. keep Aspect field** — пересчитывать второе поле, учитывать какое - было изменено - [ ] **C3. пресеты размеров** — split-into-parts-png, resize (может еще какие-то): размеры из заранее одобренного списка — 16х16, 32х32, 64х64 как минимум, единый список с задачей «увеличить максимальные значения» (для diff --git a/web/src/lib/components/fields/schema/DimensionField.svelte b/web/src/lib/components/fields/schema/DimensionField.svelte index ec6f8df..ac8ee42 100644 --- a/web/src/lib/components/fields/schema/DimensionField.svelte +++ b/web/src/lib/components/fields/schema/DimensionField.svelte @@ -11,7 +11,7 @@ label: string; value: unknown; spec: FieldSpec; - onchange?: (value: Dimension) => void; + onchange?: (value: Dimension, axis?: "width" | "height") => void; } let { label, value, spec, onchange }: Props = $props(); @@ -31,7 +31,7 @@ }); function setAxis(axis: "width" | "height", n: number) { - onchange?.({ ...current, [axis]: n }); + onchange?.({ ...current, [axis]: n }, axis); } diff --git a/web/src/lib/components/schema/SchemaFields.svelte b/web/src/lib/components/schema/SchemaFields.svelte index 2426339..e3c5186 100644 --- a/web/src/lib/components/schema/SchemaFields.svelte +++ b/web/src/lib/components/schema/SchemaFields.svelte @@ -30,7 +30,7 @@ spec: FieldSpec; toolId: string; fieldId: string; - onchange?: (value: unknown) => void; + onchange?: (value: unknown, axis?: "width" | "height") => void; } const FIELDS: Record> = { @@ -54,7 +54,7 @@ schema: ToolSchema>; values: Record; toolId: string; - onchange: (id: string, value: unknown) => void; + onchange: (id: string, value: unknown, axis?: "width" | "height") => void; onreset: () => void; } let { schema, values, toolId, onchange, onreset }: Props = $props(); @@ -80,7 +80,7 @@ spec={schema.fields[id].spec} {toolId} fieldId={id} - onchange={(v) => onchange(id, v)} + onchange={(v, axis) => onchange(id, v, axis)} /> {/each} diff --git a/web/src/lib/components/schema/SchemaToolView.svelte b/web/src/lib/components/schema/SchemaToolView.svelte index e348f40..4220053 100644 --- a/web/src/lib/components/schema/SchemaToolView.svelte +++ b/web/src/lib/components/schema/SchemaToolView.svelte @@ -17,6 +17,8 @@ applySourceDefaults, defaultSchemaParams, sanitizeSchemaParams, + withAspectLock, + type Dimension, type ToolSchema, } from "$lib/registry-schema"; import { downloadZip } from "$lib/zip"; @@ -42,6 +44,8 @@ // Поля, изменённые пользователем: при загрузке нового файла им не даём // перезаписаться source-дефолтами. Reset снимает пометку. const touched = new SvelteSet(); + // Последняя правленая ось dimension-поля — ведущая при lockAspect. + const lastAxis: Record = {}; let source = $state(null); let result = $state(null); let fileResult = $state(null); @@ -70,9 +74,38 @@ values = defaultSchemaParams(schema); } - function setValue(id: string, value: unknown) { + function setValue(id: string, value: unknown, axis?: "width" | "height") { touched.add(id); - values = { ...values, [id]: value }; + if (axis) lastAxis[id] = axis; + const next: Record = { ...values, [id]: value }; + const spec = schema?.fields[id]?.spec; + if (source && schema && spec) { + const aspect = source.width / source.height; + if ( + spec.kind === "dimension" && + spec.lockAspectWith && + next[spec.lockAspectWith] === true + ) { + next[id] = withAspectLock( + value as Dimension, + lastAxis[id] ?? "width", + aspect, + ); + } else if (spec.kind === "checkbox" && value === true) { + // Включили lockAspect — сразу подгоняем привязанные поля под аспект. + for (const [fid, f] of Object.entries(schema.fields)) { + const fs = f.spec; + if (fs.kind === "dimension" && fs.lockAspectWith === id) { + next[fid] = withAspectLock( + next[fid] as Dimension, + lastAxis[fid] ?? "width", + aspect, + ); + } + } + } + } + values = next; } function reset() { diff --git a/web/src/lib/registry-schema.test.ts b/web/src/lib/registry-schema.test.ts index 35324ec..570674e 100644 --- a/web/src/lib/registry-schema.test.ts +++ b/web/src/lib/registry-schema.test.ts @@ -6,6 +6,7 @@ import { resolveLayoutGroups, sanitizeSchemaParams, toolSchema, + withAspectLock, type Dimension, } from "./registry-schema"; @@ -64,6 +65,30 @@ describe("registry-schema: дефолты из схемы", () => { }); }); +describe("registry-schema: withAspectLock", () => { + const aspect = 64 / 48; + + it("ведущая ширина — пересчитывается высота", () => { + expect(withAspectLock({ width: 32, height: 48 }, "width", aspect)).toEqual({ + width: 32, + height: 24, + }); + }); + + it("ведущая высота — пересчитывается ширина", () => { + expect(withAspectLock({ width: 64, height: 96 }, "height", aspect)).toEqual( + { width: 128, height: 96 }, + ); + }); + + it("результат не опускается ниже 1", () => { + expect(withAspectLock({ width: 1, height: 48 }, "width", aspect)).toEqual({ + width: 1, + height: 1, + }); + }); +}); + describe("registry-schema: source-aware dimension defaults", () => { it("uses static fallback before a source exists", () => { expect(defaultSchemaParams(sourceSchema)).toEqual({ diff --git a/web/src/lib/registry-schema.ts b/web/src/lib/registry-schema.ts index 7f2381a..c1073c7 100644 --- a/web/src/lib/registry-schema.ts +++ b/web/src/lib/registry-schema.ts @@ -70,6 +70,11 @@ export interface DimensionSpec extends FieldSpecBase { width: number; height: number; defaultFromSource?: boolean; + /** + * Id checkbox-поля «сохранять пропорции»: пока оно включено, правка одной + * оси пересчитывает вторую по аспекту текущего исходника. + */ + lockAspectWith?: string; } export interface SchemaContext { @@ -276,6 +281,7 @@ export const field = { width: number; height: number; defaultFromSource?: boolean; + lockAspectWith?: string; }): Field => ({ spec: { kind: "dimension", ...s }, }), @@ -382,6 +388,21 @@ function clamp(value: number, min?: number, max?: number): number { return value; } +/** + * Пересчёт при включённом lockAspect: ведущая ось (последняя правка + * пользователя) сохраняется, вторая подгоняется под аспект исходника. + */ +export function withAspectLock( + next: Dimension, + leading: "width" | "height", + aspect: number, +): Dimension { + if (leading === "width") { + return { ...next, height: Math.max(1, Math.round(next.width / aspect)) }; + } + return { ...next, width: Math.max(1, Math.round(next.height * aspect)) }; +} + function resolveDimensionDefaults( spec: DimensionSpec, context?: SchemaContext, diff --git a/web/src/lib/registry/tools/geometry.ts b/web/src/lib/registry/tools/geometry.ts index fed16b5..864e1df 100644 --- a/web/src/lib/registry/tools/geometry.ts +++ b/web/src/lib/registry/tools/geometry.ts @@ -185,6 +185,7 @@ export const resizeSchema = toolSchema( width: 1, height: 1, defaultFromSource: true, + lockAspectWith: "keepAspect", }), keepAspect: field.checkbox({ label: "fields.keepAspect", default: true }), },