Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d056c26bf | ||
|
|
c4f037535a | ||
|
|
191bcb0280 | ||
|
|
9c625ee21f | ||
|
|
40cc4ad701 | ||
|
|
658d9813bd | ||
|
|
368ae56cf3 | ||
|
|
bd539df009 | ||
|
|
1b91b0a94e | ||
|
|
890e7281eb | ||
|
|
ccaad2d940 | ||
|
|
7e5e20cb69 | ||
|
|
e72039c123 | ||
|
|
d37dcedfd9 |
@@ -1,45 +1,75 @@
|
|||||||
# ICCery
|
# ICCery
|
||||||
|
|
||||||
Native macOS frontend for printer ICC/ICM profiling. ICCery walks a user from chart generation through measurement, `colprof`, verification, and ColorSync install. It is **not** a colour engine.
|
Native macOS frontend for printer ICC/ICM profiling. ICCery walks a user from
|
||||||
|
chart generation through measurement, `colprof`, verification, and ColorSync
|
||||||
|
install. It is **not** a colour engine.
|
||||||
|
|
||||||
All measurement, chart generation, and profile mathematics live in the [Gronod ArgyllCMS 3.5.0 fork](https://git.i3omb.com/gronod/argyllcms), spawned as AGPLv3 child processes. The GUI never `dlopen`s or links Argyll.
|
**End-user guide:** the [repository wiki](https://git.i3omb.com/gronod/iccery-v2-mac/wiki)
|
||||||
|
covers every screen (Getting Started through Troubleshooting). This README is
|
||||||
|
for building, packaging, and contributing.
|
||||||
|
|
||||||
|
All measurement, chart generation, and profile mathematics live in the
|
||||||
|
[Gronod ArgyllCMS 3.5.0 fork](https://git.i3omb.com/gronod/argyllcms), spawned
|
||||||
|
as AGPLv3 child processes. The GUI never `dlopen`s or links Argyll.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Product | ICCery v2 for macOS |
|
| Product | ICCery v2 for macOS |
|
||||||
| Bundle | `com.gronod.iccery2` |
|
| Bundle | `com.gronod.iccery2` |
|
||||||
|
| Version | 2.0.0 |
|
||||||
| Floor | macOS 12.0 Monterey, universal `arm64` + `x86_64` |
|
| Floor | macOS 12.0 Monterey, universal `arm64` + `x86_64` |
|
||||||
|
| Toolchain | Xcode 14.2 / Swift 5.7 (project `SWIFT_VERSION` is 5.0) |
|
||||||
|
| CI | Gitea Actions `macos-12` runner |
|
||||||
| Default branch | `develop` |
|
| Default branch | `develop` |
|
||||||
| M6 | Stage 0 calibration, CGATS import, SceneKit gamut viewer, packaging — shipped on `develop` |
|
| M6 | Stage 0 calibration, CGATS import, SceneKit gamut viewer, packaging — shipped |
|
||||||
| M7 | Pre-UAT hardening & baseline consolidation — shipped on `develop` |
|
| M7 | Pre-UAT hardening — shipped |
|
||||||
| M8 | Deduplication/consolidation contracts & UAT-ready hardening (#79–#86) — shipped on `develop` |
|
| M8 | Deduplication contracts & UAT-ready hardening (#79–#86) — shipped |
|
||||||
| M9 | macOS 12 / Xcode 14.2 retarget — shipped on `develop` (PR #145) |
|
| M9 | macOS 12 / Xcode 14.2 retarget (PR #145) — shipped |
|
||||||
| M10 | Studio workflow (#146–#149) — in flight on `milestone/m10-studio` |
|
| M10 | Studio workflow: gamut compare (#147), Spot Read (#148), project files (#149) shipped on `develop`; media library (#146) is in the tree, issue still open |
|
||||||
| Licence | Proprietary source in [`LICENCE.md`](LICENCE.md); bundled Argyll sidecars remain AGPLv3 |
|
| Licence | Proprietary source in [`LICENCE.md`](LICENCE.md); bundled Argyll sidecars remain AGPLv3 |
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
The wizard is artefact-gated:
|
The wizard is artefact-gated:
|
||||||
|
|
||||||
1. **Stage 0** — printer calibration: `printcal` / `applycal` session, `CAL_` basename restore
|
1. **Stage 1** — `targen` → `.ti1`
|
||||||
2. **Stage 1** — `targen` → `.ti1`
|
2. **Stage 2** — `printtarg` → `.ti2` + TIFF, unmanaged `lp` spool, bound `NSPrintPanel`
|
||||||
3. **Stage 2** — `printtarg` → `.ti2` + TIFF, unmanaged `lp` spool, bound `NSPrintPanel`
|
3. **Stage 3** — `instlist` + streaming `chartread` (strip / XY / handheld) → `.ti3`, multi-pass average, CIEDE2000
|
||||||
4. **Stage 3** — `instlist` + streaming `chartread` (strip / XY / handheld) → `.ti3`, multi-pass average, CIEDE2000
|
4. **Stage 4** — `colprof` → `.icc` / `.icm`; optional `applycal`; `iccgamut` next to the profile
|
||||||
5. **Stage 4** — `colprof` → `.icc` / `.icm`; optional `applycal`; `iccgamut` next to the profile
|
5. **Stage 5** — `profcheck`, verification history, ColorSync user/system install
|
||||||
6. **Stage 5** — `profcheck`, verification history, ColorSync user/system install
|
|
||||||
|
|
||||||
Plus CGATS dataset import (`.ti3` / `.txt` / `.cgats` / `.csv`), SceneKit gamut preview with sRGB overlay, and signed `.dmg` packaging.
|
Plus:
|
||||||
|
|
||||||
**Not this product:** display calibration (`dispwin` / `dispread`), i18n, Windows/Linux print trees, in-process Argyll, App Sandbox.
|
- **Calibrate Printer** — optional `printcal` / `applycal` session under a `CAL_` basename
|
||||||
|
- **CGATS import** — `.ti3` / `.txt` / `.cgats` / `.csv`
|
||||||
|
- **Media recipes and presets** — printer + paper + ink bound to a preset and optional `.cal`
|
||||||
|
- **Spot Read** — live one-patch Lab/XYZ from the instrument
|
||||||
|
- **Project files** — `.icceryproj` bookmark over folder, basename, recipe, last ΔE
|
||||||
|
- **Gamut viewer** — SceneKit Lab hull, sRGB overlay, second-profile compare, click-inspect
|
||||||
|
- **Settings** — default instrument, ΔE good/warning cutoffs, install location, logging
|
||||||
|
- Signed `.dmg` packaging with a HiDPI Finder background (Monterey through Sonoma)
|
||||||
|
|
||||||
|
**Not this product:** display calibration (`dispwin` / `dispread`), i18n,
|
||||||
|
Windows/Linux print trees, in-process Argyll, App Sandbox.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- macOS 14+
|
To **run** a packaged build:
|
||||||
- Xcode 15.4+ with the macOS 14 SDK and Swift 6.0
|
|
||||||
- [XcodeGen](https://github.com/yonaskolb/XcodeGen)
|
|
||||||
- Network once, to fetch Argyll sidecars
|
|
||||||
|
|
||||||
App Sandbox is **off**. Hardened Runtime is **on**. Entitlements live in `ICCery.entitlements`.
|
- macOS 12.0 Monterey or later (Intel or Apple silicon)
|
||||||
|
|
||||||
|
To **build** on the supported CI/host floor:
|
||||||
|
|
||||||
|
- macOS 12 with **Xcode 14.2** (macOS 12 SDK, Swift 5.7)
|
||||||
|
- [XcodeGen](https://github.com/yonaskolb/XcodeGen) **2.38.0** (Homebrew’s current
|
||||||
|
formula needs Xcode 15.3; CI installs the pinned zip via
|
||||||
|
`scripts/ensure-host-tools.sh`)
|
||||||
|
- Network once, to fetch Argyll sidecars
|
||||||
|
- For DMGs: Python 3.9+ and `dmgbuild==1.6.7` in `build/.venv-dmgbuild`
|
||||||
|
(`INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh`)
|
||||||
|
|
||||||
|
App Sandbox is **off**. Hardened Runtime is **on**. Entitlements live in
|
||||||
|
`ICCery.entitlements`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -49,7 +79,7 @@ cd iccery-v2-mac
|
|||||||
git checkout develop
|
git checkout develop
|
||||||
|
|
||||||
make fetch-argyll # Vendor/Argyll/macos-universal/, ad-hoc signed
|
make fetch-argyll # Vendor/Argyll/macos-universal/, ad-hoc signed
|
||||||
make test # xcodegen + xcodebuild build test
|
make test # xcodegen + xcodebuild build test (host arch)
|
||||||
make universal # ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
|
make universal # ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -62,9 +92,14 @@ xcodebuild test -scheme ICCery \
|
|||||||
ARCHS="$(uname -m)"
|
ARCHS="$(uname -m)"
|
||||||
```
|
```
|
||||||
|
|
||||||
`project.yml` sets `ARCHS: "$(ARCHS_STANDARD)"`, so a plain `xcodebuild test` (and `make test`) builds universal; the `ARCHS="$(uname -m)"` override narrows it to the host slice.
|
`project.yml` sets `ARCHS: "$(ARCHS_STANDARD)"`. CI and `make test` override
|
||||||
|
that with `ARCHS="$(uname -m)"` so unit/UI tests build the host slice only.
|
||||||
|
Fat binaries are `make universal` / `scripts/package-release.sh`.
|
||||||
|
|
||||||
Sidecars are **not** in git. `scripts/fetch-argyll.sh` pulls the latest (or `ARGYLL_RELEASE_TAG`) macOS-universal release from `gronod/argyllcms`, extracts to `Vendor/Argyll/macos-universal/`, ad-hoc signs every Mach-O, and fails if `codesign -dvv` or the `instlist` marker is missing.
|
Sidecars are **not** in git. `scripts/fetch-argyll.sh` pulls the latest (or
|
||||||
|
`ARGYLL_RELEASE_TAG`) macOS-universal release from `gronod/argyllcms`, extracts
|
||||||
|
to `Vendor/Argyll/macos-universal/`, ad-hoc signs every Mach-O, and fails if
|
||||||
|
`codesign -dvv` or the `instlist` marker is missing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# optional
|
# optional
|
||||||
@@ -74,9 +109,11 @@ export ARGYLL_RELEASE_TAG=… # default: latest
|
|||||||
export GITEA_TOKEN=… # private releases
|
export GITEA_TOKEN=… # private releases
|
||||||
```
|
```
|
||||||
|
|
||||||
`make clean` drops `ICCery.xcodeproj`, `DerivedData`, and `Packages/ICCeryCore/.build`.
|
`make clean` drops `ICCery.xcodeproj`, `DerivedData`, and
|
||||||
|
`Packages/ICCeryCore/.build`.
|
||||||
|
|
||||||
Do not open the generated xcodeproj as the source of truth. Edit `project.yml` and regenerate.
|
Do not open the generated xcodeproj as the source of truth. Edit `project.yml`
|
||||||
|
and regenerate.
|
||||||
|
|
||||||
## Release packaging
|
## Release packaging
|
||||||
|
|
||||||
@@ -84,11 +121,20 @@ Do not open the generated xcodeproj as the source of truth. Edit `project.yml` a
|
|||||||
scripts/package-release.sh # fetch → sign → universal build → verify → DMG
|
scripts/package-release.sh # fetch → sign → universal build → verify → DMG
|
||||||
```
|
```
|
||||||
|
|
||||||
The script builds with a fixed derived data path (`build/DerivedData`),
|
The script builds with a fixed derived data path (`build/DerivedData`), locates
|
||||||
locates `Release/ICCery.app` from it, signs the bundle, recursively verifies
|
`Release/ICCery.app` from it, signs the bundle, recursively verifies every
|
||||||
every bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), and
|
bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), builds a
|
||||||
writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` via `dmgbuild`. Sidecars stay
|
HiDPI TIFF from `Resources/dmg-background.png` (+ `@2x`) via `tiffutil`, and
|
||||||
ad-hoc signed inside the bundle — the app is never `codesign --deep`ed.
|
writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` with `dmgbuild==1.6.7`.
|
||||||
|
|
||||||
|
Sidecars stay ad-hoc signed inside the bundle — the app is never
|
||||||
|
`codesign --deep`ed.
|
||||||
|
|
||||||
|
`dmgbuild` is **not** a test-job dependency. The package job sets
|
||||||
|
`INSTALL_DMGBUILD=1` so `scripts/ensure-host-tools.sh` creates
|
||||||
|
`build/.venv-dmgbuild`. On the Monterey runner (Python 3.9) that install uses
|
||||||
|
`PIP_IGNORE_REQUIRES_PYTHON=1` and pins `pip>=24.3,<26.1` (pip 26.1+ needs
|
||||||
|
3.10). Missing background art is a hard fail (#95).
|
||||||
|
|
||||||
Environment variables read by the pipeline:
|
Environment variables read by the pipeline:
|
||||||
|
|
||||||
@@ -110,30 +156,47 @@ Resources/ assets; Argyll reference files (not the tools)
|
|||||||
Vendor/Argyll/ fetched sidecars (gitignored)
|
Vendor/Argyll/ fetched sidecars (gitignored)
|
||||||
Tests/ICCeryCoreTests/ argv goldens, parsers, stores
|
Tests/ICCeryCoreTests/ argv goldens, parsers, stores
|
||||||
Tests/ICCeryUITests/ fixture / mock-binary UI tests
|
Tests/ICCeryUITests/ fixture / mock-binary UI tests
|
||||||
|
scripts/ensure-host-tools.sh
|
||||||
scripts/fetch-argyll.sh
|
scripts/fetch-argyll.sh
|
||||||
|
scripts/package-release.sh
|
||||||
docs/ functional spec + v2 ticket plan
|
docs/ functional spec + v2 ticket plan
|
||||||
```
|
```
|
||||||
|
|
||||||
`ICCeryPrintKit` (issue #16, Quartz / AirPrint / TargetPrint) is v2.1 and is not in this tree.
|
`ICCeryPrintKit` (issue #16, Quartz / AirPrint / TargetPrint) is v2.1 and is
|
||||||
|
not in this tree.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **Spawn, never link.** Tools resolve through `BinaryResolver` inside the bundle / `Vendor` tree. `$PATH` is not searched. `ARGYLL_NOT_INTERACTIVE=1` is always set.
|
- **Spawn, never link.** Tools resolve through `BinaryResolver` inside the
|
||||||
- **`ProcessManager` actor** owns child lifetime. Streaming tools (`chartread`, `printcal`, etc.) use the event bus; one-shot tools use `runCaptured`. Exclusive `ProcessID` leases. Quit path: `q\n`, ~500 ms, kill; `killAll` on terminate.
|
bundle / `Vendor` tree. `$PATH` is not searched. `ARGYLL_NOT_INTERACTIVE=1`
|
||||||
- **Argv builders** in ICCeryCore (`TargenArgs`, `PrinttargArgs`, `ChartreadArgs`, `ColprofArgs`, `ApplycalArgs`, `IccgamutArgs`, `ProfcheckArgs`, `LpArgs`, …). UI must not concatenate flags.
|
is always set.
|
||||||
- **Atomic artefacts.** Writes go to `*.tmp` then `replaceItemAt`. `applycal` must not replace the input profile on cancel or non-zero exit.
|
- **`ProcessManager` actor** owns child lifetime. Streaming tools
|
||||||
- **Concurrency.** View models are `@MainActor`. No blocking I/O on the main actor. SwiftUI `@Observable` for new state.
|
(`chartread`, `printtarg`, `colprof`, …) use the event bus; one-shot tools
|
||||||
- **Print.** Unmanaged `lp` with ColorSync suppression (`AP_ColorMatchingMode` / `AP.ColorMatchingMode`). Captured `NSPrintPanel` options win over derived CUPS keys. Never `lp -o raw`.
|
(`printcal`, `applycal`, CUPS) use `runCaptured`. Exclusive `ProcessID`
|
||||||
|
leases. Quit path: `q\n`, ~500 ms, kill; `killAll` on terminate.
|
||||||
|
- **Argv builders** in ICCeryCore (`TargenArgs`, `PrinttargArgs`,
|
||||||
|
`ChartreadArgs`, `ColprofArgs`, `ApplycalArgs`, `IccgamutArgs`,
|
||||||
|
`ProfcheckArgs`, `LpArgs`, `SpotReadArgs`, …). UI must not concatenate flags.
|
||||||
|
- **Atomic artefacts.** Writes go to `*.tmp` then `replaceItemAt`. `applycal`
|
||||||
|
must not replace the input profile on cancel or non-zero exit.
|
||||||
|
- **Concurrency.** View models are `@MainActor`. No blocking I/O on the main
|
||||||
|
actor. Swift 5.7 / macOS 12: `ObservableObject`, not Observation
|
||||||
|
`@Observable`.
|
||||||
|
- **Print.** Unmanaged `lp` with ColorSync suppression
|
||||||
|
(`AP_ColorMatchingMode` / `AP.ColorMatchingMode`). Captured `NSPrintPanel`
|
||||||
|
options win over derived CUPS keys. Never `lp -o raw`.
|
||||||
|
- **SwiftUI ViewBuilder.** Xcode 14.2 / Swift 5.7 still has the ten-child
|
||||||
|
limit. Split large `VStack`/`Group` trees (#146).
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# full suite (host arch)
|
# full suite (host arch) — same as CI
|
||||||
xcodebuild test -scheme ICCery \
|
xcodebuild test -scheme ICCery \
|
||||||
-destination 'platform=macOS' \
|
-destination 'platform=macOS' \
|
||||||
ARCHS="$(uname -m)"
|
ARCHS="$(uname -m)"
|
||||||
|
|
||||||
# to compile-check both slices instead:
|
# fat compile-check (not the default test path):
|
||||||
# ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
|
# ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
|
||||||
|
|
||||||
# examples
|
# examples
|
||||||
@@ -143,9 +206,20 @@ xcodebuild test -scheme ICCery -destination 'platform=macOS' \
|
|||||||
-only-testing:ICCeryUITests/Milestone5UITests
|
-only-testing:ICCeryUITests/Milestone5UITests
|
||||||
```
|
```
|
||||||
|
|
||||||
UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll / CUPS fixtures live under the test bundles; they must not be treated as proof that a real `.gam` / `.icc` was extracted.
|
CI (`.gitea/workflows/macos.yml`) runs `build-and-test` then `package` on
|
||||||
|
`develop` and on `v*` tags. Tags whose name contains `prerelease` skip the
|
||||||
|
test job and still package. `pull_request` is wired for **`develop` only**.
|
||||||
|
|
||||||
Hardware gates (real instrument, real printer, Gatekeeper-open `.dmg`) are manual and block release, not compile.
|
UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll /
|
||||||
|
CUPS fixtures live under the test bundles; they must not be treated as proof
|
||||||
|
that a real `.gam` / `.icc` was extracted.
|
||||||
|
|
||||||
|
Hardware gates (real instrument, real printer, Gatekeeper-open `.dmg`) are
|
||||||
|
manual and block release, not compile.
|
||||||
|
|
||||||
|
`ArgyllRunnerPrinttargTests.testSuccess` can flake if streaming stdout is
|
||||||
|
dropped on a fast mock exit; that is a `ProcessManager` drain race, not a
|
||||||
|
missing fixture.
|
||||||
|
|
||||||
## Instruments
|
## Instruments
|
||||||
|
|
||||||
@@ -156,11 +230,18 @@ Detected via bundled `instlist`:
|
|||||||
- SpyderPrint (`p3`)
|
- SpyderPrint (`p3`)
|
||||||
- SpectroScan (`SS`)
|
- SpectroScan (`SS`)
|
||||||
- DTP20 / 22 / 41 / 51
|
- DTP20 / 22 / 41 / 51
|
||||||
- XY tables (SpectroScan, i1iO) when the `instlist` name matches `/spectro\s?scan|i1io/i`
|
- XY tables (SpectroScan, i1iO) when the `instlist` name matches
|
||||||
|
`/spectro\s?scan|i1io/i`
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Normative spec is [`docs/`](docs/README.md). Implementation order:
|
| Where | Audience |
|
||||||
|
|---|---|
|
||||||
|
| [Wiki](https://git.i3omb.com/gronod/iccery-v2-mac/wiki) | End users — screens, workflow, troubleshooting |
|
||||||
|
| [`docs/`](docs/README.md) | Functional spec (normative for implementers) |
|
||||||
|
| [`AGENTS.md`](AGENTS.md), [`BUILD-PLAN.md`](BUILD-PLAN.md) | Agent / branch rules |
|
||||||
|
|
||||||
|
Implementation order in `docs/`:
|
||||||
|
|
||||||
| Doc | Topic |
|
| Doc | Topic |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -168,29 +249,40 @@ Normative spec is [`docs/`](docs/README.md). Implementation order:
|
|||||||
| [`docs/03-ipc-and-process-manager.md`](docs/03-ipc-and-process-manager.md) | Spawn / stdin / kill |
|
| [`docs/03-ipc-and-process-manager.md`](docs/03-ipc-and-process-manager.md) | Spawn / stdin / kill |
|
||||||
| [`docs/04-argyll-binaries.md`](docs/04-argyll-binaries.md) | CLI argv |
|
| [`docs/04-argyll-binaries.md`](docs/04-argyll-binaries.md) | CLI argv |
|
||||||
| [`docs/06-wizard-and-artefacts.md`](docs/06-wizard-and-artefacts.md) | Gating |
|
| [`docs/06-wizard-and-artefacts.md`](docs/06-wizard-and-artefacts.md) | Gating |
|
||||||
|
| [`docs/23-assets.md`](docs/23-assets.md) | Icons, DMG chrome |
|
||||||
| [`docs/24-issues-invariants.md`](docs/24-issues-invariants.md) | Bugs that must not return |
|
| [`docs/24-issues-invariants.md`](docs/24-issues-invariants.md) | Bugs that must not return |
|
||||||
| [`docs/26-v2-mac-ticket-plan.md`](docs/26-v2-mac-ticket-plan.md) | Gitea tickets |
|
| [`docs/26-v2-mac-ticket-plan.md`](docs/26-v2-mac-ticket-plan.md) | Gitea tickets |
|
||||||
| [`docs/PREUAT.md`](docs/PREUAT.md) | Pre-UAT tester kit |
|
| [`docs/PREUAT.md`](docs/PREUAT.md) | Pre-UAT tester kit |
|
||||||
|
|
||||||
Agent / branch rules: [`AGENTS.md`](AGENTS.md), [`BUILD-PLAN.md`](BUILD-PLAN.md).
|
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
```
|
```
|
||||||
develop
|
develop # integration; PRs land here unless a milestone branch is announced
|
||||||
└── milestone/m10-studio # M10 integration branch
|
main # protected release line (PR from develop)
|
||||||
└── feat/<issue>-<slug> # one issue per branch
|
feat/<issue>-<slug>
|
||||||
|
fix/<issue>-<slug>
|
||||||
```
|
```
|
||||||
|
|
||||||
Feature PRs target the current milestone branch, not `develop`. The milestone branch merges to `develop` when its issues are green. Completion PRs for issues #146–#149 target `milestone/m10-studio`; `milestone/m10-studio` merges into `develop` once all milestone gates pass. Do not open umbrella "bugfix" branches that mix tickets.
|
Open feature/fix PRs against **`develop`**. A `milestone/m…` integration
|
||||||
|
branch is used only while that milestone is assembling; `milestone/m10-studio`
|
||||||
|
has been merged and deleted. Do not open umbrella “bugfix” branches that mix
|
||||||
|
tickets.
|
||||||
|
|
||||||
|
`main` is push-protected and requires status check
|
||||||
|
`macOS CI / build-and-test (push)`. Protected **file** patterns on `main`
|
||||||
|
block PR merges that touch matching paths — do not set that field to `*`.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
GUI source: © 2026 Gordon Bolton — see [`LICENCE.md`](LICENCE.md). Viewing and personal evaluation only unless a separate grant says otherwise.
|
GUI source: © 2026 Gordon Bolton — see [`LICENCE.md`](LICENCE.md). Viewing
|
||||||
|
and personal evaluation only unless a separate grant says otherwise.
|
||||||
|
|
||||||
ArgyllCMS binaries fetched into `Vendor/Argyll/` are **AGPLv3**. They stay subprocess-isolated (stdin / stdout / stderr only). Linking them, or spawning via `$PATH`, is a licence break.
|
ArgyllCMS binaries fetched into `Vendor/Argyll/` are **AGPLv3**. They stay
|
||||||
|
subprocess-isolated (stdin / stdout / stderr only). Linking them, or spawning
|
||||||
|
via `$PATH`, is a licence break.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
- [User wiki](https://git.i3omb.com/gronod/iccery-v2-mac/wiki)
|
||||||
- [gronod/argyllcms](https://git.i3omb.com/gronod/argyllcms) — Argyll 3.5.0 fork (`-u` JSON, `instlist`)
|
- [gronod/argyllcms](https://git.i3omb.com/gronod/argyllcms) — Argyll 3.5.0 fork (`-u` JSON, `instlist`)
|
||||||
- [gronod/ICCery](https://git.i3omb.com/gronod/ICCery) — v1 Tauri application (spec source, not this tree)
|
- [gronod/ICCery](https://git.i3omb.com/gronod/ICCery) — v1 Tauri application (spec source, not this tree)
|
||||||
|
|||||||
@@ -220,6 +220,15 @@ struct ManageMediaDialog: View {
|
|||||||
.accessibilityIdentifier("mediaLibraryList")
|
.accessibilityIdentifier("mediaLibraryList")
|
||||||
.frame(minHeight: 260)
|
.frame(minHeight: 260)
|
||||||
|
|
||||||
|
if let notice = media.manageApplyNotice {
|
||||||
|
Text(notice)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.accessibilityIdentifier("manageMediaNotice")
|
||||||
|
.accessibilityValue(notice)
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button("Apply selected") {
|
Button("Apply selected") {
|
||||||
if let id = selection,
|
if let id = selection,
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
@Published var saveMediaApplyCal = false
|
@Published var saveMediaApplyCal = false
|
||||||
/// Inline caption inside the capture sheet (no a11y id — roster complete).
|
/// Inline caption inside the capture sheet (no a11y id — roster complete).
|
||||||
@Published var saveMediaError: String?
|
@Published var saveMediaError: String?
|
||||||
|
/// Last failed Apply while Manage is open. The window banner sits
|
||||||
|
/// behind the sheet on Monterey, so the dialog shows this too (#170).
|
||||||
|
@Published var manageApplyNotice: String?
|
||||||
|
|
||||||
/// Pure flow flag — the manage sheet's "Capture current…" asks the
|
/// Pure flow flag — the manage sheet's "Capture current…" asks the
|
||||||
/// sheet's `onDismiss` to open the capture sheet, avoiding a
|
/// sheet's `onDismiss` to open the capture sheet, avoiding a
|
||||||
@@ -120,24 +123,21 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
/// with warning; the refusal is permanent so re-clicking can't help).
|
/// with warning; the refusal is permanent so re-clicking can't help).
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func apply(_ recipe: MediaRecipe) async -> Bool {
|
func apply(_ recipe: MediaRecipe) async -> Bool {
|
||||||
|
manageApplyNotice = nil
|
||||||
guard let r = try? recipe.validated() else {
|
guard let r = try? recipe.validated() else {
|
||||||
workflow.wizard.showNotice(
|
return failApply("Media recipe is invalid — not applied.", kind: .error)
|
||||||
"Media recipe is invalid — not applied.", kind: .error)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
guard let preset = environment.presetStore.all()
|
guard let preset = environment.presetStore.all()
|
||||||
.first(where: { $0.id == r.presetID })
|
.first(where: { $0.id == r.presetID })
|
||||||
else {
|
else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Preset \(r.presetID) no longer exists — recipe not applied.",
|
"Preset \(r.presetID) no longer exists — recipe not applied.",
|
||||||
kind: .error)
|
kind: .error)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
guard preset.colourSpace.lowercased() == r.colourSpace.lowercased() else {
|
guard preset.colourSpace.lowercased() == r.colourSpace.lowercased() else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Recipe colour space does not match its preset — not applied.",
|
"Recipe colour space does not match its preset — not applied.",
|
||||||
kind: .error)
|
kind: .error)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing #82 mapping: presetSelect jumps, Stage 1/2/4 fields.
|
// Existing #82 mapping: presetSelect jumps, Stage 1/2/4 fields.
|
||||||
@@ -145,8 +145,6 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
// Literal per issue: displayName, not the queue id.
|
// Literal per issue: displayName, not the queue id.
|
||||||
workflow.wizard.printerName = r.printerDisplayName
|
workflow.wizard.printerName = r.printerDisplayName
|
||||||
|
|
||||||
var succeeded = true
|
|
||||||
|
|
||||||
// Queue: enumerate fresh via the session's serialized path —
|
// Queue: enumerate fresh via the session's serialized path —
|
||||||
// listPrinters uses fixed process ids, so an overlapping
|
// listPrinters uses fixed process ids, so an overlapping
|
||||||
// enumeration would throw duplicateID. An empty result is a
|
// enumeration would throw duplicateID. An empty result is a
|
||||||
@@ -156,16 +154,14 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
workflow.print.selectedPrinter = r.printerID
|
workflow.print.selectedPrinter = r.printerID
|
||||||
await workflow.print.reloadSelectedCapabilities()
|
await workflow.print.reloadSelectedCapabilities()
|
||||||
} else {
|
} else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Printer \(r.printerDisplayName) is not installed.",
|
"Printer \(r.printerDisplayName) is not installed.",
|
||||||
kind: .warning)
|
kind: .warning)
|
||||||
succeeded = false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Could not enumerate printers — queue left unchanged.",
|
"Could not enumerate printers — queue left unchanged.",
|
||||||
kind: .warning)
|
kind: .warning)
|
||||||
succeeded = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calibration — the recipe is authoritative and runs after
|
// Calibration — the recipe is authoritative and runs after
|
||||||
@@ -195,10 +191,8 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
guard FileManager.default.fileExists(atPath: calPath) else {
|
guard FileManager.default.fileExists(atPath: calPath) else {
|
||||||
workflow.profile.applyCalibration = false
|
workflow.profile.applyCalibration = false
|
||||||
workflow.profile.calibrationFile = calPath
|
workflow.profile.calibrationFile = calPath
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Calibration file is missing: \(calPath)", kind: .error)
|
"Calibration file is missing: \(calPath)", kind: .error)
|
||||||
refreshStaleness()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let staleDays = environment.settingsStore.load().calibrationStaleDays
|
let staleDays = environment.settingsStore.load().calibrationStaleDays
|
||||||
@@ -215,23 +209,26 @@ final class MediaLibraryViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
workflow.profile.applyCalibration = false
|
workflow.profile.applyCalibration = false
|
||||||
workflow.wizard.showNotice(
|
return failApply(
|
||||||
"Could not load calibration: \(error.localizedDescription)",
|
"Could not load calibration: \(error.localizedDescription)",
|
||||||
kind: .error)
|
kind: .error)
|
||||||
refreshStaleness()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workflow.profile.applyCalibration = false
|
workflow.profile.applyCalibration = false
|
||||||
workflow.profile.calibrationFile = calPath
|
workflow.profile.calibrationFile = calPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if succeeded {
|
selectedRecipeID = r.id
|
||||||
selectedRecipeID = r.id
|
workflow.wizard.showNotice("Applied \(r.name)")
|
||||||
workflow.wizard.showNotice("Applied \(r.name)")
|
|
||||||
}
|
|
||||||
refreshStaleness()
|
refreshStaleness()
|
||||||
return succeeded
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func failApply(_ text: String, kind: Notice.Kind) -> Bool {
|
||||||
|
manageApplyNotice = text
|
||||||
|
workflow.wizard.showNotice(text, kind: kind)
|
||||||
|
refreshStaleness()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Capture
|
// MARK: - Capture
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ final class Milestone10MediaLibraryUITests: XCTestCase {
|
|||||||
XCTAssertTrue(apply.waitForExistence(timeout: 10))
|
XCTAssertTrue(apply.waitForExistence(timeout: 10))
|
||||||
apply.click()
|
apply.click()
|
||||||
|
|
||||||
let notice = waitFor("noticeText")
|
// The window banner (`noticeText`) sits behind this sheet on
|
||||||
|
// Monterey (#170). Assert the in-sheet copy instead.
|
||||||
|
let notice = waitFor("manageMediaNotice", timeout: 15)
|
||||||
let text = (notice.value as? String) ?? notice.label
|
let text = (notice.value as? String) ?? notice.label
|
||||||
XCTAssertTrue(
|
XCTAssertTrue(
|
||||||
text.contains("is not installed"),
|
text.contains("is not installed"),
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
@startuml ICCery-UI-sheets
|
||||||
|
title Sheets Settings, presets, media, Spot Read, Gamut, project
|
||||||
|
!theme plain
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam defaultFontColor black
|
||||||
|
skinparam defaultFontSize 13
|
||||||
|
skinparam mapBackgroundColor white
|
||||||
|
skinparam mapBorderColor #222222
|
||||||
|
skinparam mapFontColor black
|
||||||
|
skinparam arrowColor #222222
|
||||||
|
skinparam shadowing false
|
||||||
|
hide circle
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
map "SettingsView.swift" as set {
|
||||||
|
Argyll dir => TextField+Browse : empty = bundled
|
||||||
|
Default instrument => Picker : seeds Stage 3 and Spot Read
|
||||||
|
Enable i1Pro 2 LEDs => Toggle
|
||||||
|
settingsDeltaEGood => TextField
|
||||||
|
settingsDeltaEWarning => TextField : must be greater than Good
|
||||||
|
settingsCalStaleDays => TextField
|
||||||
|
Install location => Picker User or System
|
||||||
|
Ask before overwriting => Toggle
|
||||||
|
Open ColorSync after install => Toggle
|
||||||
|
Log level => Picker
|
||||||
|
Open log folder => Button
|
||||||
|
Cancel / Save => Save stays if validation fails
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Presets PresetDialogs.swift" as pre {
|
||||||
|
savePresetName => TextField
|
||||||
|
savePresetDesc => TextField
|
||||||
|
btnCloseSavePresetDialog => Button
|
||||||
|
btnConfirmSavePreset => Button : disabled if name empty
|
||||||
|
presetRow-id => Row : built-in cannot delete
|
||||||
|
btnImportPreset => Button
|
||||||
|
btnExportActivePreset => Button : custom selected
|
||||||
|
btnCloseManagePresetsDialog => Button
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Media MediaLibraryDialogs.swift" as media {
|
||||||
|
saveMediaName/Paper/Ink => TextField
|
||||||
|
saveMediaApplyCal => Toggle : disabled if CAL_ or missing file
|
||||||
|
btnConfirmSaveMedia => Button : name+paper+ink required
|
||||||
|
mediaLibraryList => List
|
||||||
|
btnMediaLibraryApply-id => Button per row
|
||||||
|
manageMediaNotice => Caption : failed Apply in-sheet
|
||||||
|
btnMediaLibraryApply => Button : disabled if no selection
|
||||||
|
btnCloseManageMediaDialog => Button
|
||||||
|
}
|
||||||
|
|
||||||
|
map "spotReadView SpotReadView.swift" as spot {
|
||||||
|
btnSpotDetectInstruments => Button
|
||||||
|
spotInstrumentSelect => Picker : disabled while running
|
||||||
|
spotSetDefault => Toggle
|
||||||
|
btnSpotStart => Button : needs sidecar, cwd, not chartread
|
||||||
|
btnSpotCalibrate => Button : calibrating
|
||||||
|
btnSpotTrigger => Button : Read, awaitingStrip
|
||||||
|
btnSpotStop => Button : while running
|
||||||
|
btnSpotCopyLab => Button : disabled if no sample
|
||||||
|
btnSpotExportCsv => Button : disabled if no history
|
||||||
|
btnCloseSpotRead => Button Esc : dismiss = Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
map "gamutView GamutView.swift" as gam {
|
||||||
|
gamutLayer-srgb => Toggle : can hide, cannot remove
|
||||||
|
gamutLayer-profile => Toggle : disabled if no .gam
|
||||||
|
gamutLayer-compare => Toggle : disabled if no compare
|
||||||
|
btnGamutAddCompare => Menu
|
||||||
|
btnGamutRemoveCompare => Button
|
||||||
|
btnGamutSampleTiff => Button
|
||||||
|
btnResetGamutCamera => Button R
|
||||||
|
gamutLabEntryL/A/B => TextField
|
||||||
|
btnGamutInspectLab => Button
|
||||||
|
btnCloseGamut => Button Esc
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Project alerts" as proj {
|
||||||
|
projectNewAlert => Alert : New Cancel / Confirm
|
||||||
|
Dirty save => Alert : Save / Don't Save / Cancel
|
||||||
|
projectRelocateSheet => Sheet : cwd missing on Open
|
||||||
|
}
|
||||||
|
|
||||||
|
set -[hidden]down- pre
|
||||||
|
pre -[hidden]down- media
|
||||||
|
media -[hidden]down- spot
|
||||||
|
spot -[hidden]down- gam
|
||||||
|
gam -[hidden]down- proj
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
@startuml ICCery-UI-shell
|
||||||
|
title Shell, sidebar, stepper, File menu
|
||||||
|
!theme plain
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam defaultFontColor black
|
||||||
|
skinparam defaultFontSize 13
|
||||||
|
skinparam mapBackgroundColor white
|
||||||
|
skinparam mapBorderColor #222222
|
||||||
|
skinparam mapFontColor black
|
||||||
|
skinparam arrowColor #222222
|
||||||
|
skinparam shadowing false
|
||||||
|
hide circle
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
map "Window" as win {
|
||||||
|
WindowGroup => ICCeryApp.swift : 1280x800 min 1100x700
|
||||||
|
RootView => RootView.swift : hosts every sheet
|
||||||
|
noticeText => NoticeBanner : shown if wizard.notice set; auto-hide 6s
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Sidebar header" as hdr {
|
||||||
|
openSettingsBtn => Button : always / Settings sheet
|
||||||
|
openAboutBtn => Button : always / About
|
||||||
|
btnToggleAllHelp => Button : toggles yellow help dots
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Presets" as pre {
|
||||||
|
presetSelect => Picker.menu : none + preset id; none is not factory reset
|
||||||
|
btnSavePresetModal => Button : always
|
||||||
|
btnOpenPresetsDialog => Button : always
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Media library" as med {
|
||||||
|
mediaSelect => Picker.menu : never reuses presetSelect
|
||||||
|
mediaRecipeStale => Caption : shown if staleReasons nonempty
|
||||||
|
btnMediaLibraryCapture => Button : disabled if no Stage 2 printer
|
||||||
|
btnMediaLibraryManage => Button : always
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Studio (not stepper)" as stu {
|
||||||
|
btnCalibratePrinter => Button : always; Stage 0
|
||||||
|
btnViewGamut => Button : sidebar always on; same id as Stage 5
|
||||||
|
btnSpotRead => Button : disabled if no cwd or chartread running
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Stepper 1-5 disk is truth" as stp {
|
||||||
|
Stage1 Generate => always
|
||||||
|
Stage2 Lay out => .ti1
|
||||||
|
Stage3 Measure => .ti1 AND .ti2
|
||||||
|
Stage4 Build => .ti3 (not .ti2 alone)
|
||||||
|
Stage5 Verify => .ti3 AND .icc/.icm
|
||||||
|
Calibrate => not a stepper row; always available
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Project chip" as chip {
|
||||||
|
projectChipName => Text : bound name or No project
|
||||||
|
projectChipPath => Text : shown if bound
|
||||||
|
projectChipStale => Caption : diskBehindNotes
|
||||||
|
btnProjectReveal => Button : shown if bound
|
||||||
|
btnProjectSave => Button : canSave = bound + basename + cwd
|
||||||
|
btnProjectOpen => Button : shown if not bound
|
||||||
|
}
|
||||||
|
|
||||||
|
map "File menu" as menu {
|
||||||
|
menuProjectNew => Button Cmd-N
|
||||||
|
menuProjectOpen => Button Cmd-O
|
||||||
|
menuProjectRecents => Menu : disabled if recents empty
|
||||||
|
menuProjectSave => Button Cmd-S : !canSave
|
||||||
|
menuProjectSaveAs => Button Shift-Cmd-S : basename + cwd
|
||||||
|
menuProjectReport => Button : !canReport
|
||||||
|
menuProjectClose => Button : !isBound
|
||||||
|
}
|
||||||
|
|
||||||
|
win -[hidden]down- hdr
|
||||||
|
hdr -[hidden]down- pre
|
||||||
|
pre -[hidden]down- med
|
||||||
|
med -[hidden]down- stu
|
||||||
|
stu -[hidden]down- stp
|
||||||
|
stp -[hidden]down- chip
|
||||||
|
chip -[hidden]down- menu
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
@startuml ICCery-UI-stage1-2
|
||||||
|
title Stages 1-2 Generate Target / Lay Out and Print
|
||||||
|
!theme plain
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam defaultFontColor black
|
||||||
|
skinparam defaultFontSize 13
|
||||||
|
skinparam mapBackgroundColor white
|
||||||
|
skinparam mapBorderColor #222222
|
||||||
|
skinparam mapFontColor black
|
||||||
|
skinparam arrowColor #222222
|
||||||
|
skinparam shadowing false
|
||||||
|
hide circle
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
map "stage-1 Stage1View.swift" as s1 {
|
||||||
|
colourSpace => Picker.segmented : RGB or CMYK
|
||||||
|
patchCountPreset => Picker
|
||||||
|
patchCountCustom => TextField : shown if preset is custom
|
||||||
|
whitePatches => Stepper 0-50
|
||||||
|
blackPatches => Stepper 0-50
|
||||||
|
targetBasename => TextField
|
||||||
|
btnBrowse => Button
|
||||||
|
btnSelectWorkDir => Button
|
||||||
|
btnOpenExisting => Button : .ti1 or .ti2
|
||||||
|
btn-import-dataset => Button : unlocks stage 4
|
||||||
|
selectedPathDisplay => Text
|
||||||
|
targenAdvancedDetails => DisclosureGroup : UI tests pre-expand
|
||||||
|
btnGenerate => Button : needs basename AND directory
|
||||||
|
targenLog => ProcessLogView
|
||||||
|
}
|
||||||
|
|
||||||
|
map "Stage 1 Advanced (inside disclosure)" as adv {
|
||||||
|
targenGreySteps => Toggle+field : field iff on
|
||||||
|
targenSingleChannelSteps => Toggle+field
|
||||||
|
targenNeutralSteps => Toggle+field
|
||||||
|
targenNeutralConcentration => Toggle+Slider 0-1
|
||||||
|
targenAdaptation => Toggle+Slider 0-1
|
||||||
|
targenPrecondProfile => TextField
|
||||||
|
btnBrowsePrecondProfile => Button
|
||||||
|
targenHighQuality => Toggle
|
||||||
|
targenAlgorithm => Picker
|
||||||
|
targenInkLimitGroup => Toggle+field : CMYK only
|
||||||
|
targenDarkEmphasis => Toggle+Slider 0-3
|
||||||
|
targenDevicePower => Toggle+Slider 0-3
|
||||||
|
}
|
||||||
|
|
||||||
|
map "stage-2 Stage2View.swift" as s2 {
|
||||||
|
cmWarningBanner => Banner : always on stage 2
|
||||||
|
instrumentSelect => Picker : chart code, not USB port
|
||||||
|
pageSizeSelect => Picker
|
||||||
|
customPageW / customPageH => TextField : pageSize custom
|
||||||
|
tiffDpi => Stepper 72-600
|
||||||
|
printtargLayoutOrder => Picker
|
||||||
|
printtargCustomSeed => TextField : custom seed order
|
||||||
|
btnToggleLabelEdit => Button
|
||||||
|
targetMetadataPrinter => TextField
|
||||||
|
targetMetadataInkSet => TextField
|
||||||
|
targetMetadataDriverPaper => TextField
|
||||||
|
targetMetadataActualPaper => TextField
|
||||||
|
targetLabelPreview => TextField or Text
|
||||||
|
btnCreateLayout => Button : disabled if no basename
|
||||||
|
tiffGallery => Grid : after printtarg
|
||||||
|
btnPrintPage-N => Button : disabled if no printer
|
||||||
|
}
|
||||||
|
|
||||||
|
map "rawPrintPanel" as prn {
|
||||||
|
printerSelect => Picker CUPS
|
||||||
|
printerStatusBadge => Caption
|
||||||
|
btnRefreshPrinters => Button
|
||||||
|
btnPrinterProperties => Button : disabled if no printer
|
||||||
|
printerTraySelect => Picker : if trays exist
|
||||||
|
printerMediaTypeSelect => Picker : if media types exist
|
||||||
|
btnOrientPortrait => Button
|
||||||
|
btnOrientLandscape => Button
|
||||||
|
btnPrintAll => Button : needs layout AND printer
|
||||||
|
btnAdvanceToStage3 => Button : needs .ti2 unlock
|
||||||
|
printNotificationText => Caption : if printNotice set
|
||||||
|
}
|
||||||
|
|
||||||
|
s1 -[hidden]down- adv
|
||||||
|
adv -[hidden]down- s2
|
||||||
|
s2 -[hidden]down- prn
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
@startuml ICCery-UI-stage3-5-cal
|
||||||
|
title Stages 3-5 and Calibrate Printer
|
||||||
|
!theme plain
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam defaultFontColor black
|
||||||
|
skinparam defaultFontSize 13
|
||||||
|
skinparam mapBackgroundColor white
|
||||||
|
skinparam mapBorderColor #222222
|
||||||
|
skinparam mapFontColor black
|
||||||
|
skinparam arrowColor #222222
|
||||||
|
skinparam shadowing false
|
||||||
|
hide circle
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
map "stage-3 Stage3View.swift" as s3 {
|
||||||
|
btnDetectInstruments => Button : disabled while detecting
|
||||||
|
chartreadInstrumentSelect => Picker.menu
|
||||||
|
xyTableHint => Caption : if XY instrument
|
||||||
|
xyTablePanel => Place/Align/Scan/Remove
|
||||||
|
chartreadPrompt => Text
|
||||||
|
chartreadLastError => Caption : if notice set
|
||||||
|
btnStartRead => Button : shown if not running; needs basename+cwd
|
||||||
|
btnCalibrate => Button : running and calibrating
|
||||||
|
btnTrigger => Button : running and awaitingStrip
|
||||||
|
btnDoneReadEarly => Button : awaitingStrip
|
||||||
|
btnAccept => Button : place / align / continue / warning
|
||||||
|
btnRetry => Button : error state
|
||||||
|
btnDoneRead => Button : allStripsRead
|
||||||
|
btnCancel => Button : while running
|
||||||
|
swatchGrid => swatch-rowId-loc
|
||||||
|
btnMeasureAnotherSheet => Button : after a finished pass
|
||||||
|
btnFinishAndAverage => Button : finished AND at least one pass
|
||||||
|
}
|
||||||
|
|
||||||
|
map "stage-4 Stage4View.swift" as s4 {
|
||||||
|
colprofAlgorithm => Picker
|
||||||
|
colprofQuality => Picker
|
||||||
|
colprofFwa => Picker
|
||||||
|
colprofFwaCustomPath => TextField : custom spectrum
|
||||||
|
btnBrowseFwaSp => Button
|
||||||
|
colprofIlluminant => TextField
|
||||||
|
colprofObserver => TextField
|
||||||
|
colprofInputViewCond => TextField
|
||||||
|
colprofOutputViewCond => TextField
|
||||||
|
colprofDescription => TextField
|
||||||
|
colprofCopyright => TextField
|
||||||
|
colprofApplyCalibration => Toggle
|
||||||
|
colprofCalibrationFile => TextField : if applyCalibration
|
||||||
|
btnBrowseCalibrationFile => Button : if applyCalibration
|
||||||
|
btnCreateProfile => Button : basename + cwd, not running
|
||||||
|
}
|
||||||
|
|
||||||
|
map "stage-5 Stage5View.swift" as s5 {
|
||||||
|
btnVerifyProfile => Button : needs created profile
|
||||||
|
btnViewGamut => Button : disabled if no .gam (same id as sidebar)
|
||||||
|
btnInstallProfile => Button : needs created profile
|
||||||
|
driftPrinterFilter => Picker
|
||||||
|
btnExportHistory => Button
|
||||||
|
btnClearHistory => Button
|
||||||
|
verificationHistoryTable => Table
|
||||||
|
driftChart => not interactive
|
||||||
|
profileOverwriteBtn => Alert : install collision
|
||||||
|
profileRenameBtn => Alert
|
||||||
|
profileCancelCollisionBtn => Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
map "stage-cal CalibrationView.swift" as cal {
|
||||||
|
Colour space => Picker.segmented : no a11y id
|
||||||
|
calSteps => TextField
|
||||||
|
White patches => TextField : no a11y id
|
||||||
|
calInkExplore => TextField : CMYK only
|
||||||
|
calNeutralEmphasis => Toggle
|
||||||
|
btnCalGenerate => Button : needs basename + cwd
|
||||||
|
btnCalLayout => Button : same
|
||||||
|
btnCalMeasure => Button : needs .ti3
|
||||||
|
btnCalCompute => Button : .ti3 and not computing
|
||||||
|
calApplyToggle => Toggle : after curves exist
|
||||||
|
btnCalReturn => Button Esc : restores original basename
|
||||||
|
}
|
||||||
|
|
||||||
|
s3 -[hidden]down- s4
|
||||||
|
s4 -[hidden]down- s5
|
||||||
|
s5 -[hidden]down- cal
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Interactive UI map
|
||||||
|
|
||||||
|
PlantUML **maps** (not class diagrams). Class diagrams laid out 6500px wide, so Gitea on a phone showed a sliver of a black arrow and the UML “C” icon.
|
||||||
|
|
||||||
|
Each file is one stacked column (~560–780px) that Kroki actually fits in the page.
|
||||||
|
|
||||||
|
| Diagram | File | Kroki size |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Shell, sidebar, stepper, File menu | [ui-interactive-map-shell.puml](ui-interactive-map-shell.puml) | 563 × 1367 |
|
||||||
|
| Stages 1–2 | [ui-interactive-map-stage1-2.puml](ui-interactive-map-stage1-2.puml) | 625 × 1366 |
|
||||||
|
| Stages 3–5 + Calibrate | [ui-interactive-map-stage3-5-cal.puml](ui-interactive-map-stage3-5-cal.puml) | 568 × 1386 |
|
||||||
|
| Sheets | [ui-interactive-map-sheets.puml](ui-interactive-map-sheets.puml) | 782 × 1502 |
|
||||||
Reference in New Issue
Block a user