Files
iccery-v2-mac/Sources/ICCery/GamutViewModel.swift
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 0ee609be1c feat(#28): SceneKit gamut viewer
- Add native .gam mesh parser and mesh model (GamutMeshParser, GamutMesh).
- Add GamutView / GamutViewModel with SceneKit, bundled sRGB.gam reference,
  and profile-gamut comparison.
- Wire Stage 5 "View Gamut" button and sidebar quick action.
- Extract a best-effort .gam mesh after colprof via Iccgamut in
  ProfileWorkflowViewModel, with artefact gating for the generated .gam.
- Add BinaryResolver.referenceGamut() for bundled reference gamuts.
- Add Milestone6GamutUITests and GamutMeshParserTests.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-09 22:06:19 +01:00

58 lines
1.7 KiB
Swift

import Foundation
import ICCeryCore
import Observation
/// View model for the native SceneKit gamut viewer.
///
/// Loads the bundled `sRGB.gam` reference immediately and, optionally, a
/// printer/profile `.gam` from the current working directory.
@MainActor
@Observable
final class GamutViewModel {
/// Parsed reference sRGB gamut mesh.
var sRGBMesh: GamutMesh?
/// Parsed printer/profile gamut mesh.
var profileMesh: GamutMesh?
/// User-facing status line.
var status = "Loading gamut…"
/// Closure injected into the SceneKit view to request a camera reset.
var resetCamera: () -> Void = {}
private let profileGamURL: URL?
init(profileGamURL: URL? = nil) {
self.profileGamURL = profileGamURL
Task { await load() }
}
private func load() async {
do {
let referenceURL = BinaryResolver().referenceGamut("sRGB")
let reference = try await parse(url: referenceURL)
sRGBMesh = reference
if let profileGamURL {
let profile = try await parse(url: profileGamURL)
profileMesh = profile
status = "Profile gamut (\(profile.faces.count) faces) vs sRGB reference"
} else {
status = "sRGB reference gamut (\(reference.faces.count) faces)"
}
} catch {
status = "Could not load gamut: \(error.localizedDescription)"
}
}
/// Parses a `.gam` file off the main actor so large meshes do not stall
/// the UI.
private func parse(url: URL) async throws -> GamutMesh {
try await Task.detached {
try GamutMeshParser.parse(url: url)
}.value
}
}