Development #217

Merged
gronod merged 5 commits from development into main 2026-09-06 16:08:51 +01:00
15 changed files with 200 additions and 36 deletions
+22 -10
View File
@@ -9,6 +9,8 @@ jobs:
build-macos:
name: Build macOS (${{ matrix.platform.name }})
runs-on: ${{ matrix.platform.os }}
env:
XDG_CONFIG_HOME: ${{ runner.temp }}/.config
strategy:
matrix:
platform:
@@ -17,21 +19,31 @@ jobs:
target: x86_64-apple-darwin
binary_dir: macos-x86_64
arch: x86_64
- name: Apple Silicon
os: macos
target: aarch64-apple-darwin
binary_dir: macos-aarch64
arch: arm64
- name: Universal (Intel + Apple Silicon)
os: macos
target: universal-apple-darwin
binary_dir: macos-universal
arch: universal
# - name: Apple Silicon
# os: macos
# target: aarch64-apple-darwin
# binary_dir: macos-aarch64
# arch: arm64
# - name: Universal (Intel + Apple Silicon)
# os: macos
# target: universal-apple-darwin
# binary_dir: macos-universal
# arch: universal
steps:
- name: Checkout repository
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
uses: actions/setup-node@v4
with:
+10
View File
@@ -35,6 +35,16 @@ jobs:
- name: Checkout repository
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
uses: actions/setup-node@v4
with:
+1 -1
View File
@@ -2,7 +2,7 @@
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
[![Release](https://img.shields.io/badge/version-v0.8.2-blue.svg)](https://git.i3omb.com/gronod/ICCery)
[![Release](https://img.shields.io/badge/version-v0.8.3-blue.svg)](https://git.i3omb.com/gronod/ICCery)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://git.i3omb.com/gronod/ICCery)
[![Framework](https://img.shields.io/badge/framework-Tauri%20v2%20%2B%20Rust-orange.svg)](https://tauri.app)
[![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md)
+6 -1
View File
@@ -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.
- **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`).
- **Current Version**: `v0.8.2` (Production release).
- **Current Version**: `v0.8.3` (Production release).
---
@@ -108,6 +108,11 @@ 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.
- [ ] ~~**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.
---
## 3. Future Roadmap
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "iccery",
"private": true,
"version": "0.8.2",
"version": "0.8.3",
"type": "module",
"scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs",
+1 -1
View File
@@ -1423,7 +1423,7 @@ dependencies = [
[[package]]
name = "iccery"
version = "0.8.2"
version = "0.8.3"
dependencies = [
"base64 0.22.1",
"image",
+12 -1
View File
@@ -1,9 +1,20 @@
[package]
name = "iccery"
version = "0.8.2"
version = "0.8.3"
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"]
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
+6
View File
@@ -10,6 +10,12 @@ fn main() {
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
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();
+55
View File
@@ -391,6 +391,61 @@ pub async fn select_profile_file(
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]
pub async fn select_target_file(
app: AppHandle,
+2
View File
@@ -51,6 +51,8 @@ pub fn run() {
commands::inspect_dataset_preview,
commands::select_existing_target,
commands::select_profile_file,
commands::select_spectrum_file,
commands::select_dataset_file,
commands::select_target_file,
commands::select_directory,
commands::send_stdin,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery",
"version": "0.8.2",
"version": "0.8.3",
"identifier": "com.gronod.iccery",
"build": {
"frontendDist": "../src"
+40 -11
View File
@@ -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 {
constructor(appState) {
@@ -22,26 +24,53 @@ export class CgatsInterop {
async handleImport() {
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
// Inspect first to show modal (optional, skipping for now, directly import)
// Since we are mocking the UI a bit for this branch, we will just import directly
const isWindows = filePath.includes('\\');
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', {
filePath,
targetCwd: this.appState.cwd,
targetBasename: this.appState.basename,
targetCwd,
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
await this.appState.updateGating();
this.appState.currentStage = 4;
this.appState.applyStageDOM(4);
if (this.appState?.updateGating) {
await this.appState.updateGating();
}
if (this.appState) {
this.appState.currentStage = 4;
this.appState.applyStageDOM?.(4);
}
} catch (e) {
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error');
this.appState?.showNotice?.(`Failed to import dataset: ${e}`, 'error');
}
}
+2 -2
View File
@@ -54,8 +54,8 @@ export function initColprof() {
if (btnBrowseCustomSp && colprofCustomSpPath) {
btnBrowseCustomSp.addEventListener("click", async () => {
try {
const selected = await window.__TAURI__.dialog.open({
filters: [{ name: 'Spectrum', extensions: ['sp'] }]
const selected = await invoke("select_spectrum_file", {
defaultDir: chartreadCwd || wizardState.cwd || null,
});
if (selected) {
colprofCustomSpPath.value = selected;
+2 -3
View File
@@ -1,8 +1,7 @@
import { computeQuickHull } from "./vendor/quickhull.js";
import { computeQuickHull } from "./vendor/quickhull.js";
import { labToSrgb } from "./color_convert.js";
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null;
let scene, camera, renderer, labelRenderer, controls;
let currentProfileMesh = null;
+39 -4
View File
@@ -1,20 +1,50 @@
// Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing.
// Run in a browser/devtools console after the app has loaded:
// Unit & console tests for gamut_viewer.js parsing.
// Can be run in browser devtools console:
// 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() {
console.group('gamut/profcheck parser tests');
console.group('Gamut Viewer Parser Tests');
passed = 0;
total = 0;
testParseGamutBasic();
testParseGamutDualTable();
testParseGamutWithComments();
console.log(`\nResults: ${passed} / ${total} tests passed.`);
console.groupEnd();
if (passed !== total) {
throw new Error(`Gamut viewer parser tests failed: ${total - passed} failure(s)`);
}
}
function assertEqual(actual, expected, message) {
total++;
const ok = JSON.stringify(actual) === JSON.stringify(expected);
if (ok) {
passed++;
console.log('PASS:', message);
} else {
console.error('FAIL:', message, 'expected', expected, 'got', actual);
@@ -78,3 +108,8 @@ END_DATA`;
assertEqual(vertices.length, 4, 'commented gamut vertex 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();
}