Initial commit for first version of project

This commit is contained in:
Dennis Nemec
2026-09-02 20:54:36 +02:00
parent 3d9f1ab6bc
commit 992bb32025
56 changed files with 5139 additions and 31 deletions

View File

@ -0,0 +1,81 @@
//
// 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]?
/// 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
) {
self.leadID = leadID
self.customerName = customerName
self.creationDate = creationDate
self.tank = tank
self.worldMapData = worldMapData
self.tankTransform = tankTransform
}
/// 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)
}
}
}
}

View File

@ -0,0 +1,49 @@
//
// ProjectPhoto.swift
// GasTankAR
//
// Created by Dennis Nemec on 27.08.26.
//
import Foundation
import SwiftData
import CoreLocation
/// Ein im AR-Modus aufgenommenes Foto (Snapshot der Kamera-Sicht), das zu einem Projekt gehört.
/// Die Bilddaten liegen wegen ihrer Größe extern (`.externalStorage`).
@Model
final class ProjectPhoto {
/// Aufnahmezeitpunkt – Grundlage für die Gruppierung in der Galerie.
var creationDate: Date
/// JPEG-kodierte Bilddaten.
@Attribute(.externalStorage) var imageData: Data
/// Geografische Breite des Aufnahmeorts, sofern beim Aufnehmen ein Standort vorlag.
var latitude: Double?
/// Geografische Länge des Aufnahmeorts, sofern beim Aufnehmen ein Standort vorlag.
var longitude: Double?
/// Zugehöriges Projekt (Rückbeziehung zu `Project.photos`).
var project: Project?
init(
creationDate: Date,
imageData: Data,
latitude: Double? = nil,
longitude: Double? = nil,
project: Project? = nil
) {
self.creationDate = creationDate
self.imageData = imageData
self.latitude = latitude
self.longitude = longitude
self.project = project
}
/// Aufnahmeort als Koordinate – `nil`, wenn kein Standort gespeichert wurde.
var coordinate: CLLocationCoordinate2D? {
guard let latitude, let longitude else { return nil }
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}

View File

@ -0,0 +1,80 @@
//
// Tank.swift
// GasTankAR
//
// Created by Dennis Nemec on 25.08.26.
//
import Foundation
/// Verfügbare Tankgrößen laut Projektplan Phase 4 (Modell + korrekter Maßstab).
enum TankSize: String, CaseIterable, Identifiable, Codable, Hashable {
case t1_2
case t2_1
case t2_9
var id: String { rawValue }
var displayName: String {
switch self {
case .t1_2: "1,2 t"
case .t2_1: "2,1 t"
case .t2_9: "2,9 t"
}
}
}
/// Verfügbare Tankfarben (Farbvariante mit Logo).
enum TankColor: String, CaseIterable, Identifiable, Codable, Hashable {
case white
case green
var id: String { rawValue }
var displayName: String {
switch self {
case .white: "Weiß"
case .green: "Grün"
}
}
}
/// Aufstellungsart des Tanks.
enum TankPlacement: String, CaseIterable, Identifiable, Codable, Hashable {
case aboveGround
case belowGround
var id: String { rawValue }
var displayName: String {
switch self {
case .aboveGround: "Oberirdisch"
case .belowGround: "Unterirdisch"
}
}
/// Name des Asset-Icons für diese Aufstellungsart.
var imageName: String {
switch self {
case .aboveGround: "tank-upper"
case .belowGround: "tank-lower"
}
}
}
/// Ein für ein Projekt ausgewählter Tank. Kombiniert die in Phase 4 des
/// Projektplans vorgesehenen Auswahlmöglichkeiten: Größe, Farbe und
/// Aufstellungsart sowie die optionale halbtransparente Darstellung.
struct Tank: Hashable, Codable {
var size: TankSize = .t1_2
var color: TankColor = .white
var placement: TankPlacement = .aboveGround
/// Optionale Darstellung, bei der nur der Deckel sichtbar ist.
var showLidOnly: Bool = false
/// Kurzbeschreibung für Listen und Detailanzeigen.
var displayName: String {
"\(size.displayName) · \(color.displayName) · \(placement.displayName)"
}
}