mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
fix: keep aspect ratio flow
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user