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,93 @@
//
// TankEntityFactory.swift
// GasTankAR
//
// Created by Dennis Nemec on 26.08.26.
//
import RealityKit
import UIKit
/// Baut die darzustellende Tank-Entity (aus `GasTank.usdz`): lädt/cached das Modell,
/// richtet es je nach Aufstellungsart aus, erzeugt die Kollisionsform und – bei
/// unterirdischer Aufstellung – den Höhen-Transparenz-Effekt.
@MainActor
final class TankEntityFactory {
/// Name des USDZ-Assets im Bundle.
private static let modelName = "GasTank"
/// Anteil der Modellhöhe, der bei unterirdischer Aufstellung oben herausragt („Kopf").
private static let belowGroundExposedFraction: Float = 0.25
/// Zusätzlicher Abstand zwischen Tank-Umkreis und Grubenwand (in Metern) – großzügig,
/// damit die Grube nicht gequetscht wirkt.
private static let pitMargin: Float = 0.4
/// Zusätzliche Tiefe der Grubensohle unter der Tankunterkante (in Metern).
private static let pitFloorClearance: Float = 0.1
/// Vorab geladenes Modell; wird pro Platzierung geklont.
private var loadedModel: Entity?
/// Lädt das USDZ asynchron vor (blockiert den Main-Actor nicht).
func preload() {
guard loadedModel == nil else { return }
Task { @MainActor in
self.loadedModel = try? await Entity(named: Self.modelName)
}
}
/// Erzeugt den manipulierbaren Container (mit Kollision fürs Gesten-Hit-Testing).
func makeContainer(for tank: Tank) -> ModelEntity {
let model = loadModelClone()
let bounds = model.visualBounds(relativeTo: nil)
let container = ModelEntity()
switch tank.placement {
case .aboveGround:
model.position.y -= bounds.min.y // Unterkante auf die Fläche
case .belowGround:
// Modell absenken, sodass nur der obere Teil („Kopf") herausragt …
let exposed = bounds.extents.y * Self.belowGroundExposedFraction
let buried = bounds.extents.y - exposed
model.position.y -= (bounds.max.y - exposed)
// … und den Boden „aufreißen": eine runde Grube ausheben, in welcher der Tank steht.
// Radius = Umkreis der Grundfläche (halbe Diagonale) + großzügiger Rand.
let footprintRadius = 0.5 * (bounds.extents.x * bounds.extents.x + bounds.extents.z * bounds.extents.z).squareRoot()
container.addChild(TankPitFactory.makePit(
radius: footprintRadius + Self.pitMargin,
depth: buried + Self.pitFloorClearance,
center: bounds.center
))
}
container.addChild(model)
let colliderBounds = model.visualBounds(relativeTo: container)
container.collision = CollisionComponent(shapes: [
ShapeResource
.generateBox(size: colliderBounds.extents)
.offsetBy(translation: colliderBounds.center)
])
// Auswahl-Markierung (Eck-Winkel) – zunächst versteckt, wird bei Auswahl eingeblendet.
let highlight = TankHighlightFactory.makeHighlight(for: colliderBounds)
highlight.isEnabled = false
container.addChild(highlight)
return container
}
/// Liefert einen Klon des vorab geladenen Modells. Fällt auf synchrones Laden
/// bzw. – wenn das USDZ fehlt – auf einen einfachen Cube zurück.
private func loadModelClone() -> Entity {
if loadedModel == nil {
loadedModel = try? Entity.load(named: Self.modelName)
}
if let loadedModel {
return loadedModel.clone(recursive: true)
}
return ModelEntity(
mesh: .generateBox(size: 0.3),
materials: [SimpleMaterial(color: .white, isMetallic: false)]
)
}
}