// 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 = .zero var rotation: SIMD3 = .zero // Euler XYZ (radians) var scale: SIMD3 = .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 = SIMD4(0.63, 0.63, 0.63, 1.0) // Bounding box (local space, axis-aligned) var boundingBoxMin: SIMD3 = SIMD3(repeating: -1) var boundingBoxMax: SIMD3 = SIMD3(repeating: 1) init( name: String, type: SceneObjectType, position: SIMD3 = .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, max: SIMD3) { let m = worldMatrix // Transform all 8 corners and find min/max let lo = boundingBoxMin let hi = boundingBoxMax let corners: [SIMD3] = [ .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(repeating: .greatestFiniteMagnitude) var wMax = SIMD3(repeating: -.greatestFiniteMagnitude) for c in corners { let w = (m * SIMD4(c, 1)).xyz wMin = min(wMin, w) wMax = max(wMax, w) } return (wMin, wMax) } }