From b99ccf5d7808bdd3bca7349b10e1b3858c7e56c3 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 5 Sep 2026 16:55:00 +0500 Subject: [PATCH] feat: add plate (under text) field component --- docs/plan-composite-params.md | 16 +- .../lib/components/kit/SchemaFields.svelte | 2 + .../kit/fields/schema/PlateControl.svelte | 139 ++++++++++++++++++ web/src/lib/registry-new/registry-new.test.ts | 23 ++- web/src/lib/registry-new/text.ts | 26 ++-- web/src/lib/registry-schema.ts | 53 +++++++ 6 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 web/src/lib/components/kit/fields/schema/PlateControl.svelte diff --git a/docs/plan-composite-params.md b/docs/plan-composite-params.md index 57e20db..514bcbc 100644 --- a/docs/plan-composite-params.md +++ b/docs/plan-composite-params.md @@ -12,11 +12,13 @@ > star-mask, wavy-mask); **составной тип `position9` — переведены > add-text-png и date-stamp-png** (водяной знак-картинка — отдельный шаг: > требует overlay-механику в новом превью); **составной тип `font-style` -> полностью переведён** (text-to-png, add-text, date-stamp); preview +> полностью переведён** (text-to-png, add-text, date-stamp); **составной тип +> `plate` полностью переведён** (add-text, date-stamp); preview > научен применять **генераторы** (`executeGenerate`, кнопка Generate) и > рендерить все kinds схемы (slider/number/color/select/checkbox/dimension/ -> color-pair/offset/position9/font-style). -> Следующее: `plate` (add-text → date-stamp). +> color-pair/offset/position9/font-style/plate). +> Следующее: `gradient` — собрать из dimension + color-pair + direction на +> linear-gradient. > > Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts` > (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate) @@ -381,7 +383,13 @@ Typed field builders + `Field` + `toolSchema

()` + `ToolSchema

` — `field.fontStyle`, виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed). -29. `plate` — перевести add-text → date-stamp +29. `plate` — **все 2 инструмента переведены** ✔ (`add-text-png`, + `date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип + во всех видах: вложенный объект `plate: { enabled, color, opacity }` + + `field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс + + цвет + слайдер непрозрачности, деактивируется при выключенной плашке), + kind `plate` в схеме (default/sanitize, clamp opacity к 0..100). + Покрытие тестов расширено (дефолты/sanitize plate в существующих тестах). 30. `gradient` — собрать из dimension + color-pair + direction на linear-gradient (зависит от решения по gradient, см. план) diff --git a/web/src/lib/components/kit/SchemaFields.svelte b/web/src/lib/components/kit/SchemaFields.svelte index 33d173d..5d6c34c 100644 --- a/web/src/lib/components/kit/SchemaFields.svelte +++ b/web/src/lib/components/kit/SchemaFields.svelte @@ -16,6 +16,7 @@ import OffsetControl from "./fields/schema/OffsetControl.svelte"; import PositionControl from "./fields/schema/PositionControl.svelte"; import FontStyleControl from "./fields/schema/FontStyleControl.svelte"; + import PlateControl from "./fields/schema/PlateControl.svelte"; interface FieldControlProps { label: string; @@ -36,6 +37,7 @@ offset: OffsetControl, position9: PositionControl, "font-style": FontStyleControl, + plate: PlateControl, }; interface Props { diff --git a/web/src/lib/components/kit/fields/schema/PlateControl.svelte b/web/src/lib/components/kit/fields/schema/PlateControl.svelte new file mode 100644 index 0000000..8aec4f4 --- /dev/null +++ b/web/src/lib/components/kit/fields/schema/PlateControl.svelte @@ -0,0 +1,139 @@ + + +

+ {label} +
+ + + +
+
+ + diff --git a/web/src/lib/registry-new/registry-new.test.ts b/web/src/lib/registry-new/registry-new.test.ts index bd6ce5c..3b6bacb 100644 --- a/web/src/lib/registry-new/registry-new.test.ts +++ b/web/src/lib/registry-new/registry-new.test.ts @@ -305,12 +305,16 @@ describe("registry-new (переведённые инструменты)", () => expect(s.offset).toEqual({ x: 50, y: 0 }); }); - it("add-text: дефолты position9, font-style и текстовых полей", () => { + it("add-text: дефолты position9, font-style, plate и текстовых полей", () => { const tool = TOOLS.find((t) => t.id === "add-text-png")!; const d = defaultSchemaParams(tool.schema); expect(d.position).toBe("bottom-right"); expect(d.text).toBe("Hello!"); - expect(d.plate).toBe(false); + expect(d.plate).toEqual({ + enabled: false, + color: "#000000", + opacity: 60, + }); expect(d.style).toEqual({ font: "sans", size: 48, @@ -319,7 +323,7 @@ describe("registry-new (переведённые инструменты)", () => }); }); - it("sanitize чинит мусор в position9, font-style и оставляет валидные значения", () => { + it("sanitize чинит мусор в position9, font-style, plate и оставляет валидные значения", () => { const tool = TOOLS.find((t) => t.id === "date-stamp-png")!; const s = sanitizeSchemaParams(tool.schema, { format: "YYYY-MM-DD", @@ -331,9 +335,11 @@ describe("registry-new (переведённые инструменты)", () => }, position: "somewhere-outside", margin: 20, - plate: true, - plateColor: "#000000", - plateOpacity: 55, + plate: { + enabled: "yes", + color: "teal", + opacity: 500, + }, }); expect(s.position).toBe("bottom-right"); expect(s.style).toEqual({ @@ -342,6 +348,11 @@ describe("registry-new (переведённые инструменты)", () => bold: false, color: "#ffffff", }); + expect(s.plate).toEqual({ + enabled: true, + color: "#000000", + opacity: 100, + }); const valid = sanitizeSchemaParams(tool.schema, { ...defaultSchemaParams(tool.schema), position: "top-center", diff --git a/web/src/lib/registry-new/text.ts b/web/src/lib/registry-new/text.ts index 95a1f6b..c888ed6 100644 --- a/web/src/lib/registry-new/text.ts +++ b/web/src/lib/registry-new/text.ts @@ -1,7 +1,7 @@ import { formatStamp } from "../core/datefmt"; import { drawTextBlock } from "../core/domText"; import type { Position9 } from "../core/textdraw"; -import type { FontStyle } from "../registry-schema"; +import type { FontStyle, Plate } from "../registry-schema"; import { field, toolSchema } from "../registry-schema"; import type { ToolEntry } from "./types"; @@ -10,9 +10,7 @@ interface AddTextParams { style: FontStyle; position: Position9; margin: number; - plate: boolean; - plateColor: string; - plateOpacity: number; + plate: Plate; } const addTextSchema = toolSchema({ @@ -27,9 +25,7 @@ const addTextSchema = toolSchema({ }), position: field.position9({ default: "bottom-right" }), margin: field.slider({ min: 0, max: 200, step: 1, default: 24 }), - plate: field.checkbox({ default: false }), - plateColor: field.color({ default: "#000000" }), - plateOpacity: field.slider({ min: 0, max: 100, step: 5, default: 60 }), + plate: field.plate({ enabled: false, color: "#000000", opacity: 60 }), }); const addText: ToolEntry = { @@ -50,8 +46,8 @@ const addText: ToolEntry = { opacityPercent: 100, position: p.position, margin: p.margin, - plateColor: p.plate ? p.plateColor : undefined, - plateOpacityPercent: p.plateOpacity, + plateColor: p.plate.enabled ? p.plate.color : undefined, + plateOpacityPercent: p.plate.opacity, }), }; @@ -60,9 +56,7 @@ interface DateStampParams { style: FontStyle; position: Position9; margin: number; - plate: boolean; - plateColor: string; - plateOpacity: number; + plate: Plate; } const dateStampSchema = toolSchema({ @@ -80,9 +74,7 @@ const dateStampSchema = toolSchema({ }), position: field.position9({ default: "bottom-right" }), margin: field.slider({ min: 0, max: 200, step: 1, default: 20 }), - plate: field.checkbox({ default: true }), - plateColor: field.color({ default: "#000000" }), - plateOpacity: field.slider({ min: 0, max: 100, step: 5, default: 55 }), + plate: field.plate({ enabled: true, color: "#000000", opacity: 55 }), }); const dateStamp: ToolEntry = { @@ -103,8 +95,8 @@ const dateStamp: ToolEntry = { opacityPercent: 100, position: p.position, margin: p.margin, - plateColor: p.plate ? p.plateColor : undefined, - plateOpacityPercent: p.plateOpacity, + plateColor: p.plate.enabled ? p.plate.color : undefined, + plateOpacityPercent: p.plate.opacity, }), }; diff --git a/web/src/lib/registry-schema.ts b/web/src/lib/registry-schema.ts index 088af36..81503f8 100644 --- a/web/src/lib/registry-schema.ts +++ b/web/src/lib/registry-schema.ts @@ -137,6 +137,23 @@ export interface FontStyleSpec { color: string; } +/** + * Подложка-плашка под текстовой надписью: вкл/выкл, цвет и непрозрачность + * как один объект (add-text, date-stamp). + */ +export interface Plate { + enabled: boolean; + color: string; + opacity: number; +} + +export interface PlateSpec { + kind: "plate"; + enabled: boolean; + color: string; + opacity: number; +} + /** * «Объект as const» kind → спека поля. Единственный источник правды для * перечня kinds: `FieldSpecKind` = ключи map, `FieldSpec` = значение по любому @@ -155,6 +172,7 @@ export const fieldSpecs = { offset: {} as OffsetSpec, position9: {} as Position9Spec, "font-style": {} as FontStyleSpec, + plate: {} as PlateSpec, } as const; export type FieldSpecKind = keyof typeof fieldSpecs; @@ -232,6 +250,13 @@ export const field = { }): Field => ({ spec: { kind: "font-style", ...s }, }), + plate: (s: { + enabled: boolean; + color: string; + opacity: number; + }): Field => ({ + spec: { kind: "plate", ...s }, + }), }; /** @@ -271,6 +296,12 @@ export function defaultSchemaParams

( bold: spec.bold, color: spec.color, }; + } else if (spec.kind === "plate") { + out[key] = { + enabled: spec.enabled, + color: spec.color, + opacity: spec.opacity, + }; } else { out[key] = spec.default; } @@ -388,6 +419,28 @@ export function sanitizeSchemaParams

( }; break; } + case "plate": { + const r = + typeof raw === "object" && + raw !== null && + "enabled" in raw && + "color" in raw && + "opacity" in raw + ? (raw as Record) + : undefined; + out[key] = { + enabled: typeof r?.enabled === "boolean" ? r.enabled : spec.enabled, + color: + typeof r?.color === "string" && /^#[0-9a-f]{6}$/i.test(r.color) + ? r.color + : spec.color, + opacity: + typeof r?.opacity === "number" && Number.isFinite(r.opacity) + ? clamp(r.opacity, 0, 100) + : spec.opacity, + }; + break; + } case "offset": { const r = typeof raw === "object" && raw !== null && "x" in raw && "y" in raw