Files
Gas-Tank-AR-App/GasTankAR/Services/TankThumbnailRenderer.swift

72 lines
3.3 KiB
Swift

import SceneKit
import UIKit
/// Renders the actual catalog model once per variant, shared by all project rows.
@MainActor
final class TankThumbnailRenderer {
static let shared = TankThumbnailRenderer()
private let cache = NSCache<NSString, UIImage>()
func image(for definition: TankDefinition) -> UIImage? {
let key = definition.modelPath as NSString
if let image = cache.object(forKey: key) { return image }
guard let url = Bundle.main.url(forResource: definition.modelResourceName,
withExtension: definition.modelFileExtension),
let source = try? SCNScene(url: url, options: [.convertToYUp: true]) else { return nil }
let scene = SCNScene()
let model = SCNNode()
for child in source.rootNode.childNodes { model.addChildNode(child.clone()) }
// Bundle scenes may carry studio lights; use one consistent thumbnail setup.
model.enumerateChildNodes { node, _ in
node.light = nil
node.camera = nil
}
// Underground catalog models need an additional quarter-turn after Y-up conversion.
if definition.placement == .belowGround {
model.eulerAngles.x = -.pi / 2
}
let container = SCNNode()
container.addChildNode(model)
scene.rootNode.addChildNode(container)
// Measure in the parent's coordinate system, including the corrected orientation.
let bounds = container.boundingBox
let center = SCNVector3((bounds.min.x + bounds.max.x) / 2,
(bounds.min.y + bounds.max.y) / 2,
(bounds.min.z + bounds.max.z) / 2)
let extent = SCNVector3(bounds.max.x - bounds.min.x,
bounds.max.y - bounds.min.y,
bounds.max.z - bounds.min.z)
let radius = max(sqrt(extent.x * extent.x + extent.y * extent.y + extent.z * extent.z) / 2, 0.01)
let camera = SCNNode()
camera.camera = SCNCamera()
camera.camera?.usesOrthographicProjection = true
camera.camera?.orthographicScale = Double(radius * 0.64)
camera.camera?.zNear = Double(radius * 0.01)
camera.camera?.zFar = Double(radius * 20)
camera.position = SCNVector3(center.x + radius * 0.65,
center.y + radius * 0.7, center.z + radius * 3)
camera.look(at: center)
scene.rootNode.addChildNode(camera)
let ambient = SCNNode()
ambient.light = SCNLight()
ambient.light?.type = .ambient
ambient.light?.intensity = 300
scene.rootNode.addChildNode(ambient)
let light = SCNNode()
light.light = SCNLight()
light.light?.type = .omni
light.light?.intensity = 800
light.position = SCNVector3(center.x - radius, center.y + radius * 3, center.z + radius * 2)
scene.rootNode.addChildNode(light)
let renderer = SCNRenderer(device: nil, options: nil)
renderer.scene = scene
renderer.pointOfView = camera
let image = renderer.snapshot(atTime: 0, with: CGSize(width: 600, height: 360),
antialiasingMode: .multisampling4X)
cache.setObject(image, forKey: key)
return image
}
}