mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: separate schema tool state
This commit is contained in:
@@ -21,26 +21,19 @@
|
|||||||
|
|
||||||
## High
|
## High
|
||||||
|
|
||||||
### H1. `SchemaToolView.svelte` — god-component — В РАБОТЕ
|
### H1. `SchemaToolView.svelte` — god-component — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
План декомпозиции (подшаги, каждый отдельным ревью-коммитом):
|
Компонент уменьшен со 1115 до ~660 строк (оркестратор: derived-связки, init,
|
||||||
|
handleFiles, эффекты, workspace-разметка). Декомпозиция:
|
||||||
|
|
||||||
- **H1a — СДЕЛАНО (2026-10):** aligned-раскладка вынесена в
|
- **H1a:** aligned-раскладка вынесена в `SchemaAlignedLayout.svelte` (view-only:
|
||||||
`SchemaAlignedLayout.svelte` (view-only: разметка + scoped-стили, сниппет
|
разметка + scoped-стили, сниппет `stepCard` передаётся пропсом).
|
||||||
`stepCard` остался в родителе и передаётся пропсом). Родитель уменьшился
|
- **H1b:** runner вынесен в `schema-tool-runner.svelte.ts`
|
||||||
примерно на 200 строк.
|
(`createSchemaToolRunner(ctx)`); идиома «`$state`-объект + модульные функции»
|
||||||
- **H1b — СДЕЛАНО (2026-10):** runner вынесен в `schema-tool-runner.svelte.ts`
|
зафиксирована в `docs/architecture.md`, раздел 9.
|
||||||
(`createSchemaToolRunner(ctx)`: состояние в `$state`-объекте `RunnerState`,
|
- **H1c:** стейт шагов вынесен в `schema-tool-state.svelte.ts`
|
||||||
действия — модульные функции run/download/
|
(`createSchemaToolState(ctx)`: steps, touchedByStep/lastAxis, все мутации,
|
||||||
copyText/downloadText/assignResult/toDisplayError, геттеры наружу; лимит
|
DnD; `toolSchemaOf` — модульный экспорт).
|
||||||
`max-lines-per-function` не даёт держать всё в одной фабрике). Компонент
|
|
||||||
читает состояние через `$derived`-алиасы.
|
|
||||||
- **H1c (далее):** стейт шагов (мутации `steps`, `touchedByStep`, `lastAxis`) →
|
|
||||||
`schema-tool-state.svelte.ts`.
|
|
||||||
|
|
||||||
Исходная формулировка: ~10 ответственностей в одном компоненте — состояние
|
|
||||||
цепочки шагов, загрузка файлов, оркестрация запуска, политика скачивания,
|
|
||||||
clipboard, paste-listener, mask-превью, drag&drop, aspect-lock, две раскладки.
|
|
||||||
|
|
||||||
### H2. Race condition превью — СДЕЛАНО (2026-10)
|
### H2. Race condition превью — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
|
|
||||||
## Pipeline
|
## Pipeline
|
||||||
|
|
||||||
|
- [ ] **баг: drag-n-drop перестановки шагов не работает** — обработчики на месте
|
||||||
|
(`StepCard` draggable в сниппете `stepCard` `SchemaToolView.svelte`,
|
||||||
|
`onStepDrop` → `moveStep` в `schema-tool-state.svelte.ts`), но
|
||||||
|
перетаскивание не срабатывает. Замечено до рефактора H1 (2026-10), не
|
||||||
|
регрессия от него. Начать с проверки `draggable`/`dragstart` на `StepCard`
|
||||||
|
и того, что drop не глушится дочерними элементами карточки
|
||||||
- [ ] названия инструментов — в pipeline инструмент называется «обрезать png»,
|
- [ ] названия инструментов — в pipeline инструмент называется «обрезать png»,
|
||||||
но он работает с любыми картинками, наверное надо убрать; в заголовке
|
но он работает с любыми картинками, наверное надо убрать; в заголовке
|
||||||
страницы — возможно, надо указать все форматы (связано с SEO)
|
страницы — возможно, надо указать все форматы (связано с SEO)
|
||||||
|
|||||||
@@ -32,11 +32,7 @@
|
|||||||
} from "$lib/chains.svelte";
|
} from "$lib/chains.svelte";
|
||||||
import {
|
import {
|
||||||
createChain,
|
createChain,
|
||||||
createStep,
|
|
||||||
insertStep,
|
|
||||||
isStructuralDefault,
|
isStructuralDefault,
|
||||||
moveStep,
|
|
||||||
removeStep,
|
|
||||||
type ChainStep,
|
type ChainStep,
|
||||||
} from "$lib/pipeline.svelte";
|
} from "$lib/pipeline.svelte";
|
||||||
import {
|
import {
|
||||||
@@ -48,19 +44,20 @@
|
|||||||
import {
|
import {
|
||||||
applySourceDefaults,
|
applySourceDefaults,
|
||||||
clampSourceAwareMaxes,
|
clampSourceAwareMaxes,
|
||||||
defaultSchemaParams,
|
|
||||||
withAspectLock,
|
|
||||||
type Dimension,
|
|
||||||
type ToolSchema,
|
type ToolSchema,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
import AddStepButton from "./AddStepButton.svelte";
|
import AddStepButton from "./AddStepButton.svelte";
|
||||||
import SchemaAlignedLayout from "./SchemaAlignedLayout.svelte";
|
import SchemaAlignedLayout from "./SchemaAlignedLayout.svelte";
|
||||||
import SchemaFields from "./SchemaFields.svelte";
|
import SchemaFields from "./SchemaFields.svelte";
|
||||||
import SchemaPreview from "./SchemaPreview.svelte";
|
import SchemaPreview from "./SchemaPreview.svelte";
|
||||||
import ToolPickerButton from "./ToolPickerButton.svelte";
|
import ToolPickerButton from "./ToolPickerButton.svelte";
|
||||||
import { createSchemaToolRunner } from "./schema-tool-runner.svelte";
|
import { createSchemaToolRunner } from "./schema-tool-runner.svelte";
|
||||||
|
import {
|
||||||
|
createSchemaToolState,
|
||||||
|
toolSchemaOf,
|
||||||
|
} from "./schema-tool-state.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: Page;
|
page: Page;
|
||||||
@@ -76,12 +73,6 @@
|
|||||||
|
|
||||||
const inputMode = $derived(tool.input);
|
const inputMode = $derived(tool.input);
|
||||||
|
|
||||||
let steps = $state<ChainStep[]>([]);
|
|
||||||
// Fields edited by the user (per step): on new file load they must not be
|
|
||||||
// overwritten by source defaults. Step reset clears the mark.
|
|
||||||
const touchedByStep = new SvelteMap<string, SvelteSet<string>>();
|
|
||||||
// Last edited axis of a dimension field; leads under lockAspect.
|
|
||||||
const lastAxis: Record<string, "width" | "height"> = {};
|
|
||||||
let aligned = $state(false);
|
let aligned = $state(false);
|
||||||
let source = $state<PixelImage | null>(null);
|
let source = $state<PixelImage | null>(null);
|
||||||
// The full named input set (multi-upload); `source` above is the first
|
// The full named input set (multi-upload); `source` above is the first
|
||||||
@@ -119,7 +110,22 @@
|
|||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
let dragFrom = $state<number | null>(null);
|
|
||||||
|
const stepState = createSchemaToolState({
|
||||||
|
source: () => source,
|
||||||
|
stepDims: () => runner.stepDims,
|
||||||
|
clearResults: () => runner.clearResults(),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
setStepValue,
|
||||||
|
resetStep,
|
||||||
|
addStepAt,
|
||||||
|
removeStepAt,
|
||||||
|
toggleStep,
|
||||||
|
replaceStepTool,
|
||||||
|
onStepDrop,
|
||||||
|
} = stepState;
|
||||||
|
const steps = $derived(stepState.steps);
|
||||||
|
|
||||||
const runner = createSchemaToolRunner({
|
const runner = createSchemaToolRunner({
|
||||||
pageSlug: () => page.slug,
|
pageSlug: () => page.slug,
|
||||||
@@ -206,19 +212,6 @@
|
|||||||
|
|
||||||
const debouncedRun = debounce(() => run(), 200);
|
const debouncedRun = debounce(() => run(), 200);
|
||||||
|
|
||||||
function touchedFor(key: string): SvelteSet<string> {
|
|
||||||
let set = touchedByStep.get(key);
|
|
||||||
if (!set) {
|
|
||||||
set = new SvelteSet();
|
|
||||||
touchedByStep.set(key, set);
|
|
||||||
}
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toolSchemaOf(step: ChainStep) {
|
|
||||||
return getTool(step.id)?.schema as ToolSchema<Record<string, unknown>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stepTitle(toolId: string): string {
|
function stepTitle(toolId: string): string {
|
||||||
return chainStepTitle(toolId);
|
return chainStepTitle(toolId);
|
||||||
}
|
}
|
||||||
@@ -232,7 +225,7 @@
|
|||||||
runner.resetForHost();
|
runner.resetForHost();
|
||||||
if (chainId) {
|
if (chainId) {
|
||||||
const handoff = takeHandoff(chainId);
|
const handoff = takeHandoff(chainId);
|
||||||
steps = getChain(chainId)?.steps ?? createChain(page);
|
stepState.steps = getChain(chainId)?.steps ?? createChain(page);
|
||||||
source = handoff?.source ?? null;
|
source = handoff?.source ?? null;
|
||||||
sourceFiles =
|
sourceFiles =
|
||||||
handoff?.sourceFiles ??
|
handoff?.sourceFiles ??
|
||||||
@@ -244,7 +237,7 @@
|
|||||||
else format = lastTool.output?.mime ?? "image/png";
|
else format = lastTool.output?.mime ?? "image/png";
|
||||||
limitKb = handoff?.limitKb;
|
limitKb = handoff?.limitKb;
|
||||||
} else {
|
} else {
|
||||||
steps = createChain(page);
|
stepState.steps = createChain(page);
|
||||||
format = lastTool.output?.mime ?? "image/png";
|
format = lastTool.output?.mime ?? "image/png";
|
||||||
limitKb = undefined;
|
limitKb = undefined;
|
||||||
source = null;
|
source = null;
|
||||||
@@ -253,105 +246,7 @@
|
|||||||
}
|
}
|
||||||
sourceWarnings = [];
|
sourceWarnings = [];
|
||||||
maskOnKeys.clear();
|
maskOnKeys.clear();
|
||||||
// Marks are keyed by step.key from the previous host; keep them and
|
stepState.clearMarks();
|
||||||
// stale keys leak into the new chain's fields.
|
|
||||||
touchedByStep.clear();
|
|
||||||
for (const k of Object.keys(lastAxis)) delete lastAxis[k];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStepValue(
|
|
||||||
index: number,
|
|
||||||
id: string,
|
|
||||||
value: unknown,
|
|
||||||
axis?: "width" | "height" | "both",
|
|
||||||
) {
|
|
||||||
const step = steps[index];
|
|
||||||
if (!step) return;
|
|
||||||
touchedFor(step.key).add(id);
|
|
||||||
const axisKey = `${step.key}:${id}`;
|
|
||||||
if (axis) lastAxis[axisKey] = axis === "both" ? "width" : axis;
|
|
||||||
const next: Record<string, unknown> = { ...step.params, [id]: value };
|
|
||||||
const stepSchema = toolSchemaOf(step);
|
|
||||||
const spec = stepSchema?.fields[id]?.spec;
|
|
||||||
// Aspect derives from the step input: source for the first step,
|
|
||||||
// previous step's result for the rest (dims known after the run).
|
|
||||||
const dims = index === 0 ? (source ?? undefined) : stepDims[index - 1];
|
|
||||||
if (dims && stepSchema && spec) {
|
|
||||||
const aspect = dims.width / dims.height;
|
|
||||||
if (
|
|
||||||
spec.kind === "dimension" &&
|
|
||||||
spec.lockAspectWith &&
|
|
||||||
axis !== "both" &&
|
|
||||||
next[spec.lockAspectWith] === true
|
|
||||||
) {
|
|
||||||
next[id] = withAspectLock(
|
|
||||||
value as Dimension,
|
|
||||||
lastAxis[axisKey] ?? "width",
|
|
||||||
aspect,
|
|
||||||
);
|
|
||||||
} else if (spec.kind === "checkbox" && value === true) {
|
|
||||||
// lockAspect just enabled: snap bound fields to the aspect at once.
|
|
||||||
for (const [fid, f] of Object.entries(stepSchema.fields)) {
|
|
||||||
const fs = f.spec;
|
|
||||||
if (fs.kind === "dimension" && fs.lockAspectWith === id) {
|
|
||||||
next[fid] = withAspectLock(
|
|
||||||
next[fid] as Dimension,
|
|
||||||
lastAxis[`${step.key}:${fid}`] ?? "width",
|
|
||||||
aspect,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const clamped = dims ? clampSourceAwareMaxes(stepSchema, next, dims) : next;
|
|
||||||
steps = steps.with(index, { ...step, params: clamped });
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetStep(index: number) {
|
|
||||||
const step = steps[index];
|
|
||||||
if (!step) return;
|
|
||||||
touchedFor(step.key).clear();
|
|
||||||
const stepSchema = toolSchemaOf(step);
|
|
||||||
const dims = index === 0 ? (source ?? undefined) : stepDims[index - 1];
|
|
||||||
const params = stepSchema
|
|
||||||
? defaultSchemaParams(stepSchema, { source: dims })
|
|
||||||
: {};
|
|
||||||
steps = steps.with(index, { ...step, params });
|
|
||||||
runner.clearResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addStepAt(index: number, toolId: string) {
|
|
||||||
steps = insertStep(steps, index, toolId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeStepAt(key: string) {
|
|
||||||
steps = removeStep(steps, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStep(index: number) {
|
|
||||||
const step = steps[index];
|
|
||||||
if (!step) return;
|
|
||||||
steps = steps.with(index, { ...step, collapsed: !step.collapsed });
|
|
||||||
}
|
|
||||||
|
|
||||||
// In-place tool swap: step key and collapsed state are kept, params become
|
|
||||||
// the new tool's defaults, input is recomputed on run.
|
|
||||||
function replaceStepTool(index: number, toolId: string) {
|
|
||||||
const step = steps[index];
|
|
||||||
const next = createStep(toolId);
|
|
||||||
if (!step || !next) return;
|
|
||||||
touchedFor(step.key).clear();
|
|
||||||
steps = steps.with(index, {
|
|
||||||
...next,
|
|
||||||
key: step.key,
|
|
||||||
collapsed: step.collapsed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStepDrop(e: DragEvent, to: number) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (dragFrom !== null) steps = moveStep(steps, dragFrom, to);
|
|
||||||
dragFrom = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFiles(files: File[]) {
|
async function handleFiles(files: File[]) {
|
||||||
@@ -382,7 +277,7 @@
|
|||||||
const first = steps[0];
|
const first = steps[0];
|
||||||
const firstSchema = first ? toolSchemaOf(first) : undefined;
|
const firstSchema = first ? toolSchemaOf(first) : undefined;
|
||||||
if (first && firstSchema) {
|
if (first && firstSchema) {
|
||||||
steps = steps.with(0, {
|
stepState.steps = steps.with(0, {
|
||||||
...first,
|
...first,
|
||||||
params: clampSourceAwareMaxes(
|
params: clampSourceAwareMaxes(
|
||||||
firstSchema,
|
firstSchema,
|
||||||
@@ -390,7 +285,7 @@
|
|||||||
firstSchema,
|
firstSchema,
|
||||||
first.params,
|
first.params,
|
||||||
{ source: firstImage },
|
{ source: firstImage },
|
||||||
touchedFor(first.key),
|
stepState.touchedFor(first.key),
|
||||||
),
|
),
|
||||||
firstImage,
|
firstImage,
|
||||||
),
|
),
|
||||||
@@ -553,7 +448,7 @@
|
|||||||
ontoggle={() => toggleStep(i)}
|
ontoggle={() => toggleStep(i)}
|
||||||
onremove={steps.length > 1 ? () => removeStepAt(step.key) : undefined}
|
onremove={steps.length > 1 ? () => removeStepAt(step.key) : undefined}
|
||||||
ondragstart={(e) => {
|
ondragstart={(e) => {
|
||||||
dragFrom = i;
|
stepState.dragFrom = i;
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
e.dataTransfer.setData("text/plain", String(i));
|
e.dataTransfer.setData("text/plain", String(i));
|
||||||
@@ -564,7 +459,7 @@
|
|||||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||||
}}
|
}}
|
||||||
ondrop={(e) => onStepDrop(e, i)}
|
ondrop={(e) => onStepDrop(e, i)}
|
||||||
ondragend={() => (dragFrom = null)}
|
ondragend={() => (stepState.dragFrom = null)}
|
||||||
>
|
>
|
||||||
{#snippet tools()}
|
{#snippet tools()}
|
||||||
<ToolPickerButton
|
<ToolPickerButton
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import type { PixelImage } from "$lib/core/types";
|
||||||
|
import {
|
||||||
|
createStep,
|
||||||
|
insertStep,
|
||||||
|
moveStep,
|
||||||
|
removeStep,
|
||||||
|
type ChainStep,
|
||||||
|
} from "$lib/pipeline.svelte";
|
||||||
|
import { getTool } from "$lib/registry";
|
||||||
|
import {
|
||||||
|
clampSourceAwareMaxes,
|
||||||
|
defaultSchemaParams,
|
||||||
|
withAspectLock,
|
||||||
|
type Dimension,
|
||||||
|
type ToolSchema,
|
||||||
|
} from "$lib/registry-schema";
|
||||||
|
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
||||||
|
|
||||||
|
export interface SchemaToolStateCtx {
|
||||||
|
source: () => PixelImage | null;
|
||||||
|
stepDims: () => (Dimension | undefined)[];
|
||||||
|
clearResults: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepsState {
|
||||||
|
steps: ChainStep[];
|
||||||
|
dragFrom: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toolSchemaOf(step: ChainStep) {
|
||||||
|
return getTool(step.id)?.schema as ToolSchema<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStepValue(
|
||||||
|
s: StepsState,
|
||||||
|
marks: Marks,
|
||||||
|
ctx: SchemaToolStateCtx,
|
||||||
|
index: number,
|
||||||
|
id: string,
|
||||||
|
value: unknown,
|
||||||
|
axis?: "width" | "height" | "both",
|
||||||
|
) {
|
||||||
|
const step = s.steps[index];
|
||||||
|
if (!step) return;
|
||||||
|
touchedFor(marks, step.key).add(id);
|
||||||
|
const axisKey = `${step.key}:${id}`;
|
||||||
|
if (axis) marks.lastAxis[axisKey] = axis === "both" ? "width" : axis;
|
||||||
|
const next: Record<string, unknown> = { ...step.params, [id]: value };
|
||||||
|
const stepSchema = toolSchemaOf(step);
|
||||||
|
const spec = stepSchema?.fields[id]?.spec;
|
||||||
|
// Aspect derives from the step input: source for the first step,
|
||||||
|
// previous step's result for the rest (dims known after the run).
|
||||||
|
const dims =
|
||||||
|
index === 0 ? (ctx.source() ?? undefined) : ctx.stepDims()[index - 1];
|
||||||
|
if (dims && stepSchema && spec) {
|
||||||
|
const aspect = dims.width / dims.height;
|
||||||
|
if (
|
||||||
|
spec.kind === "dimension" &&
|
||||||
|
spec.lockAspectWith &&
|
||||||
|
axis !== "both" &&
|
||||||
|
next[spec.lockAspectWith] === true
|
||||||
|
) {
|
||||||
|
next[id] = withAspectLock(
|
||||||
|
value as Dimension,
|
||||||
|
marks.lastAxis[axisKey] ?? "width",
|
||||||
|
aspect,
|
||||||
|
);
|
||||||
|
} else if (spec.kind === "checkbox" && value === true) {
|
||||||
|
// lockAspect just enabled: snap bound fields to the aspect at once.
|
||||||
|
for (const [fid, f] of Object.entries(stepSchema.fields)) {
|
||||||
|
const fs = f.spec;
|
||||||
|
if (fs.kind === "dimension" && fs.lockAspectWith === id) {
|
||||||
|
next[fid] = withAspectLock(
|
||||||
|
next[fid] as Dimension,
|
||||||
|
marks.lastAxis[`${step.key}:${fid}`] ?? "width",
|
||||||
|
aspect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clamped = dims ? clampSourceAwareMaxes(stepSchema, next, dims) : next;
|
||||||
|
s.steps = s.steps.with(index, { ...step, params: clamped });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetStep(
|
||||||
|
s: StepsState,
|
||||||
|
marks: Marks,
|
||||||
|
ctx: SchemaToolStateCtx,
|
||||||
|
index: number,
|
||||||
|
) {
|
||||||
|
const step = s.steps[index];
|
||||||
|
if (!step) return;
|
||||||
|
touchedFor(marks, step.key).clear();
|
||||||
|
const stepSchema = toolSchemaOf(step);
|
||||||
|
const dims =
|
||||||
|
index === 0 ? (ctx.source() ?? undefined) : ctx.stepDims()[index - 1];
|
||||||
|
const params = stepSchema
|
||||||
|
? defaultSchemaParams(stepSchema, { source: dims })
|
||||||
|
: {};
|
||||||
|
s.steps = s.steps.with(index, { ...step, params });
|
||||||
|
ctx.clearResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Marks {
|
||||||
|
// Fields edited by the user (per step): on new file load they must not be
|
||||||
|
// overwritten by source defaults. Step reset clears the mark.
|
||||||
|
touchedByStep: SvelteMap<string, SvelteSet<string>>;
|
||||||
|
// Last edited axis of a dimension field; leads under lockAspect.
|
||||||
|
lastAxis: Record<string, "width" | "height">;
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchedFor(marks: Marks, key: string): SvelteSet<string> {
|
||||||
|
let set = marks.touchedByStep.get(key);
|
||||||
|
if (!set) {
|
||||||
|
set = new SvelteSet();
|
||||||
|
marks.touchedByStep.set(key, set);
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chain-step state of SchemaToolView: the steps array, per-step edit marks
|
||||||
|
// and all mutations. Results/execution live in schema-tool-runner.svelte.ts.
|
||||||
|
export function createSchemaToolState(ctx: SchemaToolStateCtx) {
|
||||||
|
const s = $state<StepsState>({ steps: [], dragFrom: null });
|
||||||
|
const marks: Marks = {
|
||||||
|
touchedByStep: new SvelteMap(),
|
||||||
|
lastAxis: {},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
get steps() {
|
||||||
|
return s.steps;
|
||||||
|
},
|
||||||
|
set steps(v: ChainStep[]) {
|
||||||
|
s.steps = v;
|
||||||
|
},
|
||||||
|
get dragFrom() {
|
||||||
|
return s.dragFrom;
|
||||||
|
},
|
||||||
|
set dragFrom(v: number | null) {
|
||||||
|
s.dragFrom = v;
|
||||||
|
},
|
||||||
|
touchedFor: (key: string) => touchedFor(marks, key),
|
||||||
|
// Marks are keyed by step.key from the previous host; keep them and
|
||||||
|
// stale keys leak into the new chain's fields.
|
||||||
|
clearMarks: () => {
|
||||||
|
marks.touchedByStep.clear();
|
||||||
|
for (const k of Object.keys(marks.lastAxis)) delete marks.lastAxis[k];
|
||||||
|
},
|
||||||
|
setStepValue: (
|
||||||
|
index: number,
|
||||||
|
id: string,
|
||||||
|
value: unknown,
|
||||||
|
axis?: "width" | "height" | "both",
|
||||||
|
) => setStepValue(s, marks, ctx, index, id, value, axis),
|
||||||
|
resetStep: (index: number) => resetStep(s, marks, ctx, index),
|
||||||
|
addStepAt: (index: number, toolId: string) => {
|
||||||
|
s.steps = insertStep(s.steps, index, toolId);
|
||||||
|
},
|
||||||
|
removeStepAt: (key: string) => {
|
||||||
|
s.steps = removeStep(s.steps, key);
|
||||||
|
},
|
||||||
|
toggleStep: (index: number) => {
|
||||||
|
const step = s.steps[index];
|
||||||
|
if (!step) return;
|
||||||
|
s.steps = s.steps.with(index, {
|
||||||
|
...step,
|
||||||
|
collapsed: !step.collapsed,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// In-place tool swap: step key and collapsed state are kept, params
|
||||||
|
// become the new tool's defaults, input is recomputed on run.
|
||||||
|
replaceStepTool: (index: number, toolId: string) => {
|
||||||
|
const step = s.steps[index];
|
||||||
|
const next = createStep(toolId);
|
||||||
|
if (!step || !next) return;
|
||||||
|
touchedFor(marks, step.key).clear();
|
||||||
|
s.steps = s.steps.with(index, {
|
||||||
|
...next,
|
||||||
|
key: step.key,
|
||||||
|
collapsed: step.collapsed,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStepDrop: (e: DragEvent, to: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (s.dragFrom !== null) s.steps = moveStep(s.steps, s.dragFrom, to);
|
||||||
|
s.dragFrom = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user