Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f5bf76594 | ||
|
|
9c7d563dbf | ||
|
|
743ad86f58 | ||
|
|
ebbdbd4f55 | ||
|
|
ef0d65453e | ||
|
|
9f2ff015d7 | ||
|
|
5a1ad654b8 | ||
|
|
ec7eb2fcb4 |
@@ -9,6 +9,8 @@ jobs:
|
|||||||
build-macos:
|
build-macos:
|
||||||
name: Build macOS (${{ matrix.platform.name }})
|
name: Build macOS (${{ matrix.platform.name }})
|
||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
|
env:
|
||||||
|
XDG_CONFIG_HOME: ${{ runner.temp }}/.config
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
@@ -17,21 +19,31 @@ jobs:
|
|||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
binary_dir: macos-x86_64
|
binary_dir: macos-x86_64
|
||||||
arch: x86_64
|
arch: x86_64
|
||||||
- name: Apple Silicon
|
# - name: Apple Silicon
|
||||||
os: macos
|
# os: macos
|
||||||
target: aarch64-apple-darwin
|
# target: aarch64-apple-darwin
|
||||||
binary_dir: macos-aarch64
|
# binary_dir: macos-aarch64
|
||||||
arch: arm64
|
# arch: arm64
|
||||||
- name: Universal (Intel + Apple Silicon)
|
# - name: Universal (Intel + Apple Silicon)
|
||||||
os: macos
|
# os: macos
|
||||||
target: universal-apple-darwin
|
# target: universal-apple-darwin
|
||||||
binary_dir: macos-universal
|
# binary_dir: macos-universal
|
||||||
arch: universal
|
# arch: universal
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Ensure workspace and git permissions
|
||||||
|
run: |
|
||||||
|
mkdir -p "${{ runner.temp }}/.config/git"
|
||||||
|
touch "${{ runner.temp }}/.config/git/ignore"
|
||||||
|
mkdir -p .git/info
|
||||||
|
touch .git/info/exclude
|
||||||
|
chmod -R u+rwX .git || true
|
||||||
|
chmod 644 .git/info/exclude || true
|
||||||
|
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -59,21 +71,6 @@ jobs:
|
|||||||
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist"
|
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist"
|
||||||
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen"
|
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen"
|
||||||
|
|
||||||
- name: Run Rust tests
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: "0"
|
|
||||||
CARGO_TARGET_DIR: "${{ runner.temp }}/cargo-target"
|
|
||||||
run: |
|
|
||||||
cd src-tauri
|
|
||||||
if [ "${{ matrix.platform.target }}" = "universal-apple-darwin" ]; then
|
|
||||||
cargo test
|
|
||||||
elif [ "${{ matrix.platform.target }}" = "aarch64-apple-darwin" ] && [ "$(uname -m)" != "arm64" ]; then
|
|
||||||
cargo test --no-run --target ${{ matrix.platform.target }}
|
|
||||||
else
|
|
||||||
cargo test --target ${{ matrix.platform.target }}
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set Release Environment
|
- name: Set Release Environment
|
||||||
id: set_env
|
id: set_env
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Ensure workspace and git permissions
|
||||||
|
run: |
|
||||||
|
mkdir -p "${{ runner.temp }}/.config/git"
|
||||||
|
touch "${{ runner.temp }}/.config/git/ignore"
|
||||||
|
mkdir -p .git/info
|
||||||
|
touch .git/info/exclude
|
||||||
|
chmod -R u+rwX .git || true
|
||||||
|
chmod 644 .git/info/exclude || true
|
||||||
|
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -163,9 +163,9 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
|
|||||||
- Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries.
|
- Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries.
|
||||||
- Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`.
|
- Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`.
|
||||||
|
|
||||||
## CI & Cross-Compilation Testing
|
## CI & Cross-Compilation
|
||||||
|
|
||||||
- macOS CI workflow lives in `.gitea/workflows/build-macos.yml`.
|
- Release packaging workflows live under `.gitea/workflows/` (`build-macos.yml`, `build-linux.yml`, `build-windows.yml`).
|
||||||
- On Intel runner hosts, cross-compiling for Apple Silicon (`aarch64-apple-darwin`) must use `cargo test --no-run --target aarch64-apple-darwin`. This validates sidecar packaging, compilation, and link correctness without attempting to execute ARM64 binaries on an Intel CPU (`Bad CPU type in executable (os error 86)`).
|
- Tag release builds focus exclusively on packaging via `npm run tauri build` without redundant debug-profile test compilations.
|
||||||
- Universal and native Intel matrix jobs run and execute full tests natively.
|
- Local/CI cross-compilation test execution for Apple Silicon (`aarch64-apple-darwin`) on Intel hosts must use `cargo test --no-run --target aarch64-apple-darwin` to avoid executing ARM64 binaries on an x86_64 CPU (`Bad CPU type in executable (os error 86)`).
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
||||||
|
|
||||||
[](https://git.i3omb.com/gronod/ICCery)
|
[](https://git.i3omb.com/gronod/ICCery)
|
||||||
[](https://git.i3omb.com/gronod/ICCery)
|
[](https://git.i3omb.com/gronod/ICCery)
|
||||||
[](https://tauri.app)
|
[](https://tauri.app)
|
||||||
[](LICENCE.md)
|
[](LICENCE.md)
|
||||||
|
|||||||
+10
-1
@@ -14,7 +14,7 @@ ICCery is a native, cross-platform desktop application built with:
|
|||||||
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
|
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
|
||||||
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
|
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
|
||||||
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
|
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
|
||||||
- **Current Version**: `v0.8.2` (Production release).
|
- **Current Version**: `v0.8.4` (Production release).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -108,6 +108,15 @@ ICCery is a native, cross-platform desktop application built with:
|
|||||||
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel.
|
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel.
|
||||||
- [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology).
|
- [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology).
|
||||||
|
|
||||||
|
### Maintenance & Reliability Release (`v0.8.3`)
|
||||||
|
- [x] **Gamut Viewer Node Test Runner Support (#212)**: Guarded `window` and `window.__TAURI__` globals in `gamut_viewer.js` and added polyfill mock harness to `gamut_viewer.test.js` to enable automated headless test execution via `node src/js/gamut_viewer.test.js`.
|
||||||
|
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
|
||||||
|
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
|
||||||
|
|
||||||
|
### Maintenance & Reliability Release (`v0.8.4`)
|
||||||
|
- [x] **Atomic Verification History Persistence (#213)**: Hardened `quality_store.rs` with atomic temporary file writes (`.tmp`), explicit flush/sync, and atomic rename to prevent historical drift data loss or corruption upon unexpected system crashes.
|
||||||
|
- [x] **Frontend Unit Testing & CI Integration (#215)**: Added standard `npm test` script executing the 3 frontend test suites (`profcheck`, `chartread`, and `gamut_viewer`) and integrated automated frontend test validation into macOS, Linux, and Windows CI workflows.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Future Roadmap
|
## 3. Future Roadmap
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.2",
|
"version": "0.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1423,7 +1423,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iccery"
|
name = "iccery"
|
||||||
version = "0.8.2"
|
version = "0.8.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
+12
-1
@@ -1,9 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iccery"
|
name = "iccery"
|
||||||
version = "0.8.2"
|
version = "0.8.4"
|
||||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||||
authors = ["Gordon"]
|
authors = ["Gordon"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
include = [
|
||||||
|
"src/**/*",
|
||||||
|
"build.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
"Cargo.lock",
|
||||||
|
"tauri.conf.json",
|
||||||
|
"capabilities/**/*",
|
||||||
|
"gen/**/*",
|
||||||
|
"icons/**/*",
|
||||||
|
"windows/**/*",
|
||||||
|
]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ fn main() {
|
|||||||
|
|
||||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
println!("cargo:rerun-if-changed=src");
|
||||||
|
println!("cargo:rerun-if-changed=tauri.conf.json");
|
||||||
|
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||||
|
println!("cargo:rerun-if-changed=argyll");
|
||||||
|
|
||||||
// Validate that ArgyllCMS sidecar binaries are staged before building
|
// Validate that ArgyllCMS sidecar binaries are staged before building
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||||
|
|||||||
@@ -391,6 +391,61 @@ pub async fn select_profile_file(
|
|||||||
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn select_spectrum_file(
|
||||||
|
app: AppHandle,
|
||||||
|
default_dir: Option<String>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
|
let mut builder = app
|
||||||
|
.dialog()
|
||||||
|
.file()
|
||||||
|
.add_filter("Argyll Spectrum File (*.sp)", &["sp"]);
|
||||||
|
if let Some(ref dir) = default_dir {
|
||||||
|
if !dir.trim().is_empty() {
|
||||||
|
builder = builder.set_directory(std::path::PathBuf::from(dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
builder.pick_file(move |file_path| {
|
||||||
|
let res = file_path.map(|p| p.to_string());
|
||||||
|
let _ = tx.send(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn select_dataset_file(
|
||||||
|
app: AppHandle,
|
||||||
|
default_dir: Option<String>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
|
let mut builder = app
|
||||||
|
.dialog()
|
||||||
|
.file()
|
||||||
|
.add_filter(
|
||||||
|
"Measurement Dataset (*.ti3, *.txt, *.cgats, *.csv)",
|
||||||
|
&["ti3", "txt", "cgats", "csv"],
|
||||||
|
);
|
||||||
|
if let Some(ref dir) = default_dir {
|
||||||
|
if !dir.trim().is_empty() {
|
||||||
|
builder = builder.set_directory(std::path::PathBuf::from(dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
builder.pick_file(move |file_path| {
|
||||||
|
let res = file_path.map(|p| p.to_string());
|
||||||
|
let _ = tx.send(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn select_target_file(
|
pub async fn select_target_file(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ pub fn run() {
|
|||||||
commands::inspect_dataset_preview,
|
commands::inspect_dataset_preview,
|
||||||
commands::select_existing_target,
|
commands::select_existing_target,
|
||||||
commands::select_profile_file,
|
commands::select_profile_file,
|
||||||
|
commands::select_spectrum_file,
|
||||||
|
commands::select_dataset_file,
|
||||||
commands::select_target_file,
|
commands::select_target_file,
|
||||||
commands::select_directory,
|
commands::select_directory,
|
||||||
commands::send_stdin,
|
commands::send_stdin,
|
||||||
|
|||||||
@@ -100,15 +100,21 @@ pub fn load_history_file(path: &Path) -> Vec<VerificationRecord> {
|
|||||||
}
|
}
|
||||||
let content = match fs::read_to_string(path) {
|
let content = match fs::read_to_string(path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return Vec::new(),
|
Err(e) => {
|
||||||
|
log::warn!("Failed to read verification history at {}: {}", path.display(), e);
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match serde_json::from_str::<VerificationHistoryStore>(&content) {
|
match serde_json::from_str::<VerificationHistoryStore>(&content) {
|
||||||
Ok(store) => store.records,
|
Ok(store) => store.records,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
log::error!("Failed to parse verification history at {}: {}", path.display(), e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes verification records to the specified JSON path.
|
/// Writes verification records to the specified JSON path atomically.
|
||||||
/// Automatically creates parent directories if needed.
|
/// Automatically creates parent directories if needed.
|
||||||
/// Evicts oldest records by timestamp if count exceeds HISTORY_CAP (1,000).
|
/// Evicts oldest records by timestamp if count exceeds HISTORY_CAP (1,000).
|
||||||
pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result<(), String> {
|
pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result<(), String> {
|
||||||
@@ -132,7 +138,33 @@ pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result
|
|||||||
|
|
||||||
let json = serde_json::to_string_pretty(&store)
|
let json = serde_json::to_string_pretty(&store)
|
||||||
.map_err(|e| format!("Failed to serialize verification history: {}", e))?;
|
.map_err(|e| format!("Failed to serialize verification history: {}", e))?;
|
||||||
fs::write(path, json).map_err(|e| format!("Failed to write verification history: {}", e))?;
|
|
||||||
|
let mut tmp_path = path.as_os_str().to_os_string();
|
||||||
|
tmp_path.push(".tmp");
|
||||||
|
let tmp_path = PathBuf::from(tmp_path);
|
||||||
|
|
||||||
|
// Write to temporary file with explicit flush and sync
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
let mut file = fs::File::create(&tmp_path)
|
||||||
|
.map_err(|e| format!("Failed to create temp history file: {}", e))?;
|
||||||
|
file.write_all(json.as_bytes())
|
||||||
|
.map_err(|e| {
|
||||||
|
let _ = fs::remove_file(&tmp_path);
|
||||||
|
format!("Failed to write temp history file: {}", e)
|
||||||
|
})?;
|
||||||
|
file.sync_all()
|
||||||
|
.map_err(|e| {
|
||||||
|
let _ = fs::remove_file(&tmp_path);
|
||||||
|
format!("Failed to sync temp history file: {}", e)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically replace destination file
|
||||||
|
if let Err(e) = fs::rename(&tmp_path, path) {
|
||||||
|
let _ = fs::remove_file(&tmp_path);
|
||||||
|
return Err(format!("Failed to atomically replace verification history file: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -418,4 +450,38 @@ mod tests {
|
|||||||
assert_eq!(p2.extension().unwrap(), "csv");
|
assert_eq!(p2.extension().unwrap(), "csv");
|
||||||
assert_eq!(p2.to_string_lossy(), "/tmp/history.csv");
|
assert_eq!(p2.to_string_lossy(), "/tmp/history.csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_atomic_write_preserves_data_and_cleans_tmp() {
|
||||||
|
let temp_dir = std::env::temp_dir().join("iccery_test_atomic_write");
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
fs::create_dir_all(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let file_path = temp_dir.join("verification_history.json");
|
||||||
|
let mut tmp_file_path = file_path.as_os_str().to_os_string();
|
||||||
|
tmp_file_path.push(".tmp");
|
||||||
|
let tmp_file_path = std::path::PathBuf::from(tmp_file_path);
|
||||||
|
|
||||||
|
let rec = sample_record("vr-atom-1", "2026-09-06T12:00:00Z", 0.5);
|
||||||
|
write_history_file(&file_path, &[rec.clone()]).unwrap();
|
||||||
|
|
||||||
|
assert!(file_path.exists(), "Target file must exist");
|
||||||
|
assert!(!tmp_file_path.exists(), "Temporary file must not remain after successful atomic write");
|
||||||
|
|
||||||
|
let loaded = load_history_file(&file_path);
|
||||||
|
assert_eq!(loaded.len(), 1);
|
||||||
|
assert_eq!(loaded[0], rec);
|
||||||
|
|
||||||
|
// Overwrite with updated records to verify atomic replacement
|
||||||
|
let rec2 = sample_record("vr-atom-2", "2026-09-06T12:05:00Z", 1.2);
|
||||||
|
write_history_file(&file_path, &[rec.clone(), rec2.clone()]).unwrap();
|
||||||
|
|
||||||
|
assert!(!tmp_file_path.exists(), "Temporary file must not remain after overwrite");
|
||||||
|
let loaded2 = load_history_file(&file_path);
|
||||||
|
assert_eq!(loaded2.len(), 2);
|
||||||
|
assert_eq!(loaded2[0], rec);
|
||||||
|
assert_eq!(loaded2[1], rec2);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ICCery",
|
"productName": "ICCery",
|
||||||
"version": "0.8.2",
|
"version": "0.8.4",
|
||||||
"identifier": "com.gronod.iccery",
|
"identifier": "com.gronod.iccery",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|||||||
+40
-11
@@ -1,4 +1,6 @@
|
|||||||
const { invoke } = window.__TAURI__.core;
|
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke
|
||||||
|
? window.__TAURI__.core.invoke
|
||||||
|
: async () => {};
|
||||||
|
|
||||||
export class CgatsInterop {
|
export class CgatsInterop {
|
||||||
constructor(appState) {
|
constructor(appState) {
|
||||||
@@ -22,26 +24,53 @@ export class CgatsInterop {
|
|||||||
|
|
||||||
async handleImport() {
|
async handleImport() {
|
||||||
try {
|
try {
|
||||||
const filePath = await invoke('select_target_file');
|
const filePath = await invoke('select_dataset_file', {
|
||||||
|
defaultDir: this.appState?.cwd || null,
|
||||||
|
});
|
||||||
if (!filePath) return; // User cancelled
|
if (!filePath) return; // User cancelled
|
||||||
|
|
||||||
// Inspect first to show modal (optional, skipping for now, directly import)
|
const isWindows = filePath.includes('\\');
|
||||||
// Since we are mocking the UI a bit for this branch, we will just import directly
|
const sep = isWindows ? '\\' : '/';
|
||||||
|
const parts = filePath.split(sep);
|
||||||
|
const fileName = parts.pop();
|
||||||
|
const fileDir = parts.join(sep);
|
||||||
|
const fileStem = fileName.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
const targetCwd = this.appState?.cwd || fileDir;
|
||||||
|
const targetBasename = this.appState?.basename || fileStem;
|
||||||
|
|
||||||
|
const targetBasenameInput = document.getElementById('targetBasename');
|
||||||
|
if (targetBasenameInput && !targetBasenameInput.value.trim()) {
|
||||||
|
targetBasenameInput.value = targetBasename;
|
||||||
|
}
|
||||||
|
const selectedPathDisplay = document.getElementById('selectedPathDisplay');
|
||||||
|
if (selectedPathDisplay && (!selectedPathDisplay.textContent || selectedPathDisplay.textContent.includes('No directory') || !this.appState?.cwd)) {
|
||||||
|
selectedPathDisplay.textContent = `Directory: ${targetCwd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appState?.setTarget) {
|
||||||
|
await this.appState.setTarget(targetBasename, targetCwd);
|
||||||
|
}
|
||||||
|
|
||||||
const summary = await invoke('import_measurement_dataset', {
|
const summary = await invoke('import_measurement_dataset', {
|
||||||
filePath,
|
filePath,
|
||||||
targetCwd: this.appState.cwd,
|
targetCwd,
|
||||||
targetBasename: this.appState.basename,
|
targetBasename,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.appState.showNotice(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
|
this.appState?.showNotice?.(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
|
||||||
|
|
||||||
// Update state to jump to stage 4
|
// Update state to jump to stage 4
|
||||||
await this.appState.updateGating();
|
if (this.appState?.updateGating) {
|
||||||
this.appState.currentStage = 4;
|
await this.appState.updateGating();
|
||||||
this.appState.applyStageDOM(4);
|
}
|
||||||
|
if (this.appState) {
|
||||||
|
this.appState.currentStage = 4;
|
||||||
|
this.appState.applyStageDOM?.(4);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error');
|
this.appState?.showNotice?.(`Failed to import dataset: ${e}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -54,8 +54,8 @@ export function initColprof() {
|
|||||||
if (btnBrowseCustomSp && colprofCustomSpPath) {
|
if (btnBrowseCustomSp && colprofCustomSpPath) {
|
||||||
btnBrowseCustomSp.addEventListener("click", async () => {
|
btnBrowseCustomSp.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const selected = await window.__TAURI__.dialog.open({
|
const selected = await invoke("select_spectrum_file", {
|
||||||
filters: [{ name: 'Spectrum', extensions: ['sp'] }]
|
defaultDir: chartreadCwd || wizardState.cwd || null,
|
||||||
});
|
});
|
||||||
if (selected) {
|
if (selected) {
|
||||||
colprofCustomSpPath.value = selected;
|
colprofCustomSpPath.value = selected;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { computeQuickHull } from "./vendor/quickhull.js";
|
import { computeQuickHull } from "./vendor/quickhull.js";
|
||||||
import { labToSrgb } from "./color_convert.js";
|
import { labToSrgb } from "./color_convert.js";
|
||||||
|
|
||||||
const { invoke } = window.__TAURI__.core;
|
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null;
|
||||||
const { listen } = window.__TAURI__.event;
|
|
||||||
|
|
||||||
let scene, camera, renderer, labelRenderer, controls;
|
let scene, camera, renderer, labelRenderer, controls;
|
||||||
let currentProfileMesh = null;
|
let currentProfileMesh = null;
|
||||||
|
|||||||
@@ -1,20 +1,50 @@
|
|||||||
// Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing.
|
// Unit & console tests for gamut_viewer.js parsing.
|
||||||
// Run in a browser/devtools console after the app has loaded:
|
// Can be run in browser devtools console:
|
||||||
// import('./gamut_viewer.test.js').then(m => m.runAll())
|
// import('./gamut_viewer.test.js').then(m => m.runAll())
|
||||||
|
// Or in Node:
|
||||||
|
// node src/js/gamut_viewer.test.js
|
||||||
|
|
||||||
import { parseGamutFile } from './gamut_viewer.js';
|
// Node environment polyfill for browser globals
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
globalThis.window = {
|
||||||
|
__TAURI__: {
|
||||||
|
core: { invoke: () => Promise.resolve() },
|
||||||
|
event: { listen: () => Promise.resolve(() => {}) }
|
||||||
|
},
|
||||||
|
addEventListener: () => {},
|
||||||
|
dispatchEvent: () => {}
|
||||||
|
};
|
||||||
|
globalThis.document = {
|
||||||
|
getElementById: () => null,
|
||||||
|
querySelectorAll: () => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parseGamutFile } = await import('./gamut_viewer.js');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
export function runAll() {
|
export function runAll() {
|
||||||
console.group('gamut/profcheck parser tests');
|
console.group('Gamut Viewer Parser Tests');
|
||||||
|
passed = 0;
|
||||||
|
total = 0;
|
||||||
testParseGamutBasic();
|
testParseGamutBasic();
|
||||||
testParseGamutDualTable();
|
testParseGamutDualTable();
|
||||||
testParseGamutWithComments();
|
testParseGamutWithComments();
|
||||||
|
console.log(`\nResults: ${passed} / ${total} tests passed.`);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
|
if (passed !== total) {
|
||||||
|
throw new Error(`Gamut viewer parser tests failed: ${total - passed} failure(s)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertEqual(actual, expected, message) {
|
function assertEqual(actual, expected, message) {
|
||||||
|
total++;
|
||||||
const ok = JSON.stringify(actual) === JSON.stringify(expected);
|
const ok = JSON.stringify(actual) === JSON.stringify(expected);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
passed++;
|
||||||
console.log('PASS:', message);
|
console.log('PASS:', message);
|
||||||
} else {
|
} else {
|
||||||
console.error('FAIL:', message, 'expected', expected, 'got', actual);
|
console.error('FAIL:', message, 'expected', expected, 'got', actual);
|
||||||
@@ -78,3 +108,8 @@ END_DATA`;
|
|||||||
assertEqual(vertices.length, 4, 'commented gamut vertex count');
|
assertEqual(vertices.length, 4, 'commented gamut vertex count');
|
||||||
assertEqual(faces.length, 2, 'commented gamut face count');
|
assertEqual(faces.length, 2, 'commented gamut face count');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-run if executed in Node.js
|
||||||
|
if (typeof process !== 'undefined' && process.argv && process.argv[1]?.endsWith('gamut_viewer.test.js')) {
|
||||||
|
runAll();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user