Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
201 lines
6.7 KiB
Swift
201 lines
6.7 KiB
Swift
import Combine
|
|
import Foundation
|
|
import SwiftUI
|
|
import ICCeryCore
|
|
|
|
/// Stage 0 calibration workflow: generate wedge, print, measure, and
|
|
/// compute `.cal` curves.
|
|
@MainActor
|
|
final class CalibrationViewModel: ObservableObject {
|
|
|
|
let workflow: TargetWorkflowViewModel
|
|
let profile: ProfileWorkflowViewModel
|
|
let environment: AppEnvironment
|
|
|
|
// MARK: - Form state
|
|
|
|
@Published var colourSpace: ColourSpace = .cmyk
|
|
@Published var steps: Int = 21
|
|
@Published var whitePatches: Int = 4
|
|
@Published var includeNeutralEmphasis: Bool = false
|
|
@Published var inkLimit: String = "320"
|
|
@Published var applyToProfile: Bool = false
|
|
@Published var computedCalURL: URL?
|
|
@Published var calibrationLog: [String] = []
|
|
@Published var isGenerating = false
|
|
@Published var isComputing = false
|
|
|
|
init(workflow: TargetWorkflowViewModel, profile: ProfileWorkflowViewModel, environment: AppEnvironment) {
|
|
self.workflow = workflow
|
|
self.profile = profile
|
|
self.environment = environment
|
|
}
|
|
|
|
private var wizard: WizardViewModel { workflow.wizard }
|
|
|
|
// MARK: - Derived
|
|
|
|
var canGenerate: Bool {
|
|
!wizard.basename.isEmpty && wizard.effectiveWorkingDirectory != nil && !isGenerating
|
|
}
|
|
|
|
var canCompute: Bool {
|
|
calibrationTi3URL != nil && !isComputing
|
|
}
|
|
|
|
var calibrationTi3URL: URL? {
|
|
guard let cwd = wizard.effectiveWorkingDirectory else { return nil }
|
|
return cwd.appendingPathComponent("\(calBasename).ti3")
|
|
}
|
|
|
|
private var identity: CalibrationIdentity {
|
|
CalibrationIdentity.parse(
|
|
liveBasename: wizard.basename,
|
|
persistedOriginal: wizard.calibrationOriginalBasename
|
|
)
|
|
}
|
|
|
|
private var calBasename: String { identity.calibrationBasename }
|
|
|
|
private var calOutputURL: URL? {
|
|
guard let cwd = wizard.effectiveWorkingDirectory else { return nil }
|
|
return cwd.appendingPathComponent("\(calBasename).cal")
|
|
}
|
|
|
|
// MARK: - Generate calibration target
|
|
|
|
func generateTarget() {
|
|
guard canGenerate, let cwd = wizard.effectiveWorkingDirectory else { return }
|
|
// Snapshot the original (pre-CAL_) basename before changing the live one.
|
|
let identity = CalibrationIdentity.parse(
|
|
liveBasename: wizard.basename,
|
|
persistedOriginal: wizard.calibrationOriginalBasename
|
|
)
|
|
wizard.calibrationOriginalBasename = identity.originalBasename
|
|
wizard.basename = identity.calibrationBasename
|
|
wizard.sessionMode = .calibration
|
|
|
|
let config = CalibrationTargenConfig(
|
|
colourSpace: colourSpace,
|
|
steps: steps,
|
|
whitePatches: whitePatches,
|
|
includeNeutralEmphasis: includeNeutralEmphasis,
|
|
inkLimit: inkLimitValue,
|
|
basename: identity.originalBasename,
|
|
workingDirectory: cwd
|
|
)
|
|
|
|
Task { @MainActor in
|
|
do {
|
|
_ = try await ProcessRunSupport.runLogged(
|
|
setRunning: { self.isGenerating = $0 },
|
|
resetLog: { self.calibrationLog = [] },
|
|
onLog: { self.calibrationLog.append(contentsOf: $0) }
|
|
) { onLog in
|
|
try await self.environment.runner.runCalibrationTargen(
|
|
config: config, onLogBatch: onLog)
|
|
}
|
|
self.wizard.refreshGating()
|
|
self.wizard.showNotice("Calibration target generated.")
|
|
self.wizard.go(to: .layOutPrint)
|
|
} catch {
|
|
self.wizard.showNotice(
|
|
"Calibration target failed: \(error.localizedDescription)",
|
|
kind: .error
|
|
)
|
|
self.wizard.restoreCalibration()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Layout, print, measure
|
|
|
|
/// Hand off to the normal Stage 2/3 machinery using the `CAL_` basename.
|
|
/// After measurement, the user returns and presses Compute Curves.
|
|
func createLayout() {
|
|
wizard.sessionMode = .calibration
|
|
wizard.go(to: .layOutPrint)
|
|
}
|
|
|
|
func measureChart() {
|
|
wizard.sessionMode = .calibration
|
|
wizard.go(to: .measure)
|
|
}
|
|
|
|
// MARK: - Compute curves
|
|
|
|
func computeCurves() {
|
|
guard canCompute,
|
|
let cwd = wizard.effectiveWorkingDirectory,
|
|
let outputURL = calOutputURL else { return }
|
|
|
|
// Collision check: the Argyll `printcal` exit error contains
|
|
// "already exists" when the user declines overwrite. We do not
|
|
// silently clobber.
|
|
if FileManager.default.fileExists(atPath: outputURL.path) {
|
|
wizard.showNotice(
|
|
"\(outputURL.lastPathComponent) already exists. Rename or overwrite it first.",
|
|
kind: .error
|
|
)
|
|
return
|
|
}
|
|
|
|
let config = PrintcalConfig(
|
|
ti3Basename: calBasename,
|
|
workingDirectory: cwd,
|
|
outputURL: outputURL,
|
|
noInkLimit: false,
|
|
verify: false,
|
|
previousCalPath: nil,
|
|
totalInkLimit: inkLimitValue.map { Double($0) },
|
|
channelLimits: []
|
|
)
|
|
|
|
Task { @MainActor in
|
|
do {
|
|
let url = try await ProcessRunSupport.runLogged(
|
|
setRunning: { self.isComputing = $0 },
|
|
resetLog: { self.calibrationLog = [] },
|
|
onLog: { self.calibrationLog.append(contentsOf: $0) }
|
|
) { onLog in
|
|
try await self.environment.runner.runPrintcal(
|
|
config: config, onLogBatch: onLog)
|
|
}
|
|
self.computedCalURL = url
|
|
self.profile.calibrationFile = url.path
|
|
self.profile.applyCalibration = self.applyToProfile
|
|
self.wizard.showNotice("Calibration curves computed.")
|
|
self.wizard.restoreCalibration()
|
|
} catch {
|
|
self.wizard.showNotice(
|
|
"Calibration curve computation failed: \(error.localizedDescription)",
|
|
kind: .error
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Apply toggle
|
|
|
|
func updateApplyToProfile() {
|
|
profile.applyCalibration = applyToProfile
|
|
if applyToProfile, let url = computedCalURL {
|
|
profile.calibrationFile = url.path
|
|
} else if applyToProfile {
|
|
// User toggled on before computing; keep the path if already set.
|
|
} else {
|
|
profile.applyCalibration = false
|
|
}
|
|
}
|
|
|
|
func returnToProfiling() {
|
|
wizard.restoreCalibration()
|
|
wizard.go(to: .generate)
|
|
}
|
|
|
|
private var inkLimitValue: Int? {
|
|
guard colourSpace == .cmyk else { return nil }
|
|
return Int(inkLimit)
|
|
}
|
|
}
|