Introduce TankDefinition/TankCatalog loaded from Tanks.json, bundle the Drachengas USDZ tank models, add tank selection and preview views, and update the AR experience, editor and project views to use the catalog. Includes tests for catalog loading and model selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.0 KiB
Swift
92 lines
3.0 KiB
Swift
//
|
||
// Project.swift
|
||
// GasTankAR
|
||
//
|
||
// Created by Dennis Nemec on 25.08.26.
|
||
//
|
||
|
||
import Foundation
|
||
import SwiftData
|
||
import ARKit
|
||
|
||
@Model
|
||
final class Project {
|
||
/// Identifier given by the user for the project. In DOCUframe terms
|
||
/// it is the LeadID.
|
||
var leadID: String
|
||
|
||
/// Customer name to link the project to.
|
||
var customerName: String
|
||
|
||
var creationDate: Date
|
||
|
||
/// Der für das Projekt ausgewählte Tank. Jedes Projekt hat genau einen Tank.
|
||
/// `Tank` ist `Codable`, daher persistiert SwiftData das direkt als Attribut.
|
||
var tank: Tank
|
||
|
||
/// Archivierte `ARWorldMap` (via `NSKeyedArchiver`). Wegen der Größe extern gespeichert.
|
||
@Attribute(.externalStorage) var worldMapData: Data?
|
||
|
||
/// Welt-Transform des platzierten Tanks (Position, Rotation, Skalierung) als
|
||
/// 16 Floats (spaltenweise). Zusammen mit der WorldMap wird der Tank so wiederhergestellt.
|
||
var tankTransform: [Float]?
|
||
|
||
/// Persistierte, frei eingetragene Strecken als Codable-Daten.
|
||
@Attribute(.externalStorage) var measurementsData: Data?
|
||
|
||
/// Persistierte Rohr-Wegpunkte und ihre Verbindungen als Codable-Daten.
|
||
@Attribute(.externalStorage) var pipeRouteData: Data?
|
||
|
||
/// Im AR-Modus aufgenommene Fotos. Werden beim Löschen des Projekts mitentfernt.
|
||
@Relationship(deleteRule: .cascade, inverse: \ProjectPhoto.project)
|
||
var photos: [ProjectPhoto] = []
|
||
|
||
init(
|
||
leadID: String,
|
||
customerName: String,
|
||
creationDate: Date,
|
||
tank: Tank = Tank(),
|
||
worldMapData: Data? = nil,
|
||
tankTransform: [Float]? = nil,
|
||
measurementsData: Data? = nil,
|
||
pipeRouteData: Data? = nil
|
||
) {
|
||
self.leadID = leadID
|
||
self.customerName = customerName
|
||
self.creationDate = creationDate
|
||
self.tank = tank
|
||
self.worldMapData = worldMapData
|
||
self.tankTransform = tankTransform
|
||
self.measurementsData = measurementsData
|
||
self.pipeRouteData = pipeRouteData
|
||
}
|
||
|
||
/// Human-readable title combining the lead identifier and customer name.
|
||
var title: String {
|
||
"\(leadID) – \(customerName)"
|
||
}
|
||
|
||
/// Up to two uppercased initials derived from the customer name, for use as an avatar.
|
||
var initials: String {
|
||
let letters = customerName
|
||
.split(whereSeparator: { $0 == " " || $0 == "&" })
|
||
.prefix(2)
|
||
.compactMap { $0.first }
|
||
return String(letters).uppercased()
|
||
}
|
||
|
||
/// Gespeicherte Session der getrackten Welt. (De-)serialisiert `worldMapData`.
|
||
/// Berechnete Property – wird von SwiftData nicht persistiert.
|
||
var worldMap: ARWorldMap? {
|
||
get {
|
||
guard let worldMapData else { return nil }
|
||
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: worldMapData)
|
||
}
|
||
set {
|
||
worldMapData = newValue.flatMap {
|
||
try? NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: true)
|
||
}
|
||
}
|
||
}
|
||
}
|