Fix/averaging unique ti3 #118
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "iccery",
|
||||
"version": "0.1.4",
|
||||
"version": "0.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "iccery",
|
||||
"version": "0.1.4",
|
||||
"version": "0.3.3",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "iccery",
|
||||
"private": true,
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"tauri": "tauri"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "iccery"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||
authors = ["Gordon"]
|
||||
edition = "2021"
|
||||
|
||||
+112
-1
@@ -507,6 +507,68 @@ pub async fn run_average(
|
||||
state.spawn(app, id, binary, args, cwd).await
|
||||
}
|
||||
|
||||
fn sanitize_basename(basename: &str) -> Result<String, String> {
|
||||
let name = basename.trim();
|
||||
if name.is_empty() {
|
||||
return Err("basename is empty".to_string());
|
||||
}
|
||||
if name.contains('/') || name.contains('\\') || name.contains("..") {
|
||||
return Err("basename must not contain path separators".to_string());
|
||||
}
|
||||
Ok(name.to_string())
|
||||
}
|
||||
|
||||
pub fn snapshot_ti3_filename(basename: &str, pass_index: u32) -> Result<String, String> {
|
||||
if pass_index == 0 {
|
||||
return Err("pass_index must be 1-based".to_string());
|
||||
}
|
||||
let name = sanitize_basename(basename)?;
|
||||
Ok(format!("{}_pass{}.ti3", name, pass_index))
|
||||
}
|
||||
|
||||
/// Copy `{cwd}/{basename}.ti3` to `{cwd}/{basename}_pass{N}.ti3` and remove the
|
||||
/// canonical file so Stage 4 stays locked until Finish (#109 / #110).
|
||||
#[tauri::command]
|
||||
pub fn snapshot_ti3(cwd: String, basename: String, pass_index: u32) -> Result<String, String> {
|
||||
let dest_name = snapshot_ti3_filename(&basename, pass_index)?;
|
||||
let name = sanitize_basename(&basename)?;
|
||||
let dir = std::path::Path::new(&cwd);
|
||||
if !dir.is_dir() {
|
||||
return Err(format!("working directory does not exist: {}", cwd));
|
||||
}
|
||||
let src = dir.join(format!("{}.ti3", name));
|
||||
if !src.is_file() {
|
||||
return Err(format!("measurement file not found: {}", src.display()));
|
||||
}
|
||||
let dest = dir.join(&dest_name);
|
||||
std::fs::copy(&src, &dest).map_err(|e| format!("failed to snapshot .ti3: {}", e))?;
|
||||
std::fs::remove_file(&src)
|
||||
.map_err(|e| format!("failed to remove canonical .ti3 after snapshot: {}", e))?;
|
||||
Ok(dest_name)
|
||||
}
|
||||
|
||||
/// Copy a pass file (relative name) back to `{cwd}/{basename}.ti3`.
|
||||
#[tauri::command]
|
||||
pub fn promote_ti3(cwd: String, source: String, basename: String) -> Result<(), String> {
|
||||
let name = sanitize_basename(&basename)?;
|
||||
let src_name = source.trim();
|
||||
if src_name.is_empty()
|
||||
|| src_name.contains('/')
|
||||
|| src_name.contains('\\')
|
||||
|| src_name.contains("..")
|
||||
{
|
||||
return Err("source filename is invalid".to_string());
|
||||
}
|
||||
let dir = std::path::Path::new(&cwd);
|
||||
let src = dir.join(src_name);
|
||||
if !src.is_file() {
|
||||
return Err(format!("source measurement file not found: {}", src.display()));
|
||||
}
|
||||
let dest = dir.join(format!("{}.ti3", name));
|
||||
std::fs::copy(&src, &dest).map_err(|e| format!("failed to promote .ti3: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ColprofConfig {
|
||||
pub algorithm: String,
|
||||
@@ -824,6 +886,55 @@ mod tests {
|
||||
assert_eq!(args, vec!["-v", "pass1.ti3", "pass2.ti3", "avg.ti3"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_average_args_pass_files() {
|
||||
let config = AverageConfig {
|
||||
inputs: vec!["job_pass1.ti3".to_string(), "job_pass2.ti3".to_string()],
|
||||
output: "job.ti3".to_string(),
|
||||
cwd: "/home/user".to_string(),
|
||||
};
|
||||
let args = build_average_args(&config);
|
||||
assert_eq!(args, vec!["-v", "job_pass1.ti3", "job_pass2.ti3", "job.ti3"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snapshot_ti3_filename() {
|
||||
assert_eq!(snapshot_ti3_filename("foo", 1).unwrap(), "foo_pass1.ti3");
|
||||
assert_eq!(snapshot_ti3_filename("foo", 2).unwrap(), "foo_pass2.ti3");
|
||||
assert!(snapshot_ti3_filename("foo", 0).is_err());
|
||||
assert!(snapshot_ti3_filename("../x", 1).is_err());
|
||||
assert!(snapshot_ti3_filename("a/b", 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snapshot_and_promote_ti3_roundtrip() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"iccery_ti3_{}_{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let canonical = dir.join("job.ti3");
|
||||
std::fs::write(&canonical, b"PASS1").unwrap();
|
||||
let cwd = dir.to_string_lossy().to_string();
|
||||
let name = snapshot_ti3(cwd.clone(), "job".to_string(), 1).unwrap();
|
||||
assert_eq!(name, "job_pass1.ti3");
|
||||
assert!(!canonical.exists(), "canonical .ti3 must be removed after snapshot");
|
||||
assert_eq!(std::fs::read(dir.join("job_pass1.ti3")).unwrap(), b"PASS1");
|
||||
|
||||
std::fs::write(dir.join("job_pass2.ti3"), b"PASS2").unwrap();
|
||||
let name2 = snapshot_ti3_filename("job", 2).unwrap();
|
||||
assert_eq!(name2, "job_pass2.ti3");
|
||||
assert!(dir.join("job_pass1.ti3").exists(), "pass 1 must survive pass 2");
|
||||
|
||||
promote_ti3(cwd, name, "job".to_string()).unwrap();
|
||||
assert_eq!(std::fs::read(&canonical).unwrap(), b"PASS1");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_colprof_args() {
|
||||
let config = ColprofConfig {
|
||||
@@ -865,4 +976,4 @@ mod tests {
|
||||
let candidates_exe = get_binary_candidates("targen.exe");
|
||||
assert_eq!(candidates_exe, vec!["targen.exe"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ pub fn run() {
|
||||
commands::read_file_base64,
|
||||
commands::read_tiff_preview_png,
|
||||
commands::run_chartread,
|
||||
commands::snapshot_ti3,
|
||||
commands::promote_ti3,
|
||||
commands::run_average,
|
||||
commands::run_colprof,
|
||||
commands::run_profcheck,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ICCery",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"identifier": "com.gronod.iccery",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
|
||||
+4
-4
@@ -50,7 +50,7 @@
|
||||
<button class="step active" data-step="1">1. Generate Target</button>
|
||||
<button class="step" data-step="2">2. Print Layout</button>
|
||||
<button class="step" data-step="3">3. Measure Target</button>
|
||||
<button class="step" data-step="4">4. Create Profile</button>
|
||||
<button class="step" data-step="4" title="Complete measurement (Finish) before profiling">4. Create Profile</button>
|
||||
<button class="step" data-step="5">5. Verify</button>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -311,11 +311,11 @@
|
||||
<h3 style="margin: 0; font-size: 1rem;">Multi-Pass Target Averaging</h3>
|
||||
<span id="passCounterBadge" class="status-badge badge-idle">Pass 1 Complete</span>
|
||||
</div>
|
||||
<p class="sub-label" style="margin-bottom: 12px;">You can measure additional printed copies to reduce measurement and printing noise before computing the profile.</p>
|
||||
<p class="sub-label" style="margin-bottom: 12px;">Each successful sheet is saved as a separate _passN.ti3 file. Stage 4 stays locked until you finish. Measure extra copies to reduce noise, then finish to write the canonical .ti3.</p>
|
||||
<div id="passesList" class="passes-list" style="display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px;"></div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" class="secondary" id="btnMeasureAnotherSheet">➕ Measure Another Sheet</button>
|
||||
<button type="button" class="primary" id="btnFinishAndAverage">✓ Average Passes & Proceed →</button>
|
||||
<button type="button" class="primary" id="btnFinishAndAverage" title="Complete measurement (Finish) before profiling">✓ Average Passes & Proceed →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -564,4 +564,4 @@
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+148
-56
@@ -29,6 +29,9 @@ const STATE = {
|
||||
|
||||
let currentState = STATE.IDLE;
|
||||
let currentProcessId = "";
|
||||
let measurementInProgress = false;
|
||||
let currentPassIndex = 0;
|
||||
const recordedPasses = [];
|
||||
|
||||
export function initChartread() {
|
||||
const btnStartRead = document.getElementById("btnStartRead");
|
||||
@@ -42,6 +45,47 @@ export function initChartread() {
|
||||
const stateLabel = document.getElementById("chartreadState");
|
||||
const logContainer = document.getElementById("chartreadLogContainer");
|
||||
const logPre = document.getElementById("chartreadLog");
|
||||
const averagingPanel = document.getElementById("chartreadAveragingPanel");
|
||||
const passesList = document.getElementById("passesList");
|
||||
const passCounterBadge = document.getElementById("passCounterBadge");
|
||||
const btnMeasureAnotherSheet = document.getElementById("btnMeasureAnotherSheet");
|
||||
const btnFinishAndAverage = document.getElementById("btnFinishAndAverage");
|
||||
|
||||
function setMeasurementBusy(busy) {
|
||||
measurementInProgress = busy;
|
||||
if (btnStartRead) btnStartRead.disabled = busy;
|
||||
if (btnMeasureAnotherSheet) btnMeasureAnotherSheet.disabled = busy;
|
||||
if (btnFinishAndAverage) {
|
||||
btnFinishAndAverage.disabled = busy || recordedPasses.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPassesList() {
|
||||
if (!passesList) return;
|
||||
passesList.innerHTML = "";
|
||||
recordedPasses.forEach((pass, i) => {
|
||||
const item = document.createElement("div");
|
||||
item.style.cssText = "display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.05); padding:6px 10px; border-radius:4px; font-size:0.85rem;";
|
||||
const label = document.createElement("span");
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = `Sheet Pass #${i + 1}`;
|
||||
label.appendChild(strong);
|
||||
label.appendChild(document.createTextNode(` (${pass.filename})`));
|
||||
const time = document.createElement("span");
|
||||
time.style.opacity = "0.7";
|
||||
time.textContent = `✓ ${pass.time}`;
|
||||
item.appendChild(label);
|
||||
item.appendChild(time);
|
||||
passesList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function acceptStage3(basename, cwd) {
|
||||
wizardState.setTarget(basename, cwd);
|
||||
setStage3Result(basename, cwd);
|
||||
wizardState.updateGating();
|
||||
advanceToStage4();
|
||||
}
|
||||
|
||||
// Instrument detection logic
|
||||
if (btnDetectInstruments && instrumentSelect) {
|
||||
@@ -133,7 +177,10 @@ export function initChartread() {
|
||||
btnCancel.classList.remove("hidden");
|
||||
break;
|
||||
case STATE.FINISHED:
|
||||
btnStartRead.classList.remove("hidden");
|
||||
// After a recorded pass, further sheets go through "Measure Another Sheet"
|
||||
if (recordedPasses.length === 0) {
|
||||
btnStartRead.classList.remove("hidden");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -145,6 +192,11 @@ export function initChartread() {
|
||||
// Start reading button
|
||||
if (btnStartRead) {
|
||||
btnStartRead.addEventListener("click", async () => {
|
||||
if (measurementInProgress) {
|
||||
setPrompt("A measurement is already running. Wait for it to finish.");
|
||||
return;
|
||||
}
|
||||
|
||||
const basename = stage2Basename || wizardState.basename;
|
||||
const cwd = stage2Cwd || wizardState.cwd;
|
||||
|
||||
@@ -159,6 +211,7 @@ export function initChartread() {
|
||||
logPre.textContent = "";
|
||||
logContainer.open = false;
|
||||
logContainer.classList.remove("hidden");
|
||||
setMeasurementBusy(true);
|
||||
setState(STATE.CALIBRATING);
|
||||
setPrompt("Starting chartread... waiting for instrument calibration prompt.");
|
||||
|
||||
@@ -219,29 +272,41 @@ export function initChartread() {
|
||||
stopSwatchListener();
|
||||
|
||||
if (event.payload.code === 0) {
|
||||
setState(STATE.FINISHED);
|
||||
setPrompt("✅ Sheet measurement completed!");
|
||||
logPre.textContent += "\n[SUCCESS] chartread sheet measurement completed.\n";
|
||||
try {
|
||||
const passIndex = currentPassIndex + 1;
|
||||
const filename = await invoke("snapshot_ti3", {
|
||||
cwd: cwd,
|
||||
basename: basename,
|
||||
pass_index: passIndex,
|
||||
});
|
||||
|
||||
// Track pass in session
|
||||
currentPassIndex++;
|
||||
recordedPasses.push({
|
||||
index: currentPassIndex,
|
||||
filename: `${basename}.ti3`,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
});
|
||||
currentPassIndex = passIndex;
|
||||
recordedPasses.push({
|
||||
index: currentPassIndex,
|
||||
filename: filename,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
});
|
||||
|
||||
renderPassesList();
|
||||
if (averagingPanel) averagingPanel.classList.remove("hidden");
|
||||
if (passCounterBadge) passCounterBadge.textContent = `${recordedPasses.length} Pass(es) Recorded`;
|
||||
setState(STATE.FINISHED);
|
||||
setPrompt(`Sheet pass #${currentPassIndex} saved as ${filename}. Stage 4 stays locked until you finish.`);
|
||||
logPre.textContent += `\n[SUCCESS] chartread completed. Snapshotted ${filename} (canonical ${basename}.ti3 removed until Finish).\n`;
|
||||
|
||||
wizardState.setTarget(basename, cwd);
|
||||
setStage3Result(basename, cwd);
|
||||
renderPassesList();
|
||||
if (averagingPanel) averagingPanel.classList.remove("hidden");
|
||||
if (passCounterBadge) passCounterBadge.textContent = `${recordedPasses.length} Pass(es) Recorded`;
|
||||
// Do not call setStage3Result / wizardState.setTarget here (#110)
|
||||
} catch (snapErr) {
|
||||
setState(STATE.FINISHED);
|
||||
setPrompt(`Measurement finished but pass snapshot failed: ${snapErr}`);
|
||||
logPre.textContent += `\n[ERROR] snapshot_ti3 failed: ${snapErr}\n`;
|
||||
}
|
||||
} else {
|
||||
setState(STATE.FINISHED);
|
||||
setPrompt(`❌ chartread exited with code ${event.payload.code}.`);
|
||||
logPre.textContent += `\n[ERROR] chartread exited with code ${event.payload.code}.\n`;
|
||||
}
|
||||
|
||||
setMeasurementBusy(false);
|
||||
});
|
||||
|
||||
await invoke("run_chartread", { config });
|
||||
@@ -249,34 +314,19 @@ export function initChartread() {
|
||||
} catch (err) {
|
||||
setPrompt(`Invoke error: ${err}`);
|
||||
setState(STATE.IDLE);
|
||||
setMeasurementBusy(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Multi-pass measurement controls
|
||||
const averagingPanel = document.getElementById("chartreadAveragingPanel");
|
||||
const passesList = document.getElementById("passesList");
|
||||
const passCounterBadge = document.getElementById("passCounterBadge");
|
||||
const btnMeasureAnotherSheet = document.getElementById("btnMeasureAnotherSheet");
|
||||
const btnFinishAndAverage = document.getElementById("btnFinishAndAverage");
|
||||
let currentPassIndex = 0;
|
||||
const recordedPasses = [];
|
||||
|
||||
function renderPassesList() {
|
||||
if (!passesList) return;
|
||||
passesList.innerHTML = "";
|
||||
recordedPasses.forEach((pass, i) => {
|
||||
const item = document.createElement("div");
|
||||
item.style.cssText = "display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.05); padding:6px 10px; border-radius:4px; font-size:0.85rem;";
|
||||
item.innerHTML = `<span><strong>Sheet Pass #${i + 1}</strong> (${pass.filename})</span><span style="opacity:0.7;">✓ ${pass.time}</span>`;
|
||||
passesList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnMeasureAnotherSheet) {
|
||||
btnMeasureAnotherSheet.addEventListener("click", () => {
|
||||
if (measurementInProgress) {
|
||||
setPrompt("Wait for the current measurement to finish and be snapshotted.");
|
||||
return;
|
||||
}
|
||||
setState(STATE.IDLE);
|
||||
setPrompt(`Ready to measure sheet pass #${recordedPasses.length + 1}. Press 'Start Measurement'.`);
|
||||
setPrompt(`Ready to measure sheet pass #${recordedPasses.length + 1}. Starting chartread...`);
|
||||
if (btnStartRead) btnStartRead.click();
|
||||
});
|
||||
}
|
||||
@@ -286,11 +336,36 @@ export function initChartread() {
|
||||
const basename = stage2Basename || wizardState.basename;
|
||||
const cwd = stage2Cwd || wizardState.cwd;
|
||||
|
||||
if (recordedPasses.length <= 1) {
|
||||
setPrompt("Single pass accepted. Proceeding to Stage 4...");
|
||||
wizardState.setTarget(basename, cwd);
|
||||
setStage3Result(basename, cwd);
|
||||
advanceToStage4();
|
||||
if (!basename || !cwd) {
|
||||
setPrompt("Error: No working directory or basename.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (measurementInProgress) {
|
||||
setPrompt("Wait for the current measurement to finish.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordedPasses.length === 0) {
|
||||
setPrompt("No measurement passes recorded yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordedPasses.length === 1) {
|
||||
setPrompt("Single pass accepted. Restoring canonical .ti3 and proceeding to Stage 4...");
|
||||
btnFinishAndAverage.disabled = true;
|
||||
try {
|
||||
await invoke("promote_ti3", {
|
||||
cwd: cwd,
|
||||
source: recordedPasses[0].filename,
|
||||
basename: basename,
|
||||
});
|
||||
logPre.textContent += `\n[SUCCESS] Promoted ${recordedPasses[0].filename} → ${basename}.ti3 (single pass, average not invoked).\n`;
|
||||
acceptStage3(basename, cwd);
|
||||
} catch (e) {
|
||||
btnFinishAndAverage.disabled = false;
|
||||
setPrompt(`Failed to restore ${basename}.ti3: ${e}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,21 +379,30 @@ export function initChartread() {
|
||||
cwd: cwd,
|
||||
};
|
||||
|
||||
const unlistenAvgExit = await listen("process:exit", (event) => {
|
||||
logPre.textContent += `\n[average] average -v ${averageConfig.inputs.join(" ")} ${averageConfig.output}\n`;
|
||||
|
||||
const unlistenAvgExit = await listen("process:exit", async (event) => {
|
||||
if (event.payload.id !== `average_${basename}.ti3`) return;
|
||||
unlistenAvgExit();
|
||||
btnFinishAndAverage.disabled = false;
|
||||
|
||||
if (event.payload.code === 0) {
|
||||
setPrompt(`✅ Successfully averaged ${recordedPasses.length} measurement passes into ${basename}.ti3!`);
|
||||
wizardState.setTarget(basename, cwd);
|
||||
setStage3Result(basename, cwd);
|
||||
advanceToStage4();
|
||||
logPre.textContent += `\n[SUCCESS] average wrote ${basename}.ti3\n`;
|
||||
acceptStage3(basename, cwd);
|
||||
} else {
|
||||
setPrompt(`⚠️ Argyll average exited with code ${event.payload.code}. Proceeding with primary pass.`);
|
||||
wizardState.setTarget(basename, cwd);
|
||||
setStage3Result(basename, cwd);
|
||||
advanceToStage4();
|
||||
setPrompt(`⚠️ Argyll average exited with code ${event.payload.code}. Promoting pass 1 as fallback.`);
|
||||
logPre.textContent += `\n[ERROR] average exited ${event.payload.code}. Promoting ${recordedPasses[0].filename}.\n`;
|
||||
try {
|
||||
await invoke("promote_ti3", {
|
||||
cwd: cwd,
|
||||
source: recordedPasses[0].filename,
|
||||
basename: basename,
|
||||
});
|
||||
acceptStage3(basename, cwd);
|
||||
} catch (e) {
|
||||
btnFinishAndAverage.disabled = false;
|
||||
setPrompt(`Average failed and fallback promote failed: ${e}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -326,8 +410,7 @@ export function initChartread() {
|
||||
} catch (e) {
|
||||
console.error("run_average error:", e);
|
||||
btnFinishAndAverage.disabled = false;
|
||||
setStage3Result(basename, cwd);
|
||||
advanceToStage4();
|
||||
setPrompt(`run_average error: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -369,9 +452,15 @@ export function initChartread() {
|
||||
try {
|
||||
await invoke("kill_process", { id: currentProcessId });
|
||||
stopSwatchListener();
|
||||
setState(STATE.IDLE);
|
||||
setState(recordedPasses.length > 0 ? STATE.FINISHED : STATE.IDLE);
|
||||
setPrompt("Measurement cancelled.");
|
||||
} catch (e) { console.error("kill_process error:", e); }
|
||||
} catch (e) {
|
||||
console.error("kill_process error:", e);
|
||||
setPrompt(`Cancel failed: ${e}`);
|
||||
} finally {
|
||||
setMeasurementBusy(false);
|
||||
stopSwatchListener();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,7 +474,10 @@ function advanceToStage4() {
|
||||
const stages = document.querySelectorAll('.stage');
|
||||
|
||||
steps.forEach(s => s.classList.remove('active'));
|
||||
if (steps[3]) steps[3].classList.add('active');
|
||||
if (steps[3]) {
|
||||
steps[3].classList.add('active');
|
||||
steps[3].classList.remove('disabled');
|
||||
}
|
||||
|
||||
stages.forEach(s => {
|
||||
s.classList.remove('active');
|
||||
|
||||
Reference in New Issue
Block a user