Initial commit for first version of project
This commit is contained in:
93
GasTankAR/Views/TankAR/TankEntityFactory.swift
Normal file
93
GasTankAR/Views/TankAR/TankEntityFactory.swift
Normal 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)]
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user