169 lines
4.5 KiB
Swift
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)
|
|
}
|
|
}
|
|
|