// Новая типизированная схема параметров (для нового 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;
}