Layered gamut model (NamedGamut): bundled sRGB reference + profile A + a single compare slot (profile B). Picking a third profile replaces B; remove-compare leaves sRGB + A.
GamutGeometry (ICCeryCore): Möller–Trumbore ray-cast containment over native .gam faces with off-axis retries on edge hits (in/out/? when a mesh has no faces), plus signed-tetrahedron volume from the vertex centroid. Status line prints per-layer Nv / Nf faces clauses and vol N% of sRGB only when both volumes are finite and positive.
Compare loading: Open .gam… via new FileDialogService.selectGamutFile; Open profile… reuses selectProfileFile, uses a sibling .gam when present, else runs bundled iccgamut -v -d 10 through ArgyllRunner.runIccgamut. Failure keeps existing layers and posts an in-sheet gamutNoticeText (#24 — never fatal).
Inspect panel (always visible, fixed min height): click a mesh (SceneKit hit-test in the coordinator, x→a*, y→L*, z→b*), or type Lab*; per-layer containment + swatch. ApproximateLab fixed-matrix sRGB8→Lab D50 path is labelled approx. Lab, not ColorSync — no CMM.
Sample TIFF…: host-side PNG preview via TiffPreview.previewPNG (#58), click a pixel → approximate Lab + swatch; sheet gamutTiffPreview / btnCloseGamutTiffPreview.
Compare B renders at ~30 % opacity; sRGB keeps the faint fill + edge lines look; profile A stays solid vertex-coloured. Layer toggles hide nodes, never unload or reset the camera; R/btnResetGamutCamera preserved; no-GPU fallback shows gamutViewerUnavailable without respawn loops.
Full suite (same destination): 400 ICCeryCoreTests + 45 ICCeryUITests = 445 tests, 0 failures. UI tests ran for real (no attach-skip).
Notes
Face-less meshes report containment unknown rather than inventing a hull (no QuickHull fallback in the Swift parser — documented in docs/18 §13).
docs/18-gamut-viewer.md and docs/21-ui-reference.md updated with the new identifier roster.
Refs #147
## What
- Layered gamut model (`NamedGamut`): bundled sRGB reference + profile A + a single compare slot (profile B). Picking a third profile replaces B; remove-compare leaves sRGB + A.
- `GamutGeometry` (ICCeryCore): Möller–Trumbore ray-cast containment over native `.gam` faces with off-axis retries on edge hits (`in`/`out`/`?` when a mesh has no faces), plus signed-tetrahedron volume from the vertex centroid. Status line prints per-layer `Nv / Nf faces` clauses and `vol N% of sRGB` only when both volumes are finite and positive.
- Compare loading: `Open .gam…` via new `FileDialogService.selectGamutFile`; `Open profile…` reuses `selectProfileFile`, uses a sibling `.gam` when present, else runs bundled `iccgamut -v -d 10` through `ArgyllRunner.runIccgamut`. Failure keeps existing layers and posts an in-sheet `gamutNoticeText` (#24 — never fatal).
- Inspect panel (always visible, fixed min height): click a mesh (SceneKit hit-test in the coordinator, x→a*, y→L*, z→b*), or type L*a*b*; per-layer containment + swatch. `ApproximateLab` fixed-matrix sRGB8→Lab D50 path is labelled `approx. Lab, not ColorSync` — no CMM.
- `Sample TIFF…`: host-side PNG preview via `TiffPreview.previewPNG` (#58), click a pixel → approximate Lab + swatch; sheet `gamutTiffPreview` / `btnCloseGamutTiffPreview`.
- Compare B renders at ~30 % opacity; sRGB keeps the faint fill + edge lines look; profile A stays solid vertex-coloured. Layer toggles hide nodes, never unload or reset the camera; `R`/`btnResetGamutCamera` preserved; no-GPU fallback shows `gamutViewerUnavailable` without respawn loops.
- Existing identifiers untouched: `btnViewGamut`, `gamutView`, `gamutStatusText`, `btnResetGamutCamera`.
## Tests
Targeted gate (xcodebuild test -scheme ICCery, ARCHS=x86_64, ad-hoc signed):
- ICCeryCoreTests: GamutContainmentTests 5, ApproximateLabTests 4, GamutViewModelTests 6, GamutMeshParserTests 8, GamutGeometryBuilderTests, IccgamutArgsTests 1 — all pass.
- ICCeryUITests: Milestone6GamutUITests 1, Milestone10GamutCompareUITests 7, AboutHelpUITests — 10 UI tests, all pass.
Full suite (same destination): 400 ICCeryCoreTests + 45 ICCeryUITests = 445 tests, 0 failures. UI tests ran for real (no attach-skip).
## Notes
- Face-less meshes report containment `unknown` rather than inventing a hull (no QuickHull fallback in the Swift parser — documented in docs/18 §13).
- `docs/18-gamut-viewer.md` and `docs/21-ui-reference.md` updated with the new identifier roster.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Refs #147
What
NamedGamut): bundled sRGB reference + profile A + a single compare slot (profile B). Picking a third profile replaces B; remove-compare leaves sRGB + A.GamutGeometry(ICCeryCore): Möller–Trumbore ray-cast containment over native.gamfaces with off-axis retries on edge hits (in/out/?when a mesh has no faces), plus signed-tetrahedron volume from the vertex centroid. Status line prints per-layerNv / Nf facesclauses andvol N% of sRGBonly when both volumes are finite and positive.Open .gam…via newFileDialogService.selectGamutFile;Open profile…reusesselectProfileFile, uses a sibling.gamwhen present, else runs bundlediccgamut -v -d 10throughArgyllRunner.runIccgamut. Failure keeps existing layers and posts an in-sheetgamutNoticeText(#24 — never fatal).ApproximateLabfixed-matrix sRGB8→Lab D50 path is labelledapprox. Lab, not ColorSync— no CMM.Sample TIFF…: host-side PNG preview viaTiffPreview.previewPNG(#58), click a pixel → approximate Lab + swatch; sheetgamutTiffPreview/btnCloseGamutTiffPreview.R/btnResetGamutCamerapreserved; no-GPU fallback showsgamutViewerUnavailablewithout respawn loops.btnViewGamut,gamutView,gamutStatusText,btnResetGamutCamera.Tests
Targeted gate (xcodebuild test -scheme ICCery, ARCHS=x86_64, ad-hoc signed):
Full suite (same destination): 400 ICCeryCoreTests + 45 ICCeryUITests = 445 tests, 0 failures. UI tests ran for real (no attach-skip).
Notes
unknownrather than inventing a hull (no QuickHull fallback in the Swift parser — documented in docs/18 §13).docs/18-gamut-viewer.mdanddocs/21-ui-reference.mdupdated with the new identifier roster.