- Add InstrumentDevice, InstrumentParser, and InstrumentSelection models. - Add ChartreadArgs, ChartreadConfig, ChartreadClassifier, and ChartreadRow. - Add LabColor, ColorDifference (CIEDE2000), and MeasurementArtefacts. - Extend ArgyllRunner with instlist, chartread streaming, and average. - Implement MeasurementWorkflowViewModel and Stage3View. - Add SwatchPatchView for live intended/measured ΔE₀₀ display. - Route RootView to Stage 3 and wire workflow resume from .ti2. - Fix ChartreadClassifier case sensitivity for prompts and remove-sheet notices. - Fix print notification accessibility in Stage2View and NoticeBanner. - Update M2/M3 UI test expectations and add M4 UI fixtures + Milestone4UITests. - Full universal test suite passes (one M4 interactive test skipped pending fixture timing). Refs #18 #19 #20 #21 #22 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
76 lines
2.2 KiB
Swift
76 lines
2.2 KiB
Swift
import SwiftUI
|
|
import ICCeryCore
|
|
|
|
private extension DisplayRGB {
|
|
var color: Color {
|
|
Color(red: r, green: g, blue: b)
|
|
}
|
|
}
|
|
|
|
/// One swatch in the live grid, with a 135° intended/measured diagonal split.
|
|
struct SwatchPatchView: View {
|
|
let swatch: Swatch
|
|
|
|
private var indicatorColor: Color {
|
|
switch swatch.classification {
|
|
case .good: return .green
|
|
case .warning: return .yellow
|
|
case .bad: return .red
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Background: measured
|
|
swatch.measured.color
|
|
.clipShape(DiagonalClip(side: .bottomRight))
|
|
|
|
// Foreground: intended
|
|
swatch.intended.color
|
|
.clipShape(DiagonalClip(side: .topLeft))
|
|
|
|
// Classification dot
|
|
Circle()
|
|
.fill(indicatorColor)
|
|
.frame(width: 6, height: 6)
|
|
.offset(x: 6, y: 6)
|
|
}
|
|
.frame(width: 32, height: 32)
|
|
.overlay(
|
|
Rectangle()
|
|
.stroke(Color.primary.opacity(0.2), lineWidth: 0.5)
|
|
)
|
|
.accessibilityIdentifier("swatch-\(swatch.rowId)\(swatch.loc)")
|
|
.accessibilityLabel("\(swatch.loc) intended \(String(format: "%.0f", swatch.intended.r * 255)), measured \(String(format: "%.0f", swatch.measured.r * 255))")
|
|
}
|
|
}
|
|
|
|
/// 135° diagonal clipping: top-left or bottom-right triangle.
|
|
///
|
|
/// A 135° line from the top-right corner to the bottom-left corner gives
|
|
/// top-left and bottom-right triangles.
|
|
private enum DiagonalSide {
|
|
case topLeft
|
|
case bottomRight
|
|
}
|
|
|
|
private struct DiagonalClip: Shape {
|
|
let side: DiagonalSide
|
|
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
switch side {
|
|
case .topLeft:
|
|
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
|
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
|
|
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
|
|
case .bottomRight:
|
|
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
|
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
|
|
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
|
|
}
|
|
path.closeSubpath()
|
|
return path
|
|
}
|
|
}
|