Add lead workflows, PDF signing, AR projects and photo gallery integration
This commit is contained in:
71
GasTankAR/Services/TankThumbnailRenderer.swift
Normal file
71
GasTankAR/Services/TankThumbnailRenderer.swift
Normal file
@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user