From b7be973b54091b071e0c751533a73ad1d0ea11fb Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Wed, 7 Oct 2026 07:26:32 +0500 Subject: [PATCH] feat: aligned mode for pipeline --- docs/backlog.md | 8 - .../components/schema/SchemaPreview.svelte | 41 +- .../components/schema/SchemaToolView.svelte | 381 ++++++++++++++---- web/src/lib/i18n/en.ts | 2 + web/src/lib/i18n/ru.ts | 2 + 5 files changed, 346 insertions(+), 88 deletions(-) diff --git a/docs/backlog.md b/docs/backlog.md index 6add163..1880144 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -54,14 +54,6 @@ браузере, порядок файлов, поведение many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители 1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8 «batch-страница». -- [ ] **Идея 17. Промежуточные результаты шагов + выравнивание рядов** — оценка - M. Два режима просмотра цепочки: - **без выравнивания (по умолчанию):** в - панели просмотра цепочка изображений по 2 в колонку «исходник → результат - 1 → результат 2 → … → конечный результат»; - **с выравниванием (галочка - «выровнять инструменты и просмотр»):** у каждого инструмента показаны его - исходник и результат построчно «параметры — исходник — результат»; - промежуточные картинки при этом осознанно дублируются на экране — - результат шага является исходником следующего. ## Инструменты diff --git a/web/src/lib/components/schema/SchemaPreview.svelte b/web/src/lib/components/schema/SchemaPreview.svelte index 1ca4e71..bbf4962 100644 --- a/web/src/lib/components/schema/SchemaPreview.svelte +++ b/web/src/lib/components/schema/SchemaPreview.svelte @@ -3,6 +3,9 @@ import SchemaActions from "./SchemaActions.svelte"; import SchemaResultTile from "./SchemaResultTile.svelte"; import SchemaSourceTile from "./SchemaSourceTile.svelte"; + import PreviewTile from "./PreviewTile.svelte"; + import Toggle from "$lib/components/ui/Toggle.svelte"; + import { toDataUrl } from "$lib/core/io"; import type { PixelImage } from "$lib/core/types"; import type { OutputMime } from "$lib/core/io"; import { t } from "$lib/i18n/t"; @@ -26,6 +29,9 @@ textVars?: Record; running?: boolean; error?: string; + stepResults?: (PixelImage | null)[]; + aligned?: boolean; + ontogglealign?: () => void; format?: OutputMime; quality?: number; alphaLoss?: boolean; @@ -51,6 +57,9 @@ textVars = undefined, running = false, error = "", + stepResults = [], + aligned = false, + ontogglealign, format = "image/png", quality = undefined, alphaLoss = false, @@ -71,6 +80,16 @@
{t("resultCard.previewPanel")} + {#if stepResults.length > 1} + + {/if} + {#each stepResults.slice(0, -1) as img, i (i)} + {#if img} + + + + {/if} + {/each} - import AddStepButton from "./AddStepButton.svelte"; - import SchemaFields from "./SchemaFields.svelte"; - import SchemaPreview from "./SchemaPreview.svelte"; - import ToolPickerButton from "./ToolPickerButton.svelte"; import StepCard from "$lib/components/display/StepCard.svelte"; + import Toggle from "$lib/components/ui/Toggle.svelte"; import { hasTransparency } from "$lib/core/analyze"; import { debounce } from "$lib/core/debounce"; import { ToolError, type ErrorVars } from "$lib/core/errors"; @@ -12,6 +9,7 @@ downloadBlob, encode, outputFormatByMime, + toDataUrl, type OutputMime, } from "$lib/core/io"; import type { PixelImage } from "$lib/core/types"; @@ -51,6 +49,14 @@ import { downloadZip } from "$lib/zip"; import { onMount } from "svelte"; import { SvelteMap, SvelteSet } from "svelte/reactivity"; + import AddStepButton from "./AddStepButton.svelte"; + import PreviewTile from "./PreviewTile.svelte"; + import SchemaActions from "./SchemaActions.svelte"; + import SchemaFields from "./SchemaFields.svelte"; + import SchemaPreview from "./SchemaPreview.svelte"; + import SchemaResultTile from "./SchemaResultTile.svelte"; + import SchemaSourceTile from "./SchemaSourceTile.svelte"; + import ToolPickerButton from "./ToolPickerButton.svelte"; interface Props { page: Page; @@ -74,6 +80,8 @@ // Последняя правленая ось dimension-поля — ведущая при lockAspect. const lastAxis: Record = {}; let stepDims = $state<(Dimension | undefined)[]>([]); + let stepResults = $state<(PixelImage | null)[]>([]); + let aligned = $state(false); let source = $state(null); let result = $state(null); let fileResult = $state(null); @@ -97,6 +105,9 @@ const resultKind = $derived(lastTool.result ?? "image"); // Цепочку можно продолжить, только если последний шаг отдаёт картинку. const canExtend = $derived((lastTool.result ?? "image") === "image"); + const alignedMode = $derived( + aligned && resultKind === "image" && steps.length > 1, + ); const alphaLoss = $derived( resultKind === "image" && @@ -327,6 +338,7 @@ try { let current: PixelImage | undefined = source ?? undefined; const dims: (Dimension | undefined)[] = []; + const results: (PixelImage | null)[] = []; let out: ToolResult = ""; for (let i = 0; i < steps.length; i++) { const stepTool = getTool(steps[i].id); @@ -353,11 +365,14 @@ if (out && typeof out === "object" && "data" in out) { current = out as PixelImage; dims[i] = { width: current.width, height: current.height }; + results[i] = current; } else { current = undefined; + results[i] = null; } } stepDims = dims; + stepResults = results; assignResult(out); } catch (e) { displayError = toDisplayError(e); @@ -470,87 +485,204 @@
-
-
+ {#snippet stepCard(step: ChainStep, i: number)} + toggleStep(i)} + onremove={steps.length > 1 ? () => removeStepAt(step.key) : undefined} + ondragstart={(e) => { + dragFrom = i; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", String(i)); + } + }} + ondragover={(e) => { + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; + }} + ondrop={(e) => onStepDrop(e, i)} + ondragend={() => (dragFrom = null)} + > + {#snippet tools()} + replaceStepTool(i, id)} + /> + {/snippet} + setStepValue(i, id, v, axis)} + onreset={() => resetStep(i)} + /> + + {/snippet} + + {#if !alignedMode} +
+
+ {#if canExtend} + addStepAt(0, id)} /> + {/if} + {#each steps as step, i (step.key)} + {@render stepCard(step, i)} + {#if canExtend} + addStepAt(i + 1, id)} /> + {/if} + {/each} +
+ +
+ (aligned = !aligned)} + onformat={(v) => (format = v)} + onquality={setFormatQuality} + onupload={handleFile} + ontextsource={(textValue) => { + textSource = textValue; + }} + onrendertext={run} + oncopytext={copyText} + ondownloadtxt={downloadText} + ondownload={download} + /> +
+
+ {:else} +
+
+
+
+ {t("resultCard.previewPanel")} + + (format = v)} + onquality={setFormatQuality} + /> +
+
+ {#if errorText} + + {/if} {#if canExtend} - addStepAt(0, id)} /> +
+ addStepAt(0, id)} /> +
+
{/if} {#each steps as step, i (step.key)} - toggleStep(i)} - onremove={steps.length > 1 - ? () => removeStepAt(step.key) - : undefined} - ondragstart={(e) => { - dragFrom = i; - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/plain", String(i)); - } - }} - ondragover={(e) => { - e.preventDefault(); - if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; - }} - ondrop={(e) => onStepDrop(e, i)} - ondragend={() => (dragFrom = null)} - > - {#snippet tools()} - replaceStepTool(i, id)} - /> - {/snippet} - setStepValue(i, id, v, axis)} - onreset={() => resetStep(i)} - /> - + {@const input = i === 0 ? null : stepResults[i - 1]} + {@const out = stepResults[i]} + {#if i > 0} +
+
+
+ {/if} +
+ {@render stepCard(step, i)} +
+ {#if i === 0} + (textSource = v)} + onrendertext={run} + onupload={handleFile} + /> + {:else} + + {#if input} + + {:else} + {t("resultCard.noResult")} + {/if} + + {/if} + {#if i === steps.length - 1} + + {:else} + + {#if out} + + {:else} + {t("resultCard.noResult")} + {/if} + + {/if} +
+
{#if canExtend} - addStepAt(i + 1, id)} /> +
+ addStepAt(i + 1, id)} /> +
+
{/if} {/each} -
- -
- (format = v)} - onquality={setFormatQuality} - onupload={handleFile} - ontextsource={(textValue) => { - textSource = textValue; - }} - onrendertext={run} - oncopytext={copyText} - ondownloadtxt={downloadText} - ondownload={download} - /> -
-
+ + {/if} {:else}

{t("paramsCard.noSchema")}

@@ -599,9 +731,100 @@ .no-schema { font: var(--font-size-s) var(--font-mono); } + .aligned { + display: grid; + } + .aligned-row { + display: grid; + grid-template-columns: minmax(var(--size-workspace-min), 1fr) 2fr; + gap: var(--space-xxl); + align-items: start; + } + /* Сегменты справа образуют «одну панель»: общий фон, боковые границы, + верх у шапки и низ у последней строки. */ + .panel-segment { + background: var(--color-panel); + border-left: var(--size-border) solid var(--color-border); + border-right: var(--size-border) solid var(--color-border); + padding: var(--space-xl); + } + .head-segment { + display: flex; + align-items: center; + gap: var(--space-xl); + flex-wrap: wrap; + border-top: var(--size-border) solid var(--color-border); + border-bottom: var(--size-border) solid var(--color-border); + border-radius: var(--radius-m) var(--radius-m) 0 0; + } + .head-segment .label { + font: var(--font-size-s) var(--font-mono); + letter-spacing: var(--space-text-l); + text-transform: uppercase; + color: var(--color-main); + } + .pair-segment { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-l); + } + /* Коннектор в строках «Add step»: продолжает боковые границы панели. */ + .connect-segment { + align-self: stretch; + padding: 0; + } + .connect-segment.add-last { + border-bottom: var(--size-border) solid var(--color-border); + border-radius: 0 0 var(--radius-m) var(--radius-m); + } + /* Полоса-разделитель между группами: ячейки с фоном и боковыми границами + (как у карточки слева и панели справа), сверху — линия через всю ширину. */ + .group-divider { + position: relative; + display: grid; + grid-template-columns: minmax(var(--size-workspace-min), 1fr) 2fr; + gap: var(--space-xxl); + } + .gd-cell { + grid-column: 2; + background: var(--color-panel); + border-left: var(--size-border) solid var(--color-border); + border-right: var(--size-border) solid var(--color-border); + padding: var(--space-s) 0; + } + .group-divider::after { + content: ""; + position: absolute; + inset: 0 0 auto; + border-top: var(--size-border) solid var(--color-border); + } + .last-segment { + border-bottom: var(--size-border) solid var(--color-border); + border-radius: 0 0 var(--radius-m) var(--radius-m); + } + .align-toggle { + display: inline-flex; + align-items: center; + gap: var(--space-m); + margin-right: auto; + color: var(--color-text-muted); + font: var(--font-size-s) var(--font-mono); + cursor: pointer; + } + .error { + margin: 0; + color: var(--color-danger); + font: var(--font-size-s) var(--font-mono); + } + .empty { + font: var(--font-size-s) var(--font-mono); + } @media (--bp-tablet) { .workspace { grid-template-columns: 1fr; } + .aligned-row { + grid-template-columns: 1fr; + } } diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index e07cda7..f81acbb 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -52,6 +52,8 @@ export const en: Dict = { changeTool: "Change tool", collapseStep: "Collapse step", expandStep: "Expand step", + stepResult: "Result {n}", + alignToggle: "Align tools and preview", inputLegend: "Input", resultLegend: "Result", paramsLegend: "Parameters", diff --git a/web/src/lib/i18n/ru.ts b/web/src/lib/i18n/ru.ts index a292b27..2efbc9d 100644 --- a/web/src/lib/i18n/ru.ts +++ b/web/src/lib/i18n/ru.ts @@ -53,6 +53,8 @@ export const ru: Dict = { changeTool: "Поменять инструмент", collapseStep: "Свернуть шаг", expandStep: "Развернуть шаг", + stepResult: "Результат {n}", + alignToggle: "Выровнять инструменты и просмотр", inputLegend: "Вход", resultLegend: "Результат", paramsLegend: "Параметры",