Reshuffle GUI workspace layout

This commit is contained in:
Meta Vens 2026-04-24 16:53:56 +01:00
commit 8de96a86d2
4 changed files with 128 additions and 176 deletions

View file

@ -49,7 +49,6 @@ type AppState = {
inputPreviewImage: ImageAsset | null;
sourceImage: ImageAsset | null;
preprocessedImage: ImageAsset | null;
edgeMapImage: ImageAsset | null;
latticeImage: ImageAsset | null;
guidanceImage: ImageAsset | null;
inference: InferencePayload | null;
@ -67,7 +66,6 @@ type AppState = {
altHeld: boolean;
zoom: number;
showGrid: boolean;
currentStage: string;
};
type ViewportAnchor = {
@ -98,10 +96,8 @@ const stepSlider = byId<HTMLInputElement>("stepSlider");
const stepValue = byId<HTMLSpanElement>("stepValue");
const lossCanvas = byId<HTMLCanvasElement>("lossCanvas");
const leftCanvas = byId<HTMLCanvasElement>("leftCanvas");
const middleCanvas = byId<HTMLCanvasElement>("middleCanvas");
const rightCanvas = byId<HTMLCanvasElement>("rightCanvas");
const leftVizLabel = byId<HTMLDivElement>("leftVizLabel");
const middleVizLabel = byId<HTMLDivElement>("middleVizLabel");
const rightVizLabel = byId<HTMLDivElement>("rightVizLabel");
const metricsPanel = byId<HTMLDivElement>("metricsPanel");
const paintSwatch = byId<HTMLDivElement>("paintSwatch");
@ -161,7 +157,6 @@ const state: AppState = {
inputPreviewImage: null,
sourceImage: null,
preprocessedImage: null,
edgeMapImage: null,
latticeImage: null,
guidanceImage: null,
inference: null,
@ -179,7 +174,6 @@ const state: AppState = {
altHeld: false,
zoom: Number(zoomInput.value),
showGrid: gridToggle.checked,
currentStage: "Idle",
};
let currentEventSource: EventSource | null = null;
@ -249,7 +243,6 @@ function resetRunArtifacts(): void {
state.jobId = null;
state.sourceImage = null;
state.preprocessedImage = null;
state.edgeMapImage = null;
state.latticeImage = null;
state.guidanceImage = null;
state.inference = null;
@ -263,7 +256,6 @@ function resetRunArtifacts(): void {
state.eventLog = [];
state.editorBaseAsset = null;
state.editorDirty = false;
state.currentStage = "Waiting";
}
function parseOptionalInteger(input: HTMLInputElement): number | null {
@ -479,10 +471,8 @@ function renderSummary(): void {
async function renderViewer(): Promise<void> {
const frame = getSelectedFrame();
let leftAsset = state.preprocessedImage ?? state.sourceImage;
let middleAsset = state.edgeMapImage;
let rightAsset = state.finalOutputImage ?? state.cleanupImage ?? state.sourceImage;
let leftLabel = "Source";
let middleLabel = "Edge Scout";
let rightLabel = "Output";
if (state.latticeImage) {
@ -490,27 +480,26 @@ async function renderViewer(): Promise<void> {
leftLabel = "Lattice Prep";
}
if (state.guidanceImage) {
middleAsset = state.guidanceImage;
middleLabel = "Guidance";
leftAsset = state.guidanceImage;
leftLabel = "Guidance";
}
if (frame) {
leftAsset = frame.samplingOverlayImage;
middleAsset = frame.displacementImage;
rightAsset = frame.outputImage;
leftLabel = "Sampling Overlay";
middleLabel = "Displacement";
rightLabel = "Current Output";
} else if (state.cleanupImage) {
middleAsset = state.heatmapImage ?? middleAsset;
middleLabel = "Cleanup Heatmap";
if (state.heatmapImage) {
leftAsset = state.heatmapImage;
leftLabel = "Cleanup Heatmap";
}
rightAsset = state.cleanupImage;
rightLabel = "Cleaned Output";
}
leftVizLabel.textContent = leftLabel;
middleVizLabel.textContent = middleLabel;
rightVizLabel.textContent = rightLabel;
await Promise.all([drawAsset(leftCanvas, leftAsset), drawAsset(middleCanvas, middleAsset), drawAsset(rightCanvas, rightAsset)]);
await Promise.all([drawAsset(leftCanvas, leftAsset), drawAsset(rightCanvas, rightAsset)]);
renderMetrics();
}
@ -826,22 +815,18 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise<void
break;
case "source_loaded":
state.sourceImage = payload.sourceImage as ImageAsset;
state.currentStage = "Loaded source";
setStatus("running", "Source loaded. Survey crew is measuring the mess.");
addLog("Source", "Loaded the input image.");
break;
case "preprocess_completed":
state.preprocessedImage = payload.sourceImage as ImageAsset;
state.currentStage = "Preprocessed source";
addLog("Preprocess", "Stripped edge-connected background noise.");
break;
case "inference_candidates_ready":
state.inference = payload.inference as InferencePayload;
state.currentStage = "Scoring lattice candidates";
addLog("Inference", "Scored candidate grids and phase offsets.");
break;
case "phase_rerank_started":
state.currentStage = "Phase rerank preview";
addLog(
"Rerank",
`Running ${String(payload.previewSteps)} preview steps across ${String(payload.candidateCount)} low-confidence candidates.`,
@ -849,18 +834,14 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise<void
break;
case "phase_selection_completed":
state.inference = payload.inference as InferencePayload;
state.currentStage = "Selected lattice";
addLog("Selection", "Committed to a ruler and phase.");
break;
case "analysis_completed":
state.edgeMapImage = payload.edgeMapImage as ImageAsset;
state.currentStage = "Edge scout";
addLog("Scout", "Built the edge map that tells the solver where the floorboards creak.");
break;
case "phase_field_prepared":
state.latticeImage = payload.latticeImage as ImageAsset;
state.guidanceImage = payload.guidanceImage as ImageAsset;
state.currentStage = "Prepared phase field";
addLog("Prep", `Locked ${String(payload.targetWidth)} x ${String(payload.targetHeight)} lattice centers.`);
break;
case "phase_field_initial":
@ -874,30 +855,26 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise<void
state.frames.push(frame);
state.frames.sort((a, b) => a.step - b.step);
}
if (state.autoFollow) {
state.selectedFrameIndex = Math.max(0, state.frames.findIndex((candidate) => candidate.step === frame.step));
if (state.autoFollow) {
state.selectedFrameIndex = Math.max(0, state.frames.findIndex((candidate) => candidate.step === frame.step));
}
if (eventName === "phase_field_final") {
addLog("Solver", "Final nearest-source sample committed.");
}
break;
}
state.currentStage = frame.step === 0 ? "Initial placement" : `Solver step ${frame.step}/${frame.totalSteps}`;
if (eventName === "phase_field_final") {
addLog("Solver", "Final nearest-source sample committed.");
}
break;
}
case "cleanup_completed":
state.cleanupImage = payload.cleanedImage as ImageAsset;
state.heatmapImage = payload.heatmapImage as ImageAsset;
state.currentStage = "Cleanup";
addLog("Cleanup", "Ran the local cleanup sweep.");
break;
case "palette_completed":
state.finalOutputImage = payload.outputImage as ImageAsset;
state.currentStage = "Final output";
addLog("Output", "Final output image is ready.");
break;
case "pipeline_completed":
state.finalOutputImage = payload.outputImage as ImageAsset;
state.runSummary = (payload.runSummary as JsonRecord | null) ?? null;
state.currentStage = "Completed";
setStatus("completed", "Run finished. If the machine still did something stupid, fix it pixel by pixel.");
addLog("Done", "Full pipeline completed.");
runButton.disabled = false;

View file

@ -21,10 +21,8 @@ const stepSlider = byId("stepSlider");
const stepValue = byId("stepValue");
const lossCanvas = byId("lossCanvas");
const leftCanvas = byId("leftCanvas");
const middleCanvas = byId("middleCanvas");
const rightCanvas = byId("rightCanvas");
const leftVizLabel = byId("leftVizLabel");
const middleVizLabel = byId("middleVizLabel");
const rightVizLabel = byId("rightVizLabel");
const metricsPanel = byId("metricsPanel");
const paintSwatch = byId("paintSwatch");
@ -80,7 +78,6 @@ const state = {
inputPreviewImage: null,
sourceImage: null,
preprocessedImage: null,
edgeMapImage: null,
latticeImage: null,
guidanceImage: null,
inference: null,
@ -98,7 +95,6 @@ const state = {
altHeld: false,
zoom: Number(zoomInput.value),
showGrid: gridToggle.checked,
currentStage: "Idle",
};
let currentEventSource = null;
let painting = false;
@ -159,7 +155,6 @@ function resetRunArtifacts() {
state.jobId = null;
state.sourceImage = null;
state.preprocessedImage = null;
state.edgeMapImage = null;
state.latticeImage = null;
state.guidanceImage = null;
state.inference = null;
@ -173,7 +168,6 @@ function resetRunArtifacts() {
state.eventLog = [];
state.editorBaseAsset = null;
state.editorDirty = false;
state.currentStage = "Waiting";
}
function parseOptionalInteger(input) {
if (input.value.trim() === "") {
@ -377,37 +371,34 @@ function renderSummary() {
async function renderViewer() {
const frame = getSelectedFrame();
let leftAsset = state.preprocessedImage ?? state.sourceImage;
let middleAsset = state.edgeMapImage;
let rightAsset = state.finalOutputImage ?? state.cleanupImage ?? state.sourceImage;
let leftLabel = "Source";
let middleLabel = "Edge Scout";
let rightLabel = "Output";
if (state.latticeImage) {
leftAsset = state.latticeImage;
leftLabel = "Lattice Prep";
}
if (state.guidanceImage) {
middleAsset = state.guidanceImage;
middleLabel = "Guidance";
leftAsset = state.guidanceImage;
leftLabel = "Guidance";
}
if (frame) {
leftAsset = frame.samplingOverlayImage;
middleAsset = frame.displacementImage;
rightAsset = frame.outputImage;
leftLabel = "Sampling Overlay";
middleLabel = "Displacement";
rightLabel = "Current Output";
}
else if (state.cleanupImage) {
middleAsset = state.heatmapImage ?? middleAsset;
middleLabel = "Cleanup Heatmap";
if (state.heatmapImage) {
leftAsset = state.heatmapImage;
leftLabel = "Cleanup Heatmap";
}
rightAsset = state.cleanupImage;
rightLabel = "Cleaned Output";
}
leftVizLabel.textContent = leftLabel;
middleVizLabel.textContent = middleLabel;
rightVizLabel.textContent = rightLabel;
await Promise.all([drawAsset(leftCanvas, leftAsset), drawAsset(middleCanvas, middleAsset), drawAsset(rightCanvas, rightAsset)]);
await Promise.all([drawAsset(leftCanvas, leftAsset), drawAsset(rightCanvas, rightAsset)]);
renderMetrics();
}
function renderSlider() {
@ -704,38 +695,30 @@ async function handleEvent(eventName, payload) {
break;
case "source_loaded":
state.sourceImage = payload.sourceImage;
state.currentStage = "Loaded source";
setStatus("running", "Source loaded. Survey crew is measuring the mess.");
addLog("Source", "Loaded the input image.");
break;
case "preprocess_completed":
state.preprocessedImage = payload.sourceImage;
state.currentStage = "Preprocessed source";
addLog("Preprocess", "Stripped edge-connected background noise.");
break;
case "inference_candidates_ready":
state.inference = payload.inference;
state.currentStage = "Scoring lattice candidates";
addLog("Inference", "Scored candidate grids and phase offsets.");
break;
case "phase_rerank_started":
state.currentStage = "Phase rerank preview";
addLog("Rerank", `Running ${String(payload.previewSteps)} preview steps across ${String(payload.candidateCount)} low-confidence candidates.`);
break;
case "phase_selection_completed":
state.inference = payload.inference;
state.currentStage = "Selected lattice";
addLog("Selection", "Committed to a ruler and phase.");
break;
case "analysis_completed":
state.edgeMapImage = payload.edgeMapImage;
state.currentStage = "Edge scout";
addLog("Scout", "Built the edge map that tells the solver where the floorboards creak.");
break;
case "phase_field_prepared":
state.latticeImage = payload.latticeImage;
state.guidanceImage = payload.guidanceImage;
state.currentStage = "Prepared phase field";
addLog("Prep", `Locked ${String(payload.targetWidth)} x ${String(payload.targetHeight)} lattice centers.`);
break;
case "phase_field_initial":
@ -753,7 +736,6 @@ async function handleEvent(eventName, payload) {
if (state.autoFollow) {
state.selectedFrameIndex = Math.max(0, state.frames.findIndex((candidate) => candidate.step === frame.step));
}
state.currentStage = frame.step === 0 ? "Initial placement" : `Solver step ${frame.step}/${frame.totalSteps}`;
if (eventName === "phase_field_final") {
addLog("Solver", "Final nearest-source sample committed.");
}
@ -762,18 +744,15 @@ async function handleEvent(eventName, payload) {
case "cleanup_completed":
state.cleanupImage = payload.cleanedImage;
state.heatmapImage = payload.heatmapImage;
state.currentStage = "Cleanup";
addLog("Cleanup", "Ran the local cleanup sweep.");
break;
case "palette_completed":
state.finalOutputImage = payload.outputImage;
state.currentStage = "Final output";
addLog("Output", "Final output image is ready.");
break;
case "pipeline_completed":
state.finalOutputImage = payload.outputImage;
state.runSummary = payload.runSummary ?? null;
state.currentStage = "Completed";
setStatus("completed", "Run finished. If the machine still did something stupid, fix it pixel by pixel.");
addLog("Done", "Full pipeline completed.");
runButton.disabled = false;

View file

@ -8,22 +8,114 @@
</head>
<body>
<div class="shell app-shell">
<header class="app-shell-header">
<div class="brand">
<div class="brand-lockup">
<div class="brand-emblem" aria-hidden="true">
<span class="brand-emblem-core"></span>
</div>
<div class="copy-stack brand-copy">
<p class="eyebrow">Live Pipeline</p>
<h1>Repixelizer</h1>
<p class="copy-tag brand-tag">Take fake pixel art, force it onto a real lattice, then inspect the crime scene frame by frame.</p>
</div>
</div>
</div>
</header>
<div class="app-grid">
<main class="workspace page-content">
<header class="app-shell-header">
<div class="brand">
<div class="brand-lockup">
<div class="brand-emblem" aria-hidden="true">
<span class="brand-emblem-core"></span>
</div>
<div class="copy-stack brand-copy">
<p class="eyebrow">Live Pipeline</p>
<h1>Repixelizer</h1>
<p class="copy-tag brand-tag">Take fake pixel art, force it onto a real lattice, then inspect the crime scene frame by frame.</p>
</div>
</div>
</div>
</header>
<section class="hero viewer-panel">
<div class="card-head viewer-head">
<div class="copy-stack card-copy viewer-heading">
<h2 id="viewerTitle">Pipeline Stages</h2>
</div>
</div>
<div id="metricsPanel" class="metrics-panel stats-grid">
<div class="metric-chip metric-status-block">
<span id="statusBadge" class="status-badge">Idle</span>
<p id="statusText" class="status-text metrics-status-text">Waiting for input.</p>
</div>
<div class="metric-chip scrubber metric-scrubber">
<label for="stepSlider">Solver Step</label>
<input id="stepSlider" type="range" min="0" max="0" step="1" value="0" disabled />
<span id="stepValue" class="step-value">No frames yet</span>
</div>
</div>
<canvas id="lossCanvas" class="loss-canvas" width="800" height="120"></canvas>
<div class="viewer-grid viewer-grid-dual">
<figure class="viz-card">
<div class="viz-label" id="leftVizLabel">Source</div>
<canvas id="leftCanvas"></canvas>
</figure>
<figure class="viz-card">
<div class="viz-label" id="rightVizLabel">Output</div>
<canvas id="rightCanvas"></canvas>
</figure>
</div>
</section>
<section class="card editor-panel">
<div class="card-head editor-head">
<div class="copy-stack card-copy">
<p class="eyebrow">Pixel Fixes</p>
<h2>Output Editor</h2>
<p class="copy-tag">Pencil the stragglers. Wheel zooms to cursor, middle mouse pans, Alt steals color.</p>
</div>
<div class="editor-actions">
<button id="resetEditorButton" class="button button-small button-secondary" type="button">Reset Output</button>
<button id="downloadButton" class="button button-small" type="button">Export PNG</button>
</div>
</div>
<div class="editor-toolbar">
<div class="paint-block">
<div id="paintSwatch" class="paint-swatch"></div>
<div class="rgba-inputs">
<label class="field">
<span>R</span>
<input id="paintR" type="number" min="0" max="255" value="255" />
</label>
<label class="field">
<span>G</span>
<input id="paintG" type="number" min="0" max="255" value="255" />
</label>
<label class="field">
<span>B</span>
<input id="paintB" type="number" min="0" max="255" value="255" />
</label>
<label class="field">
<span>A</span>
<input id="paintA" type="number" min="0" max="255" value="255" />
</label>
</div>
</div>
<div class="zoom-block field">
<label for="zoomInput">Zoom</label>
<input id="zoomInput" type="range" min="4" max="32" step="1" value="12" />
<span id="zoomValue">12x</span>
</div>
<label class="toggle-row compact">
<input id="gridToggle" type="checkbox" checked />
<span>Grid</span>
</label>
</div>
<div class="editor-stage">
<div class="checkerboard editor-surface" id="editorSurface">
<canvas id="editorCanvas"></canvas>
<canvas id="editorGridCanvas"></canvas>
</div>
<div class="editor-meta">
<div id="editorMeta" class="info-stack">
<p class="muted">Final output lands here once the solver finishes its little pilgrimage.</p>
</div>
<div id="summaryPanel" class="summary-panel"></div>
</div>
</div>
</section>
</main>
<aside class="sidebar">
<section class="card section-block">
<div class="card-head">
@ -123,102 +215,6 @@
<div id="eventLog" class="event-log"></div>
</section>
</aside>
<main class="workspace page-content">
<section class="hero viewer-panel">
<div class="card-head viewer-head">
<div class="copy-stack card-copy viewer-heading">
<h2 id="viewerTitle">Pipeline Stages</h2>
</div>
</div>
<canvas id="lossCanvas" class="loss-canvas" width="800" height="120"></canvas>
<div class="viewer-grid">
<figure class="viz-card">
<div class="viz-label" id="leftVizLabel">Source</div>
<canvas id="leftCanvas"></canvas>
</figure>
<figure class="viz-card">
<div class="viz-label" id="middleVizLabel">Scout</div>
<canvas id="middleCanvas"></canvas>
</figure>
<figure class="viz-card">
<div class="viz-label" id="rightVizLabel">Output</div>
<canvas id="rightCanvas"></canvas>
</figure>
</div>
<div id="metricsPanel" class="metrics-panel stats-grid">
<div class="metric-chip metric-status-block">
<span id="statusBadge" class="status-badge">Idle</span>
<p id="statusText" class="status-text metrics-status-text">Waiting for input.</p>
</div>
<div class="metric-chip scrubber metric-scrubber">
<label for="stepSlider">Solver Step</label>
<input id="stepSlider" type="range" min="0" max="0" step="1" value="0" disabled />
<span id="stepValue" class="step-value">No frames yet</span>
</div>
</div>
</section>
<section class="card editor-panel">
<div class="card-head editor-head">
<div class="copy-stack card-copy">
<p class="eyebrow">Pixel Fixes</p>
<h2>Output Editor</h2>
<p class="copy-tag">Pencil the stragglers. Wheel zooms to cursor, middle mouse pans, Alt steals color.</p>
</div>
<div class="editor-actions">
<button id="resetEditorButton" class="button button-small button-secondary" type="button">Reset Output</button>
<button id="downloadButton" class="button button-small" type="button">Export PNG</button>
</div>
</div>
<div class="editor-toolbar">
<div class="paint-block">
<div id="paintSwatch" class="paint-swatch"></div>
<div class="rgba-inputs">
<label class="field">
<span>R</span>
<input id="paintR" type="number" min="0" max="255" value="255" />
</label>
<label class="field">
<span>G</span>
<input id="paintG" type="number" min="0" max="255" value="255" />
</label>
<label class="field">
<span>B</span>
<input id="paintB" type="number" min="0" max="255" value="255" />
</label>
<label class="field">
<span>A</span>
<input id="paintA" type="number" min="0" max="255" value="255" />
</label>
</div>
</div>
<div class="zoom-block field">
<label for="zoomInput">Zoom</label>
<input id="zoomInput" type="range" min="4" max="32" step="1" value="12" />
<span id="zoomValue">12x</span>
</div>
<label class="toggle-row compact">
<input id="gridToggle" type="checkbox" checked />
<span>Grid</span>
</label>
</div>
<div class="editor-stage">
<div class="checkerboard editor-surface" id="editorSurface">
<canvas id="editorCanvas"></canvas>
<canvas id="editorGridCanvas"></canvas>
</div>
<div class="editor-meta">
<div id="editorMeta" class="info-stack">
<p class="muted">Final output lands here once the solver finishes its little pilgrimage.</p>
</div>
<div id="summaryPanel" class="summary-panel"></div>
</div>
</div>
</section>
</main>
</div>
</div>

View file

@ -247,7 +247,7 @@ p {
.app-grid {
display: grid;
gap: 12px;
grid-template-columns: minmax(320px, 390px) minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) minmax(320px, 390px);
align-items: start;
}
@ -569,7 +569,7 @@ p {
.viewer-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}