fix: keep aspect ratio flow

This commit is contained in:
2026-10-04 20:52:47 +05:00
parent e73986fefd
commit 4d9cd2cfcb
7 changed files with 87 additions and 9 deletions
-2
View File
@@ -9,8 +9,6 @@
Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) → Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) →
D (pipeline). Делать по одной, сверху вниз. D (pipeline). Делать по одной, сверху вниз.
- [ ] **C2. keep Aspect field** — пересчитывать второе поле, учитывать какое
было изменено
- [ ] **C3. пресеты размеров** — split-into-parts-png, resize (может еще - [ ] **C3. пресеты размеров** — split-into-parts-png, resize (может еще
какие-то): размеры из заранее одобренного списка — 16х16, 32х32, 64х64 как какие-то): размеры из заранее одобренного списка — 16х16, 32х32, 64х64 как
минимум, единый список с задачей «увеличить максимальные значения» (для минимум, единый список с задачей «увеличить максимальные значения» (для
@@ -11,7 +11,7 @@
label: string; label: string;
value: unknown; value: unknown;
spec: FieldSpec; spec: FieldSpec;
onchange?: (value: Dimension) => void; onchange?: (value: Dimension, axis?: "width" | "height") => void;
} }
let { label, value, spec, onchange }: Props = $props(); let { label, value, spec, onchange }: Props = $props();
@@ -31,7 +31,7 @@
}); });
function setAxis(axis: "width" | "height", n: number) { function setAxis(axis: "width" | "height", n: number) {
onchange?.({ ...current, [axis]: n }); onchange?.({ ...current, [axis]: n }, axis);
} }
</script> </script>
@@ -30,7 +30,7 @@
spec: FieldSpec; spec: FieldSpec;
toolId: string; toolId: string;
fieldId: string; fieldId: string;
onchange?: (value: unknown) => void; onchange?: (value: unknown, axis?: "width" | "height") => void;
} }
const FIELDS: Record<FieldSpecKind, Component<FieldControlProps>> = { const FIELDS: Record<FieldSpecKind, Component<FieldControlProps>> = {
@@ -54,7 +54,7 @@
schema: ToolSchema<Record<string, unknown>>; schema: ToolSchema<Record<string, unknown>>;
values: Record<string, unknown>; values: Record<string, unknown>;
toolId: string; toolId: string;
onchange: (id: string, value: unknown) => void; onchange: (id: string, value: unknown, axis?: "width" | "height") => void;
onreset: () => void; onreset: () => void;
} }
let { schema, values, toolId, onchange, onreset }: Props = $props(); let { schema, values, toolId, onchange, onreset }: Props = $props();
@@ -80,7 +80,7 @@
spec={schema.fields[id].spec} spec={schema.fields[id].spec}
{toolId} {toolId}
fieldId={id} fieldId={id}
onchange={(v) => onchange(id, v)} onchange={(v, axis) => onchange(id, v, axis)}
/> />
{/each} {/each}
</div> </div>
@@ -17,6 +17,8 @@
applySourceDefaults, applySourceDefaults,
defaultSchemaParams, defaultSchemaParams,
sanitizeSchemaParams, sanitizeSchemaParams,
withAspectLock,
type Dimension,
type ToolSchema, type ToolSchema,
} from "$lib/registry-schema"; } from "$lib/registry-schema";
import { downloadZip } from "$lib/zip"; import { downloadZip } from "$lib/zip";
@@ -42,6 +44,8 @@
// Поля, изменённые пользователем: при загрузке нового файла им не даём // Поля, изменённые пользователем: при загрузке нового файла им не даём
// перезаписаться source-дефолтами. Reset снимает пометку. // перезаписаться source-дефолтами. Reset снимает пометку.
const touched = new SvelteSet<string>(); const touched = new SvelteSet<string>();
// Последняя правленая ось dimension-поля — ведущая при lockAspect.
const lastAxis: Record<string, "width" | "height"> = {};
let source = $state<PixelImage | null>(null); let source = $state<PixelImage | null>(null);
let result = $state<PixelImage | null>(null); let result = $state<PixelImage | null>(null);
let fileResult = $state<FileResult | null>(null); let fileResult = $state<FileResult | null>(null);
@@ -70,9 +74,38 @@
values = defaultSchemaParams(schema); values = defaultSchemaParams(schema);
} }
function setValue(id: string, value: unknown) { function setValue(id: string, value: unknown, axis?: "width" | "height") {
touched.add(id); touched.add(id);
values = { ...values, [id]: value }; if (axis) lastAxis[id] = axis;
const next: Record<string, unknown> = { ...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() { function reset() {
+25
View File
@@ -6,6 +6,7 @@ import {
resolveLayoutGroups, resolveLayoutGroups,
sanitizeSchemaParams, sanitizeSchemaParams,
toolSchema, toolSchema,
withAspectLock,
type Dimension, type Dimension,
} from "./registry-schema"; } 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", () => { describe("registry-schema: source-aware dimension defaults", () => {
it("uses static fallback before a source exists", () => { it("uses static fallback before a source exists", () => {
expect(defaultSchemaParams(sourceSchema)).toEqual({ expect(defaultSchemaParams(sourceSchema)).toEqual({
+21
View File
@@ -70,6 +70,11 @@ export interface DimensionSpec extends FieldSpecBase {
width: number; width: number;
height: number; height: number;
defaultFromSource?: boolean; defaultFromSource?: boolean;
/**
* Id checkbox-поля «сохранять пропорции»: пока оно включено, правка одной
* оси пересчитывает вторую по аспекту текущего исходника.
*/
lockAspectWith?: string;
} }
export interface SchemaContext { export interface SchemaContext {
@@ -276,6 +281,7 @@ export const field = {
width: number; width: number;
height: number; height: number;
defaultFromSource?: boolean; defaultFromSource?: boolean;
lockAspectWith?: string;
}): Field<Dimension> => ({ }): Field<Dimension> => ({
spec: { kind: "dimension", ...s }, spec: { kind: "dimension", ...s },
}), }),
@@ -382,6 +388,21 @@ function clamp(value: number, min?: number, max?: number): number {
return value; 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( function resolveDimensionDefaults(
spec: DimensionSpec, spec: DimensionSpec,
context?: SchemaContext, context?: SchemaContext,
+1
View File
@@ -185,6 +185,7 @@ export const resizeSchema = toolSchema<ResizeParams>(
width: 1, width: 1,
height: 1, height: 1,
defaultFromSource: true, defaultFromSource: true,
lockAspectWith: "keepAspect",
}), }),
keepAspect: field.checkbox({ label: "fields.keepAspect", default: true }), keepAspect: field.checkbox({ label: "fields.keepAspect", default: true }),
}, },