Initial commit: IYmtg Master project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
116
IYmtg_App_iOS/CardLogic.swift
Normal file
116
IYmtg_App_iOS/CardLogic.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
import Vision
|
||||
import CoreGraphics
|
||||
import ImageIO
|
||||
|
||||
struct CardFingerprint: Codable, Identifiable, Sendable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
let setCode: String
|
||||
let collectorNumber: String
|
||||
let hasFoilPrinting: Bool
|
||||
let hasSerializedPrinting: Bool?
|
||||
let featureData: Data
|
||||
var priceScanned: Double? = nil
|
||||
}
|
||||
|
||||
struct CardMetadata: Identifiable, Sendable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
let setCode: String
|
||||
let collectorNumber: String
|
||||
let hasFoilPrinting: Bool
|
||||
let hasSerializedPrinting: Bool
|
||||
var priceScanned: Double? = nil
|
||||
var rarity: String? = nil
|
||||
var colorIdentity: [String]? = nil
|
||||
var isSerialized: Bool = false
|
||||
}
|
||||
|
||||
struct SavedCard: Codable, Identifiable, Hashable, Sendable {
|
||||
let id: UUID
|
||||
let scryfallID: String
|
||||
var name: String
|
||||
var setCode: String
|
||||
var collectorNumber: String
|
||||
let imageFileName: String
|
||||
var condition: String
|
||||
var foilType: String
|
||||
var currentValuation: Double?
|
||||
var previousValuation: Double?
|
||||
var dateAdded: Date
|
||||
var classification: String
|
||||
var collectionName: String
|
||||
var storageLocation: String
|
||||
var rarity: String?
|
||||
var colorIdentity: [String]?
|
||||
|
||||
// Grading Fields
|
||||
var gradingService: String? // PSA, BGS
|
||||
var grade: String? // 10, 9.5
|
||||
var certNumber: String? // 123456
|
||||
var isCustomValuation: Bool = false
|
||||
var isSerialized: Bool? = false
|
||||
var currencyCode: String?
|
||||
|
||||
init(from scan: CardMetadata, imageName: String, collection: String, location: String) {
|
||||
self.id = UUID()
|
||||
self.scryfallID = "\(scan.setCode)-\(scan.collectorNumber)"
|
||||
self.name = scan.name
|
||||
self.setCode = scan.setCode
|
||||
self.collectorNumber = scan.collectorNumber
|
||||
self.imageFileName = imageName
|
||||
self.condition = AppConfig.Defaults.defaultCondition
|
||||
self.foilType = AppConfig.Defaults.defaultFoil
|
||||
self.currentValuation = scan.priceScanned
|
||||
self.previousValuation = scan.priceScanned
|
||||
self.dateAdded = Date()
|
||||
self.classification = "Unknown"
|
||||
self.collectionName = collection
|
||||
self.storageLocation = location
|
||||
self.rarity = scan.rarity
|
||||
self.colorIdentity = scan.colorIdentity
|
||||
self.isSerialized = scan.isSerialized
|
||||
}
|
||||
}
|
||||
|
||||
class FeatureMatcher {
|
||||
static let revision = VNGenerateImageFeaturePrintRequest.Revision.revision1
|
||||
|
||||
static func generateFingerprint(from image: CGImage, orientation: CGImagePropertyOrientation = .up) async throws -> VNFeaturePrintObservation {
|
||||
let req = VNGenerateImageFeaturePrintRequest()
|
||||
req.revision = revision
|
||||
req.imageCropAndScaleOption = .scaleFill
|
||||
let handler = VNImageRequestHandler(cgImage: image, orientation: orientation, options: [:])
|
||||
try handler.perform([req])
|
||||
|
||||
guard let result = req.results?.first as? VNFeaturePrintObservation else {
|
||||
throw NSError(domain: "FeatureMatcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "No features detected"])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func identify(scan: VNFeaturePrintObservation, database: [CardMetadata], cache: [UUID: VNFeaturePrintObservation]) -> MatchResult {
|
||||
var candidates: [(CardMetadata, Float)] = []
|
||||
for card in database {
|
||||
guard let obs = cache[card.id] else { continue }
|
||||
var dist: Float = 0
|
||||
if (try? scan.computeDistance(&dist, to: obs)) != nil && dist < 18.0 {
|
||||
candidates.append((card, dist))
|
||||
}
|
||||
}
|
||||
let sorted = candidates.sorted { $0.1 < $1.1 }
|
||||
guard let best = sorted.first else { return .unknown }
|
||||
|
||||
if best.1 < 6.0 { return .exact(best.0) }
|
||||
let close = sorted.filter { $0.1 < (best.1 + 3.0) }
|
||||
if close.count > 1 { return .ambiguous(name: best.0.name, candidates: close.map { $0.0 }) }
|
||||
return .exact(best.0)
|
||||
}
|
||||
}
|
||||
|
||||
enum MatchResult {
|
||||
case exact(CardMetadata)
|
||||
case ambiguous(name: String, candidates: [CardMetadata])
|
||||
case unknown
|
||||
}
|
||||
Reference in New Issue
Block a user