Fix/averaging unique ti3 #118
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"version": "0.1.4",
|
"version": "0.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"version": "0.1.4",
|
"version": "0.3.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2"
|
"@tauri-apps/cli": "^2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iccery"
|
name = "iccery"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||||
authors = ["Gordon"]
|
authors = ["Gordon"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
+112
-1
@@ -507,6 +507,68 @@ pub async fn run_average(
|
|||||||
state.spawn(app, id, binary, args, cwd).await
|
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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct ColprofConfig {
|
pub struct ColprofConfig {
|
||||||
pub algorithm: String,
|
pub algorithm: String,
|
||||||
@@ -824,6 +886,55 @@ mod tests {
|
|||||||
assert_eq!(args, vec!["-v", "pass1.ti3", "pass2.ti3", "avg.ti3"]);
|
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]
|
#[test]
|
||||||
fn test_build_colprof_args() {
|
fn test_build_colprof_args() {
|
||||||
let config = ColprofConfig {
|
let config = ColprofConfig {
|
||||||
@@ -865,4 +976,4 @@ mod tests {
|
|||||||
let candidates_exe = get_binary_candidates("targen.exe");
|
let candidates_exe = get_binary_candidates("targen.exe");
|
||||||
assert_eq!(candidates_exe, vec!["targen.exe"]);
|
assert_eq!(candidates_exe, vec!["targen.exe"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub fn run() {
|
|||||||
commands::read_file_base64,
|
commands::read_file_base64,
|
||||||
commands::read_tiff_preview_png,
|
commands::read_tiff_preview_png,
|
||||||
commands::run_chartread,
|
commands::run_chartread,
|
||||||
|
commands::snapshot_ti3,
|
||||||
|
commands::promote_ti3,
|
||||||
commands::run_average,
|
commands::run_average,
|
||||||
commands::run_colprof,
|
commands::run_colprof,
|
||||||
commands::run_profcheck,
|
commands::run_profcheck,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ICCery",
|
"productName": "ICCery",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"identifier": "com.gronod.iccery",
|
"identifier": "com.gronod.iccery",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|||||||
+4
-4
@@ -50,7 +50,7 @@
|
|||||||
<button class="step active" data-step="1">1. Generate Target</button>
|
<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="2">2. Print Layout</button>
|
||||||
<button class="step" data-step="3">3. Measure Target</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>
|
<button class="step" data-step="5">5. Verify</button>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -311,11 +311,11 @@
|
|||||||
<h3 style="margin: 0; font-size: 1rem;">Multi-Pass Target Averaging</h3>
|
<h3 style="margin: 0; font-size: 1rem;">Multi-Pass Target Averaging</h3>
|
||||||
<span id="passCounterBadge" class="status-badge badge-idle">Pass 1 Complete</span>
|
<span id="passCounterBadge" class="status-badge badge-idle">Pass 1 Complete</span>
|
||||||
</div>
|
</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 id="passesList" class="passes-list" style="display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px;"></div>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button type="button" class="secondary" id="btnMeasureAnotherSheet">➕ Measure Another Sheet</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -564,4 +564,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+148
-56
@@ -29,6 +29,9 @@ const STATE = {
|
|||||||
|
|
||||||
let currentState = STATE.IDLE;
|
let currentState = STATE.IDLE;
|
||||||
let currentProcessId = "";
|
let currentProcessId = "";
|
||||||
|
let measurementInProgress = false;
|
||||||
|
let currentPassIndex = 0;
|
||||||
|
const recordedPasses = [];
|
||||||
|
|
||||||
export function initChartread() {
|
export function initChartread() {
|
||||||
const btnStartRead = document.getElementById("btnStartRead");
|
const btnStartRead = document.getElementById("btnStartRead");
|
||||||
@@ -42,6 +45,47 @@ export function initChartread() {
|
|||||||
const stateLabel = document.getElementById("chartreadState");
|
const stateLabel = document.getElementById("chartreadState");
|
||||||
const logContainer = document.getElementById("chartreadLogContainer");
|
const logContainer = document.getElementById("chartreadLogContainer");
|
||||||
const logPre = document.getElementById("chartreadLog");
|
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
|
// Instrument detection logic
|
||||||
if (btnDetectInstruments && instrumentSelect) {
|
if (btnDetectInstruments && instrumentSelect) {
|
||||||
@@ -133,7 +177,10 @@ export function initChartread() {
|
|||||||
btnCancel.classList.remove("hidden");
|
btnCancel.classList.remove("hidden");
|
||||||
break;
|
break;
|
||||||
case STATE.FINISHED:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +192,11 @@ export function initChartread() {
|
|||||||
// Start reading button
|
// Start reading button
|
||||||
if (btnStartRead) {
|
if (btnStartRead) {
|
||||||
btnStartRead.addEventListener("click", async () => {
|
btnStartRead.addEventListener("click", async () => {
|
||||||
|
if (measurementInProgress) {
|
||||||
|
setPrompt("A measurement is already running. Wait for it to finish.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const basename = stage2Basename || wizardState.basename;
|
const basename = stage2Basename || wizardState.basename;
|
||||||
const cwd = stage2Cwd || wizardState.cwd;
|
const cwd = stage2Cwd || wizardState.cwd;
|
||||||
|
|
||||||
@@ -159,6 +211,7 @@ export function initChartread() {
|
|||||||
logPre.textContent = "";
|
logPre.textContent = "";
|
||||||
logContainer.open = false;
|
logContainer.open = false;
|
||||||
logContainer.classList.remove("hidden");
|
logContainer.classList.remove("hidden");
|
||||||
|
setMeasurementBusy(true);
|
||||||
setState(STATE.CALIBRATING);
|
setState(STATE.CALIBRATING);
|
||||||
setPrompt("Starting chartread... waiting for instrument calibration prompt.");
|
setPrompt("Starting chartread... waiting for instrument calibration prompt.");
|
||||||
|
|
||||||
@@ -219,29 +272,41 @@ export function initChartread() {
|
|||||||
stopSwatchListener();
|
stopSwatchListener();
|
||||||
|
|
||||||
if (event.payload.code === 0) {
|
if (event.payload.code === 0) {
|
||||||
setState(STATE.FINISHED);
|
try {
|
||||||
setPrompt("✅ Sheet measurement completed!");
|
const passIndex = currentPassIndex + 1;
|
||||||
logPre.textContent += "\n[SUCCESS] chartread sheet measurement completed.\n";
|
const filename = await invoke("snapshot_ti3", {
|
||||||
|
cwd: cwd,
|
||||||
|
basename: basename,
|
||||||
|
pass_index: passIndex,
|
||||||
|
});
|
||||||
|
|
||||||
// Track pass in session
|
currentPassIndex = passIndex;
|
||||||
currentPassIndex++;
|
recordedPasses.push({
|
||||||
recordedPasses.push({
|
index: currentPassIndex,
|
||||||
index: currentPassIndex,
|
filename: filename,
|
||||||
filename: `${basename}.ti3`,
|
time: new Date().toLocaleTimeString(),
|
||||||
time: new Date().toLocaleTimeString(),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
renderPassesList();
|
setState(STATE.FINISHED);
|
||||||
if (averagingPanel) averagingPanel.classList.remove("hidden");
|
setPrompt(`Sheet pass #${currentPassIndex} saved as ${filename}. Stage 4 stays locked until you finish.`);
|
||||||
if (passCounterBadge) passCounterBadge.textContent = `${recordedPasses.length} Pass(es) Recorded`;
|
logPre.textContent += `\n[SUCCESS] chartread completed. Snapshotted ${filename} (canonical ${basename}.ti3 removed until Finish).\n`;
|
||||||
|
|
||||||
wizardState.setTarget(basename, cwd);
|
renderPassesList();
|
||||||
setStage3Result(basename, cwd);
|
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 {
|
} else {
|
||||||
setState(STATE.FINISHED);
|
setState(STATE.FINISHED);
|
||||||
setPrompt(`❌ chartread exited with code ${event.payload.code}.`);
|
setPrompt(`❌ chartread exited with code ${event.payload.code}.`);
|
||||||
logPre.textContent += `\n[ERROR] chartread exited with code ${event.payload.code}.\n`;
|
logPre.textContent += `\n[ERROR] chartread exited with code ${event.payload.code}.\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMeasurementBusy(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
await invoke("run_chartread", { config });
|
await invoke("run_chartread", { config });
|
||||||
@@ -249,34 +314,19 @@ export function initChartread() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setPrompt(`Invoke error: ${err}`);
|
setPrompt(`Invoke error: ${err}`);
|
||||||
setState(STATE.IDLE);
|
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) {
|
if (btnMeasureAnotherSheet) {
|
||||||
btnMeasureAnotherSheet.addEventListener("click", () => {
|
btnMeasureAnotherSheet.addEventListener("click", () => {
|
||||||
|
if (measurementInProgress) {
|
||||||
|
setPrompt("Wait for the current measurement to finish and be snapshotted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(STATE.IDLE);
|
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();
|
if (btnStartRead) btnStartRead.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -286,11 +336,36 @@ export function initChartread() {
|
|||||||
const basename = stage2Basename || wizardState.basename;
|
const basename = stage2Basename || wizardState.basename;
|
||||||
const cwd = stage2Cwd || wizardState.cwd;
|
const cwd = stage2Cwd || wizardState.cwd;
|
||||||
|
|
||||||
if (recordedPasses.length <= 1) {
|
if (!basename || !cwd) {
|
||||||
setPrompt("Single pass accepted. Proceeding to Stage 4...");
|
setPrompt("Error: No working directory or basename.");
|
||||||
wizardState.setTarget(basename, cwd);
|
return;
|
||||||
setStage3Result(basename, cwd);
|
}
|
||||||
advanceToStage4();
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,21 +379,30 @@ export function initChartread() {
|
|||||||
cwd: cwd,
|
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;
|
if (event.payload.id !== `average_${basename}.ti3`) return;
|
||||||
unlistenAvgExit();
|
unlistenAvgExit();
|
||||||
btnFinishAndAverage.disabled = false;
|
|
||||||
|
|
||||||
if (event.payload.code === 0) {
|
if (event.payload.code === 0) {
|
||||||
setPrompt(`✅ Successfully averaged ${recordedPasses.length} measurement passes into ${basename}.ti3!`);
|
setPrompt(`✅ Successfully averaged ${recordedPasses.length} measurement passes into ${basename}.ti3!`);
|
||||||
wizardState.setTarget(basename, cwd);
|
logPre.textContent += `\n[SUCCESS] average wrote ${basename}.ti3\n`;
|
||||||
setStage3Result(basename, cwd);
|
acceptStage3(basename, cwd);
|
||||||
advanceToStage4();
|
|
||||||
} else {
|
} else {
|
||||||
setPrompt(`⚠️ Argyll average exited with code ${event.payload.code}. Proceeding with primary pass.`);
|
setPrompt(`⚠️ Argyll average exited with code ${event.payload.code}. Promoting pass 1 as fallback.`);
|
||||||
wizardState.setTarget(basename, cwd);
|
logPre.textContent += `\n[ERROR] average exited ${event.payload.code}. Promoting ${recordedPasses[0].filename}.\n`;
|
||||||
setStage3Result(basename, cwd);
|
try {
|
||||||
advanceToStage4();
|
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) {
|
} catch (e) {
|
||||||
console.error("run_average error:", e);
|
console.error("run_average error:", e);
|
||||||
btnFinishAndAverage.disabled = false;
|
btnFinishAndAverage.disabled = false;
|
||||||
setStage3Result(basename, cwd);
|
setPrompt(`run_average error: ${e}`);
|
||||||
advanceToStage4();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -369,9 +452,15 @@ export function initChartread() {
|
|||||||
try {
|
try {
|
||||||
await invoke("kill_process", { id: currentProcessId });
|
await invoke("kill_process", { id: currentProcessId });
|
||||||
stopSwatchListener();
|
stopSwatchListener();
|
||||||
setState(STATE.IDLE);
|
setState(recordedPasses.length > 0 ? STATE.FINISHED : STATE.IDLE);
|
||||||
setPrompt("Measurement cancelled.");
|
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');
|
const stages = document.querySelectorAll('.stage');
|
||||||
|
|
||||||
steps.forEach(s => s.classList.remove('active'));
|
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 => {
|
stages.forEach(s => {
|
||||||
s.classList.remove('active');
|
s.classList.remove('active');
|
||||||
|
|||||||
Reference in New Issue
Block a user