spb/focale Public
Swift 100%
1//2// CaptureContext.swift3// Focale4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//8// The differentiating piece of the whole project (CLAUDE.md §4):9// everything we know about a photo at the moment the shutter fires,10// captured for free, with zero inference cost.11//1213import Foundation1415/// Context recorded at shutter time. Written both to the local index and16/// into the asset's own metadata, so the information survives an uninstall.17struct CaptureContext: Codable, Sendable {18 var recipe: Recipe.ID? // active preset19 var project: Project.ID? // project declared by the user20 var projectName: String? // denormalized for metadata embedding21 var subjectHint: String? // typed or dictated before/after the shot22 var settings: ManualSettings // ISO, shutter, lens, focus23 var scene: SceneSignal // luminance, motion, distance (LiDAR if available)24 var burstRole: BurstRole? // selection within a burst25 var place: PlaceRef? // place, never raw coordinates2627 /// Compact JSON used when embedding into asset metadata (EXIF UserComment).28 func metadataJSON() -> String? {29 let encoder = JSONEncoder()30 encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]31 guard let data = try? encoder.encode(self) else { return nil }32 return String(data: data, encoding: .utf8)33 }3435 static func decode(fromMetadataJSON json: String) -> CaptureContext? {36 guard let data = json.data(using: .utf8) else { return nil }37 return try? JSONDecoder().decode(CaptureContext.self, from: data)38 }39}4041/// Snapshot of the manual settings applied when the photo was taken.42struct ManualSettings: Codable, Sendable, Hashable {43 var iso: Float? // nil = auto44 var shutterSeconds: Double? // nil = auto45 var lensPosition: Float? // 0...1, nil = autofocus46 var whiteBalanceKelvin: Float? // nil = auto WB47 var exposureBias: Float48 var lens: LensKind49 var zoomFactor: Double50 var format: CaptureFormat51 /// Optional so recipes saved before this field existed still decode.52 /// nil = auto.53 var flashMode: FlashMode?5455 static let automatic = ManualSettings(56 iso: nil, shutterSeconds: nil, lensPosition: nil,57 whiteBalanceKelvin: nil, exposureBias: 0,58 lens: .wide, zoomFactor: 1.0, format: .heic, flashMode: nil59 )60}6162enum FlashMode: String, Codable, Sendable, CaseIterable {63 case auto, on, off6465 var displayName: String {66 switch self {67 case .auto: "Flash auto"68 case .on: "Flash activé"69 case .off: "Flash désactivé"70 }71 }7273 var symbolName: String {74 switch self {75 case .auto: "bolt.badge.a"76 case .on: "bolt.fill"77 case .off: "bolt.slash.fill"78 }79 }8081 var next: FlashMode {82 switch self {83 case .auto: .on84 case .on: .off85 case .off: .auto86 }87 }88}8990enum LensKind: String, Codable, Sendable, CaseIterable {91 case ultraWide, wide, telephoto9293 /// User-facing name, in French (code stays in English).94 var displayName: String {95 switch self {96 case .ultraWide: "Ultra grand-angle"97 case .wide: "Grand-angle"98 case .telephoto: "Téléobjectif"99 }100 }101}102103enum CaptureFormat: String, Codable, Sendable, CaseIterable {104 case heic105 case proRAW106107 var displayName: String {108 switch self {109 case .heic: "HEIC"110 case .proRAW: "ProRAW"111 }112 }113}114115enum BurstRole: String, Codable, Sendable {116 case selected // user's pick within the burst117 case member118}119120/// A place, deliberately coarse. Never raw coordinates (CLAUDE.md §4).121struct PlaceRef: Codable, Sendable {122 var name: String? // "Chalet", "Garage Marcel"123 var locality: String? // "Québec"124 var countryCode: String? // "CA"125}126