Replace solver scrubber with fixed loss axis
This commit is contained in:
parent
918e57ba87
commit
0417a46f22
4 changed files with 170 additions and 139 deletions
|
|
@ -94,8 +94,7 @@ type AppState = {
|
|||
heatmapImage: ImageAsset | null;
|
||||
finalOutputImage: ImageAsset | null;
|
||||
runSummary: JsonRecord | null;
|
||||
selectedFrameIndex: number;
|
||||
autoFollow: boolean;
|
||||
solverStepBudget: number;
|
||||
eventLog: LogItem[];
|
||||
paintColor: [number, number, number, number];
|
||||
editorBaseAsset: ImageAsset | null;
|
||||
|
|
@ -131,9 +130,6 @@ const statusMetrics = byId<HTMLDivElement>("statusMetrics");
|
|||
const inferenceSummary = byId<HTMLDivElement>("inferenceSummary");
|
||||
const candidateList = byId<HTMLDivElement>("candidateList");
|
||||
const eventLog = byId<HTMLDivElement>("eventLog");
|
||||
const scrubberPanel = byId<HTMLDivElement>("scrubberPanel");
|
||||
const stepSlider = byId<HTMLInputElement>("stepSlider");
|
||||
const stepValue = byId<HTMLSpanElement>("stepValue");
|
||||
const lossCanvas = byId<HTMLCanvasElement>("lossCanvas");
|
||||
const leftCanvas = byId<HTMLCanvasElement>("leftCanvas");
|
||||
const rightCanvas = byId<HTMLCanvasElement>("rightCanvas");
|
||||
|
|
@ -215,8 +211,7 @@ const state: AppState = {
|
|||
heatmapImage: null,
|
||||
finalOutputImage: null,
|
||||
runSummary: null,
|
||||
selectedFrameIndex: 0,
|
||||
autoFollow: true,
|
||||
solverStepBudget: 0,
|
||||
eventLog: [],
|
||||
paintColor: [255, 255, 255, 255],
|
||||
editorBaseAsset: null,
|
||||
|
|
@ -319,8 +314,7 @@ function resetRunArtifacts(options: { preserveSourceImage?: boolean } = {}): voi
|
|||
state.heatmapImage = null;
|
||||
state.finalOutputImage = null;
|
||||
state.runSummary = null;
|
||||
state.selectedFrameIndex = 0;
|
||||
state.autoFollow = true;
|
||||
state.solverStepBudget = 0;
|
||||
state.eventLog = [];
|
||||
state.editorBaseAsset = null;
|
||||
state.editorDirty = false;
|
||||
|
|
@ -424,7 +418,7 @@ function getSelectedFrame(): FramePayload | null {
|
|||
if (state.frames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return state.frames[Math.max(0, Math.min(state.selectedFrameIndex, state.frames.length - 1))];
|
||||
return state.frames[state.frames.length - 1];
|
||||
}
|
||||
|
||||
function renderInference(): void {
|
||||
|
|
@ -585,26 +579,6 @@ async function renderViewer(): Promise<void> {
|
|||
await Promise.all([drawAsset(leftCanvas, leftAsset), drawAsset(rightCanvas, rightAsset)]);
|
||||
}
|
||||
|
||||
function renderSlider(): void {
|
||||
scrubberPanel.hidden = state.frames.length === 0;
|
||||
if (state.frames.length === 0) {
|
||||
stepSlider.disabled = true;
|
||||
stepSlider.max = "0";
|
||||
stepSlider.value = "0";
|
||||
stepValue.textContent = "No frames yet";
|
||||
return;
|
||||
}
|
||||
stepSlider.disabled = false;
|
||||
stepSlider.max = String(state.frames.length - 1);
|
||||
stepSlider.value = String(state.selectedFrameIndex);
|
||||
const frame = getSelectedFrame();
|
||||
if (!frame) {
|
||||
stepValue.textContent = "No frames yet";
|
||||
return;
|
||||
}
|
||||
stepValue.textContent = `Frame ${frame.step} / ${frame.totalSteps}${state.autoFollow ? " • following live" : ""}`;
|
||||
}
|
||||
|
||||
function drawWrappedCanvasText(
|
||||
context: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
|
|
@ -639,6 +613,32 @@ function renderLossPlaceholder(context: CanvasRenderingContext2D, width: number,
|
|||
drawWrappedCanvasText(context, detail, { x: 18, y: 38, maxWidth: width - 36, lineHeight: 20 });
|
||||
}
|
||||
|
||||
function getLossDomainMax(): number {
|
||||
const frameDomain = state.frames.reduce((best, frame) => Math.max(best, frame.totalSteps), 0);
|
||||
return Math.max(frameDomain, state.solverStepBudget);
|
||||
}
|
||||
|
||||
function buildLossSamples(): Array<{ step: number; loss: number }> {
|
||||
return state.frames
|
||||
.filter((frame) => typeof frame.loss === "number" && Number.isFinite(frame.loss))
|
||||
.map((frame) => ({ step: frame.step, loss: frame.loss as number }))
|
||||
.sort((left, right) => left.step - right.step);
|
||||
}
|
||||
|
||||
function formatTickLabel(value: number): string {
|
||||
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
||||
}
|
||||
|
||||
function buildStepTicks(domainMax: number): number[] {
|
||||
if (domainMax <= 1) {
|
||||
return [0, domainMax];
|
||||
}
|
||||
const quarter = domainMax / 4;
|
||||
const ticks = [0, quarter, quarter * 2, quarter * 3, domainMax];
|
||||
const rounded = ticks.map((tick, index) => (index === 0 || index === ticks.length - 1 ? tick : Math.round(tick)));
|
||||
return rounded.filter((tick, index) => rounded.findIndex((candidate) => Math.abs(candidate - tick) < 1e-6) === index);
|
||||
}
|
||||
|
||||
function renderLossChart(): void {
|
||||
lossCanvas.hidden = state.stageKey !== "solver";
|
||||
if (lossCanvas.hidden) {
|
||||
|
|
@ -653,31 +653,71 @@ function renderLossChart(): void {
|
|||
context.clearRect(0, 0, width, height);
|
||||
context.fillStyle = "rgba(7, 15, 17, 0.96)";
|
||||
context.fillRect(0, 0, width, height);
|
||||
const domainMax = getLossDomainMax();
|
||||
const samples = buildLossSamples();
|
||||
const chartLeft = 56;
|
||||
const chartRight = width - 18;
|
||||
const chartTop = 16;
|
||||
const chartBottom = height - 28;
|
||||
const chartWidth = chartRight - chartLeft;
|
||||
const chartHeight = chartBottom - chartTop;
|
||||
const rawMinLoss = samples.length === 0 ? 0 : Math.min(...samples.map((sample) => sample.loss));
|
||||
const rawMaxLoss = samples.length === 0 ? 1 : Math.max(...samples.map((sample) => sample.loss));
|
||||
const domainPadding = rawMaxLoss - rawMinLoss < 1e-6 ? Math.max(0.0025, Math.abs(rawMaxLoss) * 0.05) : 0;
|
||||
const minLoss = rawMinLoss - domainPadding;
|
||||
const maxLoss = rawMaxLoss + domainPadding;
|
||||
const span = Math.max(1e-6, maxLoss - minLoss);
|
||||
const yTicks = 4;
|
||||
context.strokeStyle = "rgba(255,255,255,0.08)";
|
||||
context.lineWidth = 1;
|
||||
for (let index = 1; index <= 4; index += 1) {
|
||||
const y = (height / 5) * index;
|
||||
context.font = "12px 'VT323', monospace";
|
||||
context.fillStyle = "rgba(255,255,255,0.58)";
|
||||
context.textAlign = "right";
|
||||
context.textBaseline = "middle";
|
||||
for (let index = 0; index <= yTicks; index += 1) {
|
||||
const ratio = index / yTicks;
|
||||
const y = chartTop + ratio * chartHeight;
|
||||
context.beginPath();
|
||||
context.moveTo(0, y);
|
||||
context.lineTo(width, y);
|
||||
context.moveTo(chartLeft, y);
|
||||
context.lineTo(chartRight, y);
|
||||
context.stroke();
|
||||
const tickValue = maxLoss - ratio * span;
|
||||
context.fillText(formatNumber(tickValue, 4), chartLeft - 8, y);
|
||||
}
|
||||
const losses = state.frames
|
||||
.filter((frame) => typeof frame.loss === "number")
|
||||
.map((frame) => frame.loss as number);
|
||||
if (losses.length === 0) {
|
||||
renderLossPlaceholder(context, width, "Phase-field solve", "Loss samples land here once the solver starts moving.");
|
||||
const stepTicks = buildStepTicks(Math.max(1, domainMax));
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "top";
|
||||
for (const tick of stepTicks) {
|
||||
const x = chartLeft + (tick / Math.max(1, domainMax)) * chartWidth;
|
||||
context.beginPath();
|
||||
context.moveTo(x, chartTop);
|
||||
context.lineTo(x, chartBottom);
|
||||
context.stroke();
|
||||
context.fillText(formatTickLabel(tick), x, chartBottom + 6);
|
||||
}
|
||||
context.fillStyle = "rgba(255, 216, 74, 0.9)";
|
||||
context.font = "10px 'Press Start 2P', monospace";
|
||||
context.fillText("SOLVER STEP", chartLeft + chartWidth * 0.5, height - 14);
|
||||
context.strokeStyle = "rgba(255, 216, 74, 0.18)";
|
||||
context.lineWidth = 1.5;
|
||||
context.strokeRect(chartLeft, chartTop, chartWidth, chartHeight);
|
||||
if (samples.length === 0) {
|
||||
renderLossPlaceholder(
|
||||
context,
|
||||
width,
|
||||
"Phase-field solve",
|
||||
state.solverStepBudget <= 0
|
||||
? "Solver steps were pinned to zero, so there is no loss walk to chart."
|
||||
: "Loss samples land here once the solver starts moving.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const minLoss = Math.min(...losses);
|
||||
const maxLoss = Math.max(...losses);
|
||||
const span = Math.max(1e-6, maxLoss - minLoss);
|
||||
context.strokeStyle = "#8fe2c8";
|
||||
context.lineWidth = 3;
|
||||
context.beginPath();
|
||||
losses.forEach((loss, index) => {
|
||||
const x = losses.length === 1 ? width / 2 : (index / (losses.length - 1)) * (width - 24) + 12;
|
||||
const y = height - 18 - ((loss - minLoss) / span) * (height - 36);
|
||||
samples.forEach((sample, index) => {
|
||||
const x = chartLeft + (sample.step / Math.max(1, domainMax)) * chartWidth;
|
||||
const y = chartBottom - ((sample.loss - minLoss) / span) * chartHeight;
|
||||
if (index === 0) {
|
||||
context.moveTo(x, y);
|
||||
} else {
|
||||
|
|
@ -688,10 +728,11 @@ function renderLossChart(): void {
|
|||
|
||||
const selected = getSelectedFrame();
|
||||
if (selected && selected.loss !== null) {
|
||||
const matchingIndex = losses.findIndex((value) => Math.abs(value - selected.loss!) < 1e-6);
|
||||
const matchingIndex = samples.findIndex((sample) => sample.step === selected.step);
|
||||
if (matchingIndex >= 0) {
|
||||
const x = losses.length === 1 ? width / 2 : (matchingIndex / (losses.length - 1)) * (width - 24) + 12;
|
||||
const y = height - 18 - (((selected.loss as number) - minLoss) / span) * (height - 36);
|
||||
const sample = samples[matchingIndex];
|
||||
const x = chartLeft + (sample.step / Math.max(1, domainMax)) * chartWidth;
|
||||
const y = chartBottom - (((selected.loss as number) - minLoss) / span) * chartHeight;
|
||||
context.fillStyle = "#f5b76b";
|
||||
context.beginPath();
|
||||
context.arc(x, y, 5, 0, Math.PI * 2);
|
||||
|
|
@ -915,7 +956,6 @@ function buildFormData(): FormData {
|
|||
|
||||
async function renderEverything(): Promise<void> {
|
||||
renderInference();
|
||||
renderSlider();
|
||||
renderStatusMetrics();
|
||||
renderLossChart();
|
||||
renderSummary();
|
||||
|
|
@ -1113,9 +1153,6 @@ 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));
|
||||
}
|
||||
setStage(
|
||||
"solver",
|
||||
"Phase-field solve",
|
||||
|
|
@ -1183,6 +1220,7 @@ async function startRun(): Promise<void> {
|
|||
resetRunArtifacts({ preserveSourceImage: true });
|
||||
renderEventLog();
|
||||
runButton.disabled = true;
|
||||
state.solverStepBudget = parseOptionalInteger(stepsInput) ?? 48;
|
||||
setJobState("queued");
|
||||
setStage("queued", "Queued", "Starting the pipeline. This should wake up almost immediately.");
|
||||
addLog("Queued", `Starting ${state.file.name}.`);
|
||||
|
|
@ -1257,12 +1295,6 @@ runButton.addEventListener("click", () => {
|
|||
void startRun();
|
||||
});
|
||||
|
||||
stepSlider.addEventListener("input", () => {
|
||||
state.selectedFrameIndex = Number(stepSlider.value);
|
||||
state.autoFollow = state.selectedFrameIndex >= state.frames.length - 1;
|
||||
void renderEverything();
|
||||
});
|
||||
|
||||
zoomInput.addEventListener("input", () => {
|
||||
setZoom(Number(zoomInput.value));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ const statusMetrics = byId("statusMetrics");
|
|||
const inferenceSummary = byId("inferenceSummary");
|
||||
const candidateList = byId("candidateList");
|
||||
const eventLog = byId("eventLog");
|
||||
const scrubberPanel = byId("scrubberPanel");
|
||||
const stepSlider = byId("stepSlider");
|
||||
const stepValue = byId("stepValue");
|
||||
const lossCanvas = byId("lossCanvas");
|
||||
const leftCanvas = byId("leftCanvas");
|
||||
const rightCanvas = byId("rightCanvas");
|
||||
|
|
@ -99,8 +96,7 @@ const state = {
|
|||
heatmapImage: null,
|
||||
finalOutputImage: null,
|
||||
runSummary: null,
|
||||
selectedFrameIndex: 0,
|
||||
autoFollow: true,
|
||||
solverStepBudget: 0,
|
||||
eventLog: [],
|
||||
paintColor: [255, 255, 255, 255],
|
||||
editorBaseAsset: null,
|
||||
|
|
@ -192,8 +188,7 @@ function resetRunArtifacts(options = {}) {
|
|||
state.heatmapImage = null;
|
||||
state.finalOutputImage = null;
|
||||
state.runSummary = null;
|
||||
state.selectedFrameIndex = 0;
|
||||
state.autoFollow = true;
|
||||
state.solverStepBudget = 0;
|
||||
state.eventLog = [];
|
||||
state.editorBaseAsset = null;
|
||||
state.editorDirty = false;
|
||||
|
|
@ -289,7 +284,7 @@ function getSelectedFrame() {
|
|||
if (state.frames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return state.frames[Math.max(0, Math.min(state.selectedFrameIndex, state.frames.length - 1))];
|
||||
return state.frames[state.frames.length - 1];
|
||||
}
|
||||
function renderInference() {
|
||||
if (!state.inference) {
|
||||
|
|
@ -447,25 +442,6 @@ async function renderViewer() {
|
|||
rightVizLabel.textContent = rightLabel;
|
||||
await Promise.all([drawAsset(leftCanvas, leftAsset), drawAsset(rightCanvas, rightAsset)]);
|
||||
}
|
||||
function renderSlider() {
|
||||
scrubberPanel.hidden = state.frames.length === 0;
|
||||
if (state.frames.length === 0) {
|
||||
stepSlider.disabled = true;
|
||||
stepSlider.max = "0";
|
||||
stepSlider.value = "0";
|
||||
stepValue.textContent = "No frames yet";
|
||||
return;
|
||||
}
|
||||
stepSlider.disabled = false;
|
||||
stepSlider.max = String(state.frames.length - 1);
|
||||
stepSlider.value = String(state.selectedFrameIndex);
|
||||
const frame = getSelectedFrame();
|
||||
if (!frame) {
|
||||
stepValue.textContent = "No frames yet";
|
||||
return;
|
||||
}
|
||||
stepValue.textContent = `Frame ${frame.step} / ${frame.totalSteps}${state.autoFollow ? " • following live" : ""}`;
|
||||
}
|
||||
function drawWrappedCanvasText(context, text, options) {
|
||||
const { x, y, maxWidth, lineHeight } = options;
|
||||
const words = text.split(/\s+/);
|
||||
|
|
@ -494,6 +470,28 @@ function renderLossPlaceholder(context, width, heading, detail) {
|
|||
context.font = "18px 'VT323', monospace";
|
||||
drawWrappedCanvasText(context, detail, { x: 18, y: 38, maxWidth: width - 36, lineHeight: 20 });
|
||||
}
|
||||
function getLossDomainMax() {
|
||||
const frameDomain = state.frames.reduce((best, frame) => Math.max(best, frame.totalSteps), 0);
|
||||
return Math.max(frameDomain, state.solverStepBudget);
|
||||
}
|
||||
function buildLossSamples() {
|
||||
return state.frames
|
||||
.filter((frame) => typeof frame.loss === "number" && Number.isFinite(frame.loss))
|
||||
.map((frame) => ({ step: frame.step, loss: frame.loss }))
|
||||
.sort((left, right) => left.step - right.step);
|
||||
}
|
||||
function formatTickLabel(value) {
|
||||
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
||||
}
|
||||
function buildStepTicks(domainMax) {
|
||||
if (domainMax <= 1) {
|
||||
return [0, domainMax];
|
||||
}
|
||||
const quarter = domainMax / 4;
|
||||
const ticks = [0, quarter, quarter * 2, quarter * 3, domainMax];
|
||||
const rounded = ticks.map((tick, index) => (index === 0 || index === ticks.length - 1 ? tick : Math.round(tick)));
|
||||
return rounded.filter((tick, index) => rounded.findIndex((candidate) => Math.abs(candidate - tick) < 1e-6) === index);
|
||||
}
|
||||
function renderLossChart() {
|
||||
lossCanvas.hidden = state.stageKey !== "solver";
|
||||
if (lossCanvas.hidden) {
|
||||
|
|
@ -508,31 +506,66 @@ function renderLossChart() {
|
|||
context.clearRect(0, 0, width, height);
|
||||
context.fillStyle = "rgba(7, 15, 17, 0.96)";
|
||||
context.fillRect(0, 0, width, height);
|
||||
const domainMax = getLossDomainMax();
|
||||
const samples = buildLossSamples();
|
||||
const chartLeft = 56;
|
||||
const chartRight = width - 18;
|
||||
const chartTop = 16;
|
||||
const chartBottom = height - 28;
|
||||
const chartWidth = chartRight - chartLeft;
|
||||
const chartHeight = chartBottom - chartTop;
|
||||
const rawMinLoss = samples.length === 0 ? 0 : Math.min(...samples.map((sample) => sample.loss));
|
||||
const rawMaxLoss = samples.length === 0 ? 1 : Math.max(...samples.map((sample) => sample.loss));
|
||||
const domainPadding = rawMaxLoss - rawMinLoss < 1e-6 ? Math.max(0.0025, Math.abs(rawMaxLoss) * 0.05) : 0;
|
||||
const minLoss = rawMinLoss - domainPadding;
|
||||
const maxLoss = rawMaxLoss + domainPadding;
|
||||
const span = Math.max(1e-6, maxLoss - minLoss);
|
||||
const yTicks = 4;
|
||||
context.strokeStyle = "rgba(255,255,255,0.08)";
|
||||
context.lineWidth = 1;
|
||||
for (let index = 1; index <= 4; index += 1) {
|
||||
const y = (height / 5) * index;
|
||||
context.font = "12px 'VT323', monospace";
|
||||
context.fillStyle = "rgba(255,255,255,0.58)";
|
||||
context.textAlign = "right";
|
||||
context.textBaseline = "middle";
|
||||
for (let index = 0; index <= yTicks; index += 1) {
|
||||
const ratio = index / yTicks;
|
||||
const y = chartTop + ratio * chartHeight;
|
||||
context.beginPath();
|
||||
context.moveTo(0, y);
|
||||
context.lineTo(width, y);
|
||||
context.moveTo(chartLeft, y);
|
||||
context.lineTo(chartRight, y);
|
||||
context.stroke();
|
||||
const tickValue = maxLoss - ratio * span;
|
||||
context.fillText(formatNumber(tickValue, 4), chartLeft - 8, y);
|
||||
}
|
||||
const losses = state.frames
|
||||
.filter((frame) => typeof frame.loss === "number")
|
||||
.map((frame) => frame.loss);
|
||||
if (losses.length === 0) {
|
||||
renderLossPlaceholder(context, width, "Phase-field solve", "Loss samples land here once the solver starts moving.");
|
||||
const stepTicks = buildStepTicks(Math.max(1, domainMax));
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "top";
|
||||
for (const tick of stepTicks) {
|
||||
const x = chartLeft + (tick / Math.max(1, domainMax)) * chartWidth;
|
||||
context.beginPath();
|
||||
context.moveTo(x, chartTop);
|
||||
context.lineTo(x, chartBottom);
|
||||
context.stroke();
|
||||
context.fillText(formatTickLabel(tick), x, chartBottom + 6);
|
||||
}
|
||||
context.fillStyle = "rgba(255, 216, 74, 0.9)";
|
||||
context.font = "10px 'Press Start 2P', monospace";
|
||||
context.fillText("SOLVER STEP", chartLeft + chartWidth * 0.5, height - 14);
|
||||
context.strokeStyle = "rgba(255, 216, 74, 0.18)";
|
||||
context.lineWidth = 1.5;
|
||||
context.strokeRect(chartLeft, chartTop, chartWidth, chartHeight);
|
||||
if (samples.length === 0) {
|
||||
renderLossPlaceholder(context, width, "Phase-field solve", state.solverStepBudget <= 0
|
||||
? "Solver steps were pinned to zero, so there is no loss walk to chart."
|
||||
: "Loss samples land here once the solver starts moving.");
|
||||
return;
|
||||
}
|
||||
const minLoss = Math.min(...losses);
|
||||
const maxLoss = Math.max(...losses);
|
||||
const span = Math.max(1e-6, maxLoss - minLoss);
|
||||
context.strokeStyle = "#8fe2c8";
|
||||
context.lineWidth = 3;
|
||||
context.beginPath();
|
||||
losses.forEach((loss, index) => {
|
||||
const x = losses.length === 1 ? width / 2 : (index / (losses.length - 1)) * (width - 24) + 12;
|
||||
const y = height - 18 - ((loss - minLoss) / span) * (height - 36);
|
||||
samples.forEach((sample, index) => {
|
||||
const x = chartLeft + (sample.step / Math.max(1, domainMax)) * chartWidth;
|
||||
const y = chartBottom - ((sample.loss - minLoss) / span) * chartHeight;
|
||||
if (index === 0) {
|
||||
context.moveTo(x, y);
|
||||
}
|
||||
|
|
@ -543,10 +576,11 @@ function renderLossChart() {
|
|||
context.stroke();
|
||||
const selected = getSelectedFrame();
|
||||
if (selected && selected.loss !== null) {
|
||||
const matchingIndex = losses.findIndex((value) => Math.abs(value - selected.loss) < 1e-6);
|
||||
const matchingIndex = samples.findIndex((sample) => sample.step === selected.step);
|
||||
if (matchingIndex >= 0) {
|
||||
const x = losses.length === 1 ? width / 2 : (matchingIndex / (losses.length - 1)) * (width - 24) + 12;
|
||||
const y = height - 18 - ((selected.loss - minLoss) / span) * (height - 36);
|
||||
const sample = samples[matchingIndex];
|
||||
const x = chartLeft + (sample.step / Math.max(1, domainMax)) * chartWidth;
|
||||
const y = chartBottom - ((selected.loss - minLoss) / span) * chartHeight;
|
||||
context.fillStyle = "#f5b76b";
|
||||
context.beginPath();
|
||||
context.arc(x, y, 5, 0, Math.PI * 2);
|
||||
|
|
@ -754,7 +788,6 @@ function buildFormData() {
|
|||
}
|
||||
async function renderEverything() {
|
||||
renderInference();
|
||||
renderSlider();
|
||||
renderStatusMetrics();
|
||||
renderLossChart();
|
||||
renderSummary();
|
||||
|
|
@ -917,9 +950,6 @@ async function handleEvent(eventName, payload) {
|
|||
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));
|
||||
}
|
||||
setStage("solver", "Phase-field solve", frame.totalSteps <= 0
|
||||
? "Initial placement committed. No iterative solver steps were requested."
|
||||
: `Solver step ${frame.step} / ${frame.totalSteps}. Loss and drift terms stay live in this panel while it walks.`);
|
||||
|
|
@ -981,6 +1011,7 @@ async function startRun() {
|
|||
resetRunArtifacts({ preserveSourceImage: true });
|
||||
renderEventLog();
|
||||
runButton.disabled = true;
|
||||
state.solverStepBudget = parseOptionalInteger(stepsInput) ?? 48;
|
||||
setJobState("queued");
|
||||
setStage("queued", "Queued", "Starting the pipeline. This should wake up almost immediately.");
|
||||
addLog("Queued", `Starting ${state.file.name}.`);
|
||||
|
|
@ -1052,11 +1083,6 @@ dropzone.addEventListener("drop", (event) => {
|
|||
runButton.addEventListener("click", () => {
|
||||
void startRun();
|
||||
});
|
||||
stepSlider.addEventListener("input", () => {
|
||||
state.selectedFrameIndex = Number(stepSlider.value);
|
||||
state.autoFollow = state.selectedFrameIndex >= state.frames.length - 1;
|
||||
void renderEverything();
|
||||
});
|
||||
zoomInput.addEventListener("input", () => {
|
||||
setZoom(Number(zoomInput.value));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,11 +49,6 @@
|
|||
</div>
|
||||
<div id="statusMetrics" class="status-metrics"></div>
|
||||
</div>
|
||||
<div id="scrubberPanel" class="metric-chip scrubber metric-scrubber" hidden>
|
||||
<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" hidden></canvas>
|
||||
<div class="viewer-grid viewer-grid-dual">
|
||||
|
|
|
|||
|
|
@ -460,7 +460,6 @@ p {
|
|||
}
|
||||
|
||||
.field > span,
|
||||
.scrubber label,
|
||||
.zoom-block label {
|
||||
font-size: 0.84rem;
|
||||
line-height: 1;
|
||||
|
|
@ -619,18 +618,6 @@ p {
|
|||
color: var(--ui-text-bright);
|
||||
}
|
||||
|
||||
.scrubber {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.step-value {
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.loss-canvas {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
|
|
@ -695,15 +682,6 @@ p {
|
|||
max-width: none;
|
||||
}
|
||||
|
||||
.metric-scrubber {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metric-scrubber[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-activity {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue