Gamut compare: second profile, reference spaces, click-inspect #147
Closed
opened 2026-09-12 18:34:18 +01:00 by gronod
·
2 comments
No Branch/Tag Specified
main
docs/27-roadmap-candidates
develop
milestone/m13-print-ticket
feat/218-paper-source
feat/217-panel-ticket-restore
bugfix/211-print-output-flip
milestone/m12-native-spool
feat/202-airprint-detection
feat/201-native-print-operation
feat/201-raster-canvas
feat/201-ticket-capture
feat/201-pm-ticket-bridge
chore/m11-sync-develop
milestone/m11-print-settings
chore/m11-docs-hygiene
feat/186-panel-binding
feat/181-canon-media-locale
feat/180-epson-qual-options
feat/183-stage2-print-settings
chore/m11-tracking
docs/ui-interactive-map
v2.0.4-prerelease-docs27
v2.0.4
v2.0.3
v2.0.3-prerelease
v2.0.2
v2.0.1
v2.0.0
Labels
Clear labels
Bug/Architecture
Bug/Backend
Bug/DevOps
Bug/UI
Compat/Breaking
Feature/Architecture
Feature/Backend
Feature/DevOps
Feature/UI
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Project/ICCery-v2
Core / wizard / process infra defect
Argyll orchestration, file/data defect
Build, packaging, CI defect
Views, controls, dialogs defect
Breaking change that won't be backward compatible
Core / wizard / process infra
Argyll orchestration, file/data engines
Build, packaging, CI
Views, controls, dialogs
Something is not working
Documentation changes
Improve existing functionality
New functionality
This is security issue
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Low
4
The priority is low
Priority
Medium
3
The priority is medium
ICCery v2 Mac rewrite
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
3
This issue won't be fixed
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
Milestone
No items
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: gronod/iccery-v2-mac#147
Reference in New Issue
Block a user
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.
Summary
GamutViewModelondeveloploads bundledsRGB.gamplus at most one profile.gamnext to the ICC (Sources/ICCery/GamutViewModel.swift). Make the viewer a comparison tool: second profile or extra reference mesh, volume / containment readout, click a vertex or drop a Lab / TIFF sample and see in/out of which hulls.Spec refs
docs/18-gamut-viewer.md(axes X=a* ±128, Y=L* 0–100, Z=b* ±128, camera home (180,120,180) lookAt (0,50,0), R resets on a focusable container, native faces > hull)docs/04-argyll-binaries.md§10iccgamut -v -d 10(density, not a directory)BinaryResolver.referenceGamut(_:)GamutMesh/GamutMeshParsericcgamutfailure is info, not fatal)GamutViewScope
In:
.gamis shipped (never an 8-cusp stub)ICCeryCore(no new CMM)Out:
cctiffticket)AdobeRGB.gamat runtime — ship the file or omit the presetImplementation notes
Core
Point-in-mesh uses the same Lab→SIMD3 mapping as
GamutVertex.position(x=a*,y=L*,z=b*). Use the parser’s existing hull fallback when faces are missing. Do not invent Delaunay.Volume: sum signed tetrahedra from centroid to each face; expose Lab-cubic units in the status line only. A “percent of sRGB” ratio is allowed only when both volumes are finite and > 0.
Runner
If the user picks an
.icc/.icmthat has no sibling.gam, call existingArgyllRunner.runIccgamut(config: IccgamutConfig(profileURL:))with-d 10, cwd = parent, idiccgamut_{stem}. Non-zero or missing.gam→ info banner, mesh omitted, other meshes stay (parity with #24).Add
FileDialogService.selectGamutFile(open.gam) and reuseselectProfileFile(.icc/.icm/.mpp, not.ti*, #172). One picker per purpose — do not shareselectDatasetFile.View model
Replace the two-optional-mesh shape:
Load sRGB on init via
BinaryResolver.referenceGamut("sRGB")as today. “Add profile…” appends profile B; adding a third profile replaces B, never sRGB. Parse off-main viaTask.detachedas today.Click: convert SceneKit hit to Lab (
x→a*,y→L*,z→b*). Show Lab* + per-layer inside/outside + vertex RGB already stored onGamutVertex.rgb.Optional “Sample TIFF”: host-side TIFF→PNG already exists (
readTiffPreviewPng, #58). Pick a pixel, convert sRGB 8-bit → Lab D50 with a documented approximate matrix helper inICCeryCorelabelled approximate, not ColorSync. Do not call a CMM.SceneKit
Keep one
SCNView. Distinct geometries per layer, vertex colours fromGamutVertex.rgb, profile B wireframe or ~30% opacity so overlaps read. Pause render loop on disappear (already there). R still resets camera on the focusable container — do not port the v1 dead-key trap.Intel / context-lost: if
SCNViewfails to create a context, setgamutViewerUnavailableand show thedocs/18fallback banner. Do not respawn the view in a loop.UI
Stay inside the existing
GamutViewsheet. Do not add a second viewer, do not embed SceneKit in Stage 5 itself, and do not put layer chrome in the 270 pt sidebar.btnViewGamutalready exists in bothSidebarViewandStage5Viewand already opensRootView’s.sheet→GamutView(profileGamURL:). That entry path stays.Existing identifiers that must keep working for
Milestone6GamutUITests:btnViewGamutgamutView(containment on the whole sheet)gamutStatusTextbtnResetGamutCameraDo not rename
btnResetGamutCameratobtnGamutReset. The short list in the first draft of this ticket is wrong; use the ids already ondevelop.Sheet chrome
GamutViewis currently aZStack(scene + floating Reset + floating status), min 500×400. Grow it:.frame(minWidth: 720, minHeight: 520)so the layer row and inspect panel fit without covering the meshTheme.backgroundaround the chrome; keep the SceneKit clear colourNSColor(red: 0.055, green: 0.055, blue: 0.078, alpha: 1)(already inGamutSceneView)RootView. Do not present fromSidebarVieworStage5View(clips, double sheets)Layout, top to bottom:
GamutSceneView, still.focusable()so R worksgamutStatusText, extend the stringReset stays as the existing top-trailing
ButtonbtnResetGamutCamera. Do not add a second Reset in the toolbar.Toolbar — layer toggles
A leading
HStackofToggles, one perNamedGamutinlayers, bound tovisibleIDs.gamutLayer-sRGBText("sRGB")gamutLayer-profileText(profileDisplayName)orText("Profile")gamutLayer-compareText(compareDisplayName)orText("Compare").toggleStyle(.checkbox)so they read as the v1 layer list, not as switches.SCNNode; it does not unload theGamutMesh.Textonly (#114). Never put the raw path in the toggle label; put the filename in.help.Buttons on the trailing side of the same toolbar:
Add compare…btnGamutAddCompare— always enabled. Opens an.actionSheet/ menu with two items:Open .gam…btnGamutOpenGam→FileDialogService.selectGamutFileOpen profile…btnGamutOpenProfile→FileDialogService.selectProfileFilethenrunIccgamutif no sibling.gamRemove comparebtnGamutRemoveCompare— enabled only when layer B exists. Removes B, leaves A + sRGB.Sample TIFF…btnGamutSampleTiff— optional, enabled whenever the sheet is open. See Inspect.Adding a third profile replaces B. Do not grow the toolbar to four checkboxes. Show a one-line info notice inside the sheet (
gamutNoticeText) “Compare slot holds one profile. The previous compare mesh was replaced.” — not the mainNoticeBanner, which sits behind the sheet.If
iccgamutfails or the.gamparse fails: keep sRGB + A, setgamutNoticeTextto the existing #24 info copy, leavegamutLayer-comparehidden/disabled. Do not close the sheet.Status line
Keep the floating caption id
gamutStatusText. ExtendviewModel.statusto one line, caption font,Theme.text:Rules:
volclause rather than printingvol n/aMilestone6GamutUITestsalready asserts this identifier exists and is non-empty. Keep it non-empty even with only sRGB (current behaviour)Inspect panel
A bottom bar, id
gamutInspectPanel, min height 56,Theme.panelbackground. Hidden is not allowed — show the idle copy so the sheet height is stable (helps UI tests and avoids SceneKit resize storms).Idle:
Text("Click the mesh, or enter Lab, to inspect.")idgamutInspectIdleAfter a sample:
gamutInspectLTextof one decimalgamutInspectATextof one decimalgamutInspectBTextof one decimalgamutInspect-sRGBText("sRGB in")/Text("sRGB out")/Text("sRGB ?")gamutInspect-profilegamutInspect-comparegamutInspectSwatchColorfromGamutVertex.rgbwhen the hit was a vertex; otherwise the approximate sRGB of the typed LabClick path:
SCNView. If the hit node is a gamut mesh vertex or face, convert position with the existing mappingx→a*,y→L*,z→b*and publishinspectLab.onTapGestureon theZStackthat eats SceneKit orbit drags.Manual Lab path — compact fields in the same panel, not a separate dialog:
TextFieldsgamutLabEntryL/gamutLabEntryA/gamutLabEntryB, width ~56 eachInspectbuttonbtnGamutInspectLab— disabled while any field is non-numeric?if the point is outside every hullTIFF path (
btnGamutSampleTiff):readTiffPreviewPng(#58). Show a small preview sheetgamutTiffPreviewwith the PNG and a click-to-sample cursorText("approx. Lab, not ColorSync")idgamutInspectApproxwhenever the sample came from TIFF or from typed RGB-less LabbtnCloseGamutTiffPreview. No CMM, noNSImageas the 3D sourceScene behaviour the UI must not break
SCNView. Profile B is wireframe or ~30% opacity (pick one and keep it; do not toggle both). sRGB stays the current reference look. A stays solid vertex-coloured.onKeyPress(.init("R"))already inGamutView). Reset button staysbtnResetGamutCamera..onDisappear { pause() }stays. Do not start the render loop inRootView.onAppear.SCNViewcannot create a context: hideGamutSceneView, showTextfromdocs/18fallback, idgamutViewerUnavailable. Do not rebuild the representable in a loop. Layer toggles stay visible but disabled.Help overlays
Wire
helpOverlayonbtnViewGamut(already has a label),btnGamutAddCompare,btnGamutSampleTiff, and the inspect panel. Do not attach a help overlay toGamutSceneView— it steals first-responder from R.Stage 5 / sidebar
No new sidebar controls in this ticket. #146 may already have added a Media block; this ticket must not insert anything between Media and Calibrate / View Gamut.
btnViewGamutremains enabled even when no profile.gamexists (today it still opens sRGB-only). Keep that: compare is how the user loads a second mesh without finishing Stage 5.Stage 5 may keep its own
btnViewGamut. Do not give it a second identifier such asbtnViewGamutStage5—Milestone6GamutUITestsuseswaitFor("btnViewGamut").UI tests
Keep
Milestone6GamutUITestsgreen. AddMilestone10GamutCompareUITests(no GPU assertion beyond “sheet opened”):testLayerTogglesExistWithSRGB—gamutLayer-sRGBexists and is on;gamutLayer-compareexists and is disabled before a compare loadtestAddCompareButtonExists—btnGamutAddCompareexists; cancel of the file dialog leaves sRGB status non-emptytestInspectPanelIdleStableHeight—gamutInspectPanelexists before any click; sheet still hasgamutStatusTexttestResetIdentifierUnchanged—btnResetGamutCamerastill existsIdentifiers (complete)
Rewrite invariants
iccgamut -dis density 10. Nevercolprof -u. Never feed TIFF toNSImageas the 3D source. User-supplied profile names viaText. AGPL: spawniccgamutonly.Dependencies
Blocks-on: #28 viewer, #24 iccgamut, #6 dialogs, #3 reference asset.
Unblocks: none.
Test
GamutMeshParserfixtures unchangedsRGB.gam.gamleaves sRGB layerMilestone10GamutCompareUITests: layer toggles, Add compare, idle inspect panel, Reset id unchanged (no SceneKit click in CI).gamoverlays; click-inspect a vertex; no GPU hang on leave/reenterAcceptance criteria
BinaryResolver.referenceGamut("sRGB")gamutView,gamutStatusText,btnResetGamutCamera,btnViewGamutunchangedGamutViewsheet, not the sidebarMilestone6GamutUITestsstay greenfeat/<this>-gamut-compare→milestone/m10-studio→developM10 implementation order (see milestone): 3 of 4. May develop in parallel with #148; merge after #148 to keep sidebar/a11y diffs serial.
Implementation landed in PR #153 at
398d9b4756on milestone/m10-studio (merged as99c0d7e).Acceptance evidence:
BinaryResolver.referenceGamut("sRGB")—GamutViewModelseeds the reference layer from it.btnGamutAddCompareoffers.gam/ profile load; failure surfaces as info text (gamutNoticeText), never a crash.gamutLabEntry{L,A,B}/btnGamutInspectLabwrite intogamutInspectPanelwith per-layer containment (GamutContainment, Möller–Trumbore + signed-tetra volume).isPlayingtoggled in coordinator).ApproximateLabsRGB8→Lab D50.gamutView,gamutStatusText,btnResetGamutCamera,btnViewGamut.GamutViewsheet toolbar (gamutLayer-*toggles,btnGamutAddCompare), not the sidebar.Milestone6GamutUITestsgreen (testViewGamutOpensSceneKitSheetpassed 19.171 s);Milestone10GamutCompareUITests7/7 green.Verification at milestone/m10-studio
c39afe0dfd(x86_64):xcodebuild test -scheme ICCery -destination 'platform=macOS' -derivedDataPath build/DerivedData-m10-final ARCHS=x86_64 CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-'Hardware item (two real
.gamfiles, click-inspect, no GPU hang) remains a milestone gate (Phase 8), not this issue's merge gate.Risk register rows proven at this SHA: R16 (SceneKit ids stable under compare chrome).
Closing manually only after code and tests are present on the milestone branch.