mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: add size presets for tools
This commit is contained in:
+4
-4
@@ -9,10 +9,6 @@
|
|||||||
Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) →
|
Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) →
|
||||||
D (pipeline). Делать по одной, сверху вниз.
|
D (pipeline). Делать по одной, сверху вниз.
|
||||||
|
|
||||||
- [ ] **C3. пресеты размеров** — split-into-parts-png, resize (может еще
|
|
||||||
какие-то): размеры из заранее одобренного списка — 16х16, 32х32, 64х64 как
|
|
||||||
минимум, единый список с задачей «увеличить максимальные значения» (для
|
|
||||||
инди-разработчиков, стандартные тайлы)
|
|
||||||
- [ ] **B. Загрузка исходника** — ctrl+v и drag-and-drop (dropzone)
|
- [ ] **B. Загрузка исходника** — ctrl+v и drag-and-drop (dropzone)
|
||||||
- [ ] **E. Download** — кнопка сразу пишет текущий формат, селектор
|
- [ ] **E. Download** — кнопка сразу пишет текущий формат, селектор
|
||||||
PNG/JPG/WebP/BMP для всех инструментов, предупреждение при несовместимости
|
PNG/JPG/WebP/BMP для всех инструментов, предупреждение при несовместимости
|
||||||
@@ -37,6 +33,10 @@ D (pipeline). Делать по одной, сверху вниз.
|
|||||||
|
|
||||||
- [ ] split-into-parts-png - увеличить максимальные значения, плохо работает для
|
- [ ] 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` дублируют локаторы из
|
- [ ] e2e: `i18n.spec.ts` и `text-and-verdicts.spec.ts` дублируют локаторы из
|
||||||
`helpers/page.ts` вместо вызова — правило «спека зовёт helpers»
|
`helpers/page.ts` вместо вызова — правило «спека зовёт helpers»
|
||||||
(`docs/testing-strategy.md`, раздел 5)
|
(`docs/testing-strategy.md`, раздел 5)
|
||||||
|
|||||||
@@ -43,10 +43,26 @@ test("resize-png keeps user-set size when another image is uploaded", async ({
|
|||||||
await uploadImage(page, opaquePng);
|
await uploadImage(page, opaquePng);
|
||||||
const width = page.getByRole("spinbutton", { name: "Width" });
|
const width = page.getByRole("spinbutton", { name: "Width" });
|
||||||
await width.fill("25");
|
await width.fill("25");
|
||||||
|
// keepAspect включён по умолчанию: высота пересчитана из 64×48 → 19
|
||||||
|
await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue(
|
||||||
|
"19",
|
||||||
|
);
|
||||||
await uploadImage(page, landscapePng);
|
await uploadImage(page, landscapePng);
|
||||||
await expect(width).toHaveValue("25");
|
await expect(width).toHaveValue("25");
|
||||||
await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue(
|
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 radius.fill("20");
|
||||||
await expect(radius).toHaveValue("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(radius).toHaveValue("4");
|
||||||
await expect(sourceImage(page)).toBeVisible();
|
await expect(sourceImage(page)).toBeVisible();
|
||||||
await expect(resultImage(page)).toBeVisible();
|
await expect(resultImage(page)).toBeVisible();
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
import type {
|
import {
|
||||||
Dimension,
|
SIZE_PRESETS,
|
||||||
DimensionSpec,
|
type Dimension,
|
||||||
FieldSpec,
|
type DimensionSpec,
|
||||||
|
type FieldSpec,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
|
import ChipButton from "$lib/components/ui/ChipButton.svelte";
|
||||||
import Control from "./Control.svelte";
|
import Control from "./Control.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
spec: FieldSpec;
|
spec: FieldSpec;
|
||||||
onchange?: (value: Dimension, axis?: "width" | "height") => void;
|
onchange?: (value: Dimension, axis?: "width" | "height" | "both") => void;
|
||||||
}
|
}
|
||||||
let { label, value, spec, onchange }: Props = $props();
|
let { label, value, spec, onchange }: Props = $props();
|
||||||
|
|
||||||
@@ -33,6 +35,11 @@
|
|||||||
function setAxis(axis: "width" | "height", n: number) {
|
function setAxis(axis: "width" | "height", n: number) {
|
||||||
onchange?.({ ...current, [axis]: n }, axis);
|
onchange?.({ ...current, [axis]: n }, axis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyPreset(n: number) {
|
||||||
|
// Пресет задаёт обе оси явно — lockAspect его не пересчитывает.
|
||||||
|
onchange?.({ width: n, height: n }, "both");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Control {label} caps>
|
<Control {label} caps>
|
||||||
@@ -60,6 +67,17 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{#if sp.presets}
|
||||||
|
<div class="dimension-presets">
|
||||||
|
{#each SIZE_PRESETS as n (n)}
|
||||||
|
<ChipButton
|
||||||
|
label={`${n}×${n}`}
|
||||||
|
active={current.width === n && current.height === n}
|
||||||
|
onclick={() => applyPreset(n)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Control>
|
</Control>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -81,4 +99,10 @@
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font: var(--font-size-s) var(--font-mono);
|
font: var(--font-size-s) var(--font-mono);
|
||||||
}
|
}
|
||||||
|
.dimension-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-s);
|
||||||
|
margin-top: var(--space-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<span class="range-trailing">
|
<span class="range-trailing">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={Minus}
|
icon={Minus}
|
||||||
label={t("ui.decrease")}
|
label={`${t("ui.decrease")} ${label}`}
|
||||||
variant="clear"
|
variant="clear"
|
||||||
size="s"
|
size="s"
|
||||||
disabled={atMin}
|
disabled={atMin}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={Plus}
|
icon={Plus}
|
||||||
label={t("ui.increase")}
|
label={`${t("ui.increase")} ${label}`}
|
||||||
variant="clear"
|
variant="clear"
|
||||||
size="s"
|
size="s"
|
||||||
disabled={atMax}
|
disabled={atMax}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={RotateCcw}
|
icon={RotateCcw}
|
||||||
label={t("ui.reset")}
|
label={`${t("ui.reset")} ${label}`}
|
||||||
variant="clear"
|
variant="clear"
|
||||||
size="s"
|
size="s"
|
||||||
disabled={atDefault}
|
disabled={atDefault}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
spec: FieldSpec;
|
spec: FieldSpec;
|
||||||
toolId: string;
|
toolId: string;
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
onchange?: (value: unknown, axis?: "width" | "height") => void;
|
onchange?: (value: unknown, axis?: "width" | "height" | "both") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIELDS: Record<FieldSpecKind, Component<FieldControlProps>> = {
|
const FIELDS: Record<FieldSpecKind, Component<FieldControlProps>> = {
|
||||||
@@ -54,7 +54,11 @@
|
|||||||
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, axis?: "width" | "height") => void;
|
onchange: (
|
||||||
|
id: string,
|
||||||
|
value: unknown,
|
||||||
|
axis?: "width" | "height" | "both",
|
||||||
|
) => void;
|
||||||
onreset: () => void;
|
onreset: () => void;
|
||||||
}
|
}
|
||||||
let { schema, values, toolId, onchange, onreset }: Props = $props();
|
let { schema, values, toolId, onchange, onreset }: Props = $props();
|
||||||
|
|||||||
@@ -74,9 +74,13 @@
|
|||||||
values = defaultSchemaParams(schema);
|
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);
|
touched.add(id);
|
||||||
if (axis) lastAxis[id] = axis;
|
if (axis) lastAxis[id] = axis === "both" ? "width" : axis;
|
||||||
const next: Record<string, unknown> = { ...values, [id]: value };
|
const next: Record<string, unknown> = { ...values, [id]: value };
|
||||||
const spec = schema?.fields[id]?.spec;
|
const spec = schema?.fields[id]?.spec;
|
||||||
if (source && schema && spec) {
|
if (source && schema && spec) {
|
||||||
@@ -84,6 +88,7 @@
|
|||||||
if (
|
if (
|
||||||
spec.kind === "dimension" &&
|
spec.kind === "dimension" &&
|
||||||
spec.lockAspectWith &&
|
spec.lockAspectWith &&
|
||||||
|
axis !== "both" &&
|
||||||
next[spec.lockAspectWith] === true
|
next[spec.lockAspectWith] === true
|
||||||
) {
|
) {
|
||||||
next[id] = withAspectLock(
|
next[id] = withAspectLock(
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export interface Dimension {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Единый список тайловых пресетов (квадраты) для dimension-полей. */
|
||||||
|
export const SIZE_PRESETS = [16, 32, 64, 128, 256, 512] as const;
|
||||||
|
|
||||||
export interface DimensionSpec extends FieldSpecBase {
|
export interface DimensionSpec extends FieldSpecBase {
|
||||||
kind: "dimension";
|
kind: "dimension";
|
||||||
/** Общий диапазон для обоих измерений. */
|
/** Общий диапазон для обоих измерений. */
|
||||||
@@ -75,6 +78,8 @@ export interface DimensionSpec extends FieldSpecBase {
|
|||||||
* оси пересчитывает вторую по аспекту текущего исходника.
|
* оси пересчитывает вторую по аспекту текущего исходника.
|
||||||
*/
|
*/
|
||||||
lockAspectWith?: string;
|
lockAspectWith?: string;
|
||||||
|
/** Показывать чипы быстрого выбора из SIZE_PRESETS. */
|
||||||
|
presets?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SchemaContext {
|
export interface SchemaContext {
|
||||||
@@ -282,6 +287,7 @@ export const field = {
|
|||||||
height: number;
|
height: number;
|
||||||
defaultFromSource?: boolean;
|
defaultFromSource?: boolean;
|
||||||
lockAspectWith?: string;
|
lockAspectWith?: string;
|
||||||
|
presets?: boolean;
|
||||||
}): Field<Dimension> => ({
|
}): Field<Dimension> => ({
|
||||||
spec: { kind: "dimension", ...s },
|
spec: { kind: "dimension", ...s },
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const createEmptySchema = toolSchema<CreateEmptyParams>(
|
|||||||
max: 20000,
|
max: 20000,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
transparent: field.checkbox({ label: "fields.transparent", default: true }),
|
transparent: field.checkbox({ label: "fields.transparent", default: true }),
|
||||||
color: field.color({ label: "fields.fillColor", default: "#ffffff" }),
|
color: field.color({ label: "fields.fillColor", default: "#ffffff" }),
|
||||||
@@ -130,6 +131,7 @@ export const singleColorSchema = toolSchema<SingleColorParams>({
|
|||||||
max: 20000,
|
max: 20000,
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
color: field.color({ label: "fields.color", default: "#ff0000" }),
|
color: field.color({ label: "fields.color", default: "#ff0000" }),
|
||||||
});
|
});
|
||||||
@@ -157,6 +159,7 @@ export const randomNoiseSchema = toolSchema<RandomNoiseParams>({
|
|||||||
max: 5000,
|
max: 5000,
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
seed: field.number({
|
seed: field.number({
|
||||||
label: "fields.seed",
|
label: "fields.seed",
|
||||||
@@ -191,6 +194,7 @@ export const linearGradientSchema = toolSchema<LinearGradientParams>(
|
|||||||
max: 20000,
|
max: 20000,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
gradient: field.gradient({
|
gradient: field.gradient({
|
||||||
label: "fields.gradient",
|
label: "fields.gradient",
|
||||||
@@ -241,6 +245,7 @@ export const colorSpectrumSchema = toolSchema<ColorSpectrumParams>(
|
|||||||
max: 5000,
|
max: 5000,
|
||||||
width: 1024,
|
width: 1024,
|
||||||
height: 128,
|
height: 128,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
direction: field.select({
|
direction: field.select({
|
||||||
label: "fields.direction",
|
label: "fields.direction",
|
||||||
@@ -303,6 +308,7 @@ export const randomColorsSchema = toolSchema<RandomColorsParams>(
|
|||||||
max: 5000,
|
max: 5000,
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
blockSize: field.slider({
|
blockSize: field.slider({
|
||||||
label: "fields.blockSize",
|
label: "fields.blockSize",
|
||||||
@@ -357,6 +363,7 @@ export const drawGridSchema = toolSchema<DrawGridParams>(
|
|||||||
max: 5000,
|
max: 5000,
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
cols: field.slider({
|
cols: field.slider({
|
||||||
label: "fields.cols",
|
label: "fields.cols",
|
||||||
@@ -433,6 +440,7 @@ export const placeholderSchema = toolSchema<PlaceholderParams>(
|
|||||||
max: 5000,
|
max: 5000,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 400,
|
height: 400,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
backgroundColor: field.color({
|
backgroundColor: field.color({
|
||||||
label: "fields.backgroundColor",
|
label: "fields.backgroundColor",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const fitOnBackgroundSchema = toolSchema<FitOnBackgroundParams>(
|
|||||||
max: 20000,
|
max: 20000,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
transparent: field.checkbox({
|
transparent: field.checkbox({
|
||||||
label: "fields.transparent",
|
label: "fields.transparent",
|
||||||
@@ -130,6 +131,7 @@ export const changeCanvasSizeSchema = toolSchema<ChangeCanvasSizeParams>(
|
|||||||
max: 20000,
|
max: 20000,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
anchor: field.select({
|
anchor: field.select({
|
||||||
label: "fields.anchor",
|
label: "fields.anchor",
|
||||||
@@ -186,6 +188,7 @@ export const resizeSchema = toolSchema<ResizeParams>(
|
|||||||
height: 1,
|
height: 1,
|
||||||
defaultFromSource: true,
|
defaultFromSource: true,
|
||||||
lockAspectWith: "keepAspect",
|
lockAspectWith: "keepAspect",
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
keepAspect: field.checkbox({ label: "fields.keepAspect", default: true }),
|
keepAspect: field.checkbox({ label: "fields.keepAspect", default: true }),
|
||||||
},
|
},
|
||||||
@@ -254,6 +257,7 @@ export const cropSchema = toolSchema<CropParams>(
|
|||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
defaultFromSource: true,
|
defaultFromSource: true,
|
||||||
|
presets: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user