From 91150a14bf8203627132f6d3a409109845ed749f Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 4 Oct 2026 21:59:18 +0500 Subject: [PATCH] feat: add size presets for tools --- docs/backlog.md | 8 ++--- web/e2e/tool-flows.spec.ts | 20 +++++++++-- .../fields/schema/DimensionField.svelte | 34 ++++++++++++++++--- .../fields/schema/RangeControl.svelte | 6 ++-- .../lib/components/schema/SchemaFields.svelte | 8 +++-- .../components/schema/SchemaToolView.svelte | 9 +++-- web/src/lib/registry-schema.ts | 6 ++++ web/src/lib/registry/tools/generate.ts | 8 +++++ web/src/lib/registry/tools/geometry.ts | 4 +++ 9 files changed, 85 insertions(+), 18 deletions(-) diff --git a/docs/backlog.md b/docs/backlog.md index e260a25..aa94d1c 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -9,10 +9,6 @@ Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) → D (pipeline). Делать по одной, сверху вниз. -- [ ] **C3. пресеты размеров** — split-into-parts-png, resize (может еще - какие-то): размеры из заранее одобренного списка — 16х16, 32х32, 64х64 как - минимум, единый список с задачей «увеличить максимальные значения» (для - инди-разработчиков, стандартные тайлы) - [ ] **B. Загрузка исходника** — ctrl+v и drag-and-drop (dropzone) - [ ] **E. Download** — кнопка сразу пишет текущий формат, селектор PNG/JPG/WebP/BMP для всех инструментов, предупреждение при несовместимости @@ -37,6 +33,10 @@ D (pipeline). Делать по одной, сверху вниз. - [ ] split-into-parts-png - увеличить максимальные значения, плохо работает для нарезки ассетов +- [ ] split-into-parts-png — режим «по размеру тайла»: указываешь размер тайла + (16×16, 32×32…, список `SIZE_PRESETS`), инструмент сам считает сетку — + сценарий «режу спрайтшит, знаю размер тайла, не знаю сетку» (новый режим: + схема + core, из задачи C3) - [ ] e2e: `i18n.spec.ts` и `text-and-verdicts.spec.ts` дублируют локаторы из `helpers/page.ts` вместо вызова — правило «спека зовёт helpers» (`docs/testing-strategy.md`, раздел 5) diff --git a/web/e2e/tool-flows.spec.ts b/web/e2e/tool-flows.spec.ts index 5d24dd7..cdfe6b2 100644 --- a/web/e2e/tool-flows.spec.ts +++ b/web/e2e/tool-flows.spec.ts @@ -43,10 +43,26 @@ test("resize-png keeps user-set size when another image is uploaded", async ({ await uploadImage(page, opaquePng); const width = page.getByRole("spinbutton", { name: "Width" }); await width.fill("25"); + // keepAspect включён по умолчанию: высота пересчитана из 64×48 → 19 + await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue( + "19", + ); await uploadImage(page, landscapePng); await expect(width).toHaveValue("25"); await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue( - "48", + "19", + ); +}); + +test("resize-png applies size preset chips", async ({ page }) => { + await openTool(page, "resize-png"); + await uploadImage(page, opaquePng); + await page.getByRole("button", { name: "32×32" }).click(); + await expect(page.getByRole("spinbutton", { name: "Width" })).toHaveValue( + "32", + ); + await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue( + "32", ); }); @@ -141,7 +157,7 @@ test("reset restores defaults and re-runs with the source", async ({ await radius.fill("20"); await expect(radius).toHaveValue("20"); - await page.getByRole("button", { name: "Reset" }).click(); + await page.getByRole("button", { name: "Reset", exact: true }).click(); await expect(radius).toHaveValue("4"); await expect(sourceImage(page)).toBeVisible(); await expect(resultImage(page)).toBeVisible(); diff --git a/web/src/lib/components/fields/schema/DimensionField.svelte b/web/src/lib/components/fields/schema/DimensionField.svelte index ac8ee42..9f79775 100644 --- a/web/src/lib/components/fields/schema/DimensionField.svelte +++ b/web/src/lib/components/fields/schema/DimensionField.svelte @@ -1,17 +1,19 @@ @@ -60,6 +67,17 @@ /> + {#if sp.presets} +
+ {#each SIZE_PRESETS as n (n)} + applyPreset(n)} + /> + {/each} +
+ {/if}
diff --git a/web/src/lib/components/fields/schema/RangeControl.svelte b/web/src/lib/components/fields/schema/RangeControl.svelte index 2daa450..f3dd094 100644 --- a/web/src/lib/components/fields/schema/RangeControl.svelte +++ b/web/src/lib/components/fields/schema/RangeControl.svelte @@ -39,7 +39,7 @@ void; + onchange?: (value: unknown, axis?: "width" | "height" | "both") => void; } const FIELDS: Record> = { @@ -54,7 +54,11 @@ schema: ToolSchema>; values: Record; toolId: string; - onchange: (id: string, value: unknown, axis?: "width" | "height") => void; + onchange: ( + id: string, + value: unknown, + axis?: "width" | "height" | "both", + ) => void; onreset: () => void; } let { schema, values, toolId, onchange, onreset }: Props = $props(); diff --git a/web/src/lib/components/schema/SchemaToolView.svelte b/web/src/lib/components/schema/SchemaToolView.svelte index 4220053..b71cca4 100644 --- a/web/src/lib/components/schema/SchemaToolView.svelte +++ b/web/src/lib/components/schema/SchemaToolView.svelte @@ -74,9 +74,13 @@ values = defaultSchemaParams(schema); } - function setValue(id: string, value: unknown, axis?: "width" | "height") { + function setValue( + id: string, + value: unknown, + axis?: "width" | "height" | "both", + ) { touched.add(id); - if (axis) lastAxis[id] = axis; + if (axis) lastAxis[id] = axis === "both" ? "width" : axis; const next: Record = { ...values, [id]: value }; const spec = schema?.fields[id]?.spec; if (source && schema && spec) { @@ -84,6 +88,7 @@ if ( spec.kind === "dimension" && spec.lockAspectWith && + axis !== "both" && next[spec.lockAspectWith] === true ) { next[id] = withAspectLock( diff --git a/web/src/lib/registry-schema.ts b/web/src/lib/registry-schema.ts index c1073c7..a05585e 100644 --- a/web/src/lib/registry-schema.ts +++ b/web/src/lib/registry-schema.ts @@ -62,6 +62,9 @@ export interface Dimension { height: number; } +/** Единый список тайловых пресетов (квадраты) для dimension-полей. */ +export const SIZE_PRESETS = [16, 32, 64, 128, 256, 512] as const; + export interface DimensionSpec extends FieldSpecBase { kind: "dimension"; /** Общий диапазон для обоих измерений. */ @@ -75,6 +78,8 @@ export interface DimensionSpec extends FieldSpecBase { * оси пересчитывает вторую по аспекту текущего исходника. */ lockAspectWith?: string; + /** Показывать чипы быстрого выбора из SIZE_PRESETS. */ + presets?: boolean; } export interface SchemaContext { @@ -282,6 +287,7 @@ export const field = { height: number; defaultFromSource?: boolean; lockAspectWith?: string; + presets?: boolean; }): Field => ({ spec: { kind: "dimension", ...s }, }), diff --git a/web/src/lib/registry/tools/generate.ts b/web/src/lib/registry/tools/generate.ts index 09d0546..e5c4192 100644 --- a/web/src/lib/registry/tools/generate.ts +++ b/web/src/lib/registry/tools/generate.ts @@ -90,6 +90,7 @@ export const createEmptySchema = toolSchema( max: 20000, width: 800, height: 600, + presets: true, }), transparent: field.checkbox({ label: "fields.transparent", default: true }), color: field.color({ label: "fields.fillColor", default: "#ffffff" }), @@ -130,6 +131,7 @@ export const singleColorSchema = toolSchema({ max: 20000, width: 256, height: 256, + presets: true, }), color: field.color({ label: "fields.color", default: "#ff0000" }), }); @@ -157,6 +159,7 @@ export const randomNoiseSchema = toolSchema({ max: 5000, width: 512, height: 512, + presets: true, }), seed: field.number({ label: "fields.seed", @@ -191,6 +194,7 @@ export const linearGradientSchema = toolSchema( max: 20000, width: 800, height: 600, + presets: true, }), gradient: field.gradient({ label: "fields.gradient", @@ -241,6 +245,7 @@ export const colorSpectrumSchema = toolSchema( max: 5000, width: 1024, height: 128, + presets: true, }), direction: field.select({ label: "fields.direction", @@ -303,6 +308,7 @@ export const randomColorsSchema = toolSchema( max: 5000, width: 512, height: 512, + presets: true, }), blockSize: field.slider({ label: "fields.blockSize", @@ -357,6 +363,7 @@ export const drawGridSchema = toolSchema( max: 5000, width: 512, height: 512, + presets: true, }), cols: field.slider({ label: "fields.cols", @@ -433,6 +440,7 @@ export const placeholderSchema = toolSchema( max: 5000, width: 800, height: 400, + presets: true, }), backgroundColor: field.color({ label: "fields.backgroundColor", diff --git a/web/src/lib/registry/tools/geometry.ts b/web/src/lib/registry/tools/geometry.ts index 864e1df..b9591e9 100644 --- a/web/src/lib/registry/tools/geometry.ts +++ b/web/src/lib/registry/tools/geometry.ts @@ -77,6 +77,7 @@ export const fitOnBackgroundSchema = toolSchema( max: 20000, width: 800, height: 600, + presets: true, }), transparent: field.checkbox({ label: "fields.transparent", @@ -130,6 +131,7 @@ export const changeCanvasSizeSchema = toolSchema( max: 20000, width: 800, height: 600, + presets: true, }), anchor: field.select({ label: "fields.anchor", @@ -186,6 +188,7 @@ export const resizeSchema = toolSchema( height: 1, defaultFromSource: true, lockAspectWith: "keepAspect", + presets: true, }), keepAspect: field.checkbox({ label: "fields.keepAspect", default: true }), }, @@ -254,6 +257,7 @@ export const cropSchema = toolSchema( width: 1, height: 1, defaultFromSource: true, + presets: true, }), }, {