// Новая типизированная схема параметров (для нового UI, старый UI не трогаем). // // Параллельный источник правды к старому `ParamDef[]`: старый pipeline // продолжает работать на `ParamDef[]` как сейчас, а новый строит UI/deфолты/ // валидацию поверх `ToolSchema

`. // // Связка типов на этапе компиляции (защита от рассинхрона): // - `field.x()` возвращает `Field` — phantom `__fieldT` связывает // runtime-спецификацию поля с ожидаемым типом значения (`number`|`string`|`boolean`). // - `toolSchema

(fields)` требует, чтобы ключи `fields` в точности совпадали // с полями `P`, а `Field` тип-проверялся на соответствие `P[K]`. // Ошибки компилятора ловят расхождения между интерфейсом Params и схемой. export interface NumberSpec { kind: "number"; default: number; min?: number; max?: number; step?: number; } export interface SliderSpec { kind: "slider"; default: number; min: number; max: number; step?: number; } export interface ColorSpec { kind: "color"; default: string; } export interface SelectSpec { kind: "select"; default: V; options: { value: V; label: string }[]; } export interface TextSpec { kind: "text"; default: string; placeholder?: string; } export interface CheckboxSpec { kind: "checkbox"; default: boolean; } /** Составное поле «размеры»: width + height как один объект. */ export interface Dimension { width: number; height: number; } export interface DimensionSpec { kind: "dimension"; /** Общий диапазон для обоих измерений. */ min: number; max: number; width: number; height: number; } export type FieldSpec = | NumberSpec | SliderSpec | ColorSpec | SelectSpec | TextSpec | CheckboxSpec | DimensionSpec; /** `Field`: runtime-спека поля + phantom-тип ожидаемого значения (number|string|boolean). */ export interface Field { spec: FieldSpec; /** Phantom, только для типобезопасности — не задаётся и не читается в рантайме. */ __fieldT?: T; } export interface ToolSchemaMeta { /** Пер-инструмент раскладка полей (группировка/колонки). */ layout?: { group?: string; cols?: number }; /** Короткая подпись инструмента для нового UI (необязательно). */ label?: string; } export interface ToolSchema

{ fields: { [K in keyof P]: Field }; layout?: { group?: string; cols?: number }; label?: string; } export const field = { number: (s: Omit): Field => ({ spec: { kind: "number", ...s }, }), slider: (s: Omit): Field => ({ spec: { kind: "slider", ...s }, }), color: (s: Omit): Field => ({ spec: { kind: "color", ...s }, }), select: (s: Omit, "kind">): Field => ({ spec: { kind: "select", ...s }, }), text: (s: Omit): Field => ({ spec: { kind: "text", ...s }, }), checkbox: (s: Omit): Field => ({ spec: { kind: "checkbox", ...s }, }), dimension: (s: { min: number; max: number; width: number; height: number }): Field => ({ spec: { kind: "dimension", ...s }, }), }; /** * Собирает `ToolSchema

` из объявленных полей. * Компилятор проверяет: ключи `fields` === полям `P`, типы полей совместимы с `P[K]`. */ export function toolSchema

( fields: { [K in keyof P]: Field }, meta?: ToolSchemaMeta, ): ToolSchema

{ return { fields, ...(meta ?? {}) }; } function clamp(value: number, min?: number, max?: number): number { if (min !== undefined && value < min) return min; if (max !== undefined && value > max) return max; return value; } /** Дефолты из схемы — единый источник значений по умолчанию для нового UI. */ export function defaultSchemaParams

( schema: ToolSchema

, ): Record { const out = {} as Record; for (const key of Object.keys(schema.fields) as (keyof P)[]) { const spec = schema.fields[key].spec; if (spec.kind === "dimension") { out[key] = { width: spec.width, height: spec.height }; } else { out[key] = spec.default; } } return out; } /** Валидация/нормализация значений по схеме. Аналог старого `sanitizeParams`. */ export function sanitizeSchemaParams

( schema: ToolSchema

, values: Record, ): Record { const out = {} as Record; for (const key of Object.keys(schema.fields) as (keyof P)[]) { const spec = schema.fields[key].spec; const raw = values[key as string]; switch (spec.kind) { case "number": case "slider": { const n = typeof raw === "number" && Number.isFinite(raw) ? raw : spec.default; out[key] = clamp(n, spec.min, spec.max); break; } case "select": out[key] = typeof raw === "string" && spec.options.some((o) => o.value === raw) ? raw : spec.default; break; case "checkbox": out[key] = typeof raw === "boolean" ? raw : spec.default; break; case "color": out[key] = typeof raw === "string" && /^#[0-9a-f]{6}$/i.test(raw) ? raw : spec.default; break; case "text": out[key] = typeof raw === "string" ? raw : spec.default; break; case "dimension": { const r = typeof raw === "object" && raw !== null && "width" in raw && "height" in raw ? (raw as Record) : undefined; const w = typeof r?.width === "number" && Number.isFinite(r.width) ? r.width : spec.width; const h = typeof r?.height === "number" && Number.isFinite(r.height) ? r.height : spec.height; out[key] = { width: clamp(w, spec.min, spec.max), height: clamp(h, spec.min, spec.max), }; break; } } } return out; }