Blender4iOS/Sources/Scene/SceneObject.swift
2026-04-10 21:34:23 -07:00

169 lines
4.5 KiB
Swift

// SceneObject.swift
// A single node in the scene graph with transform, mesh, and hierarchy.
import Observation
import simd
import Metal
// MARK: - Object Type
enum SceneObjectType: String, CaseIterable, Identifiable, Codable {
case mesh = "Mesh"
case camera = "Camera"
case light = "Light"
case empty = "Empty"
var id: String { rawValue }
var icon: String {
switch self {
case .mesh: "cube.fill"
case .camera: "camera.fill"
case .light: "light.max"
case .empty: "square.dashed"
}
}
}
// MARK: - Transform
@Observable
final class Transform {
var position: SIMD3<Float> = .zero
var rotation: SIMD3<Float> = .zero // Euler XYZ (radians)
var scale: SIMD3<Float> = .init(repeating: 1)
var modelMatrix: simd_float4x4 {
let t = MathUtils.translation(position)
let rx = MathUtils.rotationX(rotation.x)
let ry = MathUtils.rotationY(rotation.y)
let rz = MathUtils.rotationZ(rotation.z)
let s = MathUtils.scale(scale)
return t * rz * ry * rx * s
}
func copy() -> Transform {
let c = Transform()
c.position = position
c.rotation = rotation
c.scale = scale
return c
}
}
// MARK: - Mesh Handle (GPU-side)
/// Holds the Metal buffers for a mesh. Shared across objects
/// that reference the same primitive (instancing-ready).
final class MeshHandle {
let vertexBuffer: MTLBuffer
let vertexCount: Int
let wireVertexBuffer: MTLBuffer?
let wireVertexCount: Int
let label: String
// Stats
let faceCount: Int
let edgeCount: Int
init(
vertexBuffer: MTLBuffer,
vertexCount: Int,
wireVertexBuffer: MTLBuffer? = nil,
wireVertexCount: Int = 0,
faceCount: Int = 0,
edgeCount: Int = 0,
label: String = "Mesh"
) {
self.vertexBuffer = vertexBuffer
self.vertexCount = vertexCount
self.wireVertexBuffer = wireVertexBuffer
self.wireVertexCount = wireVertexCount
self.faceCount = faceCount
self.edgeCount = edgeCount
self.label = label
}
}
// MARK: - Scene Object
@Observable
final class SceneObject: Identifiable {
let id: UUID
var name: String
var type: SceneObjectType
let transform: Transform
// Mesh data (nil for cameras/lights/empties)
var meshHandle: MeshHandle?
// Hierarchy
weak var parent: SceneObject?
var children: [SceneObject] = []
// Visibility & selection
var isVisible: Bool = true
var isSelected: Bool = false
// Per-object color (for viewport solid shading)
var color: SIMD4<Float> = SIMD4<Float>(0.63, 0.63, 0.63, 1.0)
// Bounding box (local space, axis-aligned)
var boundingBoxMin: SIMD3<Float> = SIMD3<Float>(repeating: -1)
var boundingBoxMax: SIMD3<Float> = SIMD3<Float>(repeating: 1)
init(
name: String,
type: SceneObjectType,
position: SIMD3<Float> = .zero
) {
self.id = UUID()
self.name = name
self.type = type
self.transform = Transform()
self.transform.position = position
}
// MARK: Hierarchy helpers
func addChild(_ child: SceneObject) {
child.parent = self
children.append(child)
}
func removeFromParent() {
parent?.children.removeAll { $0.id == self.id }
parent = nil
}
/// World-space model matrix walking up the parent chain.
var worldMatrix: simd_float4x4 {
if let parent {
return parent.worldMatrix * transform.modelMatrix
}
return transform.modelMatrix
}
/// Axis-aligned bounding box in world space.
var worldBounds: (min: SIMD3<Float>, max: SIMD3<Float>) {
let m = worldMatrix
// Transform all 8 corners and find min/max
let lo = boundingBoxMin
let hi = boundingBoxMax
let corners: [SIMD3<Float>] = [
.init(lo.x, lo.y, lo.z), .init(hi.x, lo.y, lo.z),
.init(lo.x, hi.y, lo.z), .init(hi.x, hi.y, lo.z),
.init(lo.x, lo.y, hi.z), .init(hi.x, lo.y, hi.z),
.init(lo.x, hi.y, hi.z), .init(hi.x, hi.y, hi.z),
]
var wMin = SIMD3<Float>(repeating: .greatestFiniteMagnitude)
var wMax = SIMD3<Float>(repeating: -.greatestFiniteMagnitude)
for c in corners {
let w = (m * SIMD4<Float>(c, 1)).xyz
wMin = min(wMin, w)
wMax = max(wMax, w)
}
return (wMin, wMax)
}
}