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

143 lines
4.2 KiB
Swift

// MetalContext.swift
// Shared Metal state across the entire app.
// Creates and configures renderers for each viewport pane.
import Metal
import Observation
@Observable
final class MetalContext {
let device: MTLDevice
let library: MTLLibrary
let sceneGraph: SceneGraph
// Up to 4 renderers (for quad split)
let renderers: [ViewportRenderer]
// Layout state
var viewportLayout: ViewportLayout = .single
init() {
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("Metal is not supported on this device.")
}
guard let library = device.makeDefaultLibrary() else {
fatalError("Failed to create Metal library.")
}
self.device = device
self.library = library
self.sceneGraph = SceneGraph.defaultScene(device: device)
// Create 4 renderers with preset cameras
let presets: [(CameraPreset, String)] = [
(.perspective, "Perspective"),
(.top, "Top"),
(.front, "Front"),
(.right, "Right"),
]
var rends: [ViewportRenderer] = []
for (preset, _) in presets {
let cam = OrbitalCamera()
preset.configure(cam)
let renderer = ViewportRenderer(camera: cam)
renderer.configure(device: device, library: library)
renderer.sceneGraph = sceneGraph
rends.append(renderer)
}
self.renderers = rends
}
// MARK: - Add primitives
func addPrimitive(_ type: PrimitiveType) {
let data: PrimitiveMeshData
let name: String
switch type {
case .cube:
data = PrimitiveGenerator.cube()
name = uniqueName("Cube")
case .plane:
data = PrimitiveGenerator.plane()
name = uniqueName("Plane")
case .uvSphere:
data = PrimitiveGenerator.uvSphere()
name = uniqueName("Sphere")
case .icosphere:
data = PrimitiveGenerator.icosphere()
name = uniqueName("Icosphere")
case .cylinder:
data = PrimitiveGenerator.cylinder()
name = uniqueName("Cylinder")
case .cone:
data = PrimitiveGenerator.cone()
name = uniqueName("Cone")
case .torus:
data = PrimitiveGenerator.torus()
name = uniqueName("Torus")
}
let obj = SceneObject(name: name, type: .mesh)
obj.meshHandle = MeshUploader.upload(
vertices: data.vertices,
wireVertices: data.wireVertices,
faceCount: data.faceCount,
edgeCount: data.edgeCount,
label: name,
device: device
)
obj.boundingBoxMin = data.boundingBoxMin
obj.boundingBoxMax = data.boundingBoxMax
// Place at cursor (origin for now) with slight offset to avoid overlap
let existingCount = sceneGraph.allObjects.filter { $0.type == .mesh }.count
obj.transform.position = SIMD3<Float>(Float(existingCount) * 3.0, 0, 0)
sceneGraph.select(nil)
sceneGraph.addObject(obj)
sceneGraph.select(obj)
}
func deleteSelected() {
guard let selected = sceneGraph.selectedObject else { return }
sceneGraph.removeObject(selected)
}
private func uniqueName(_ base: String) -> String {
let existing = sceneGraph.allObjects.map { $0.name }
if !existing.contains(base) { return base }
var i = 1
while existing.contains("\(base).\(String(format: "%03d", i))") {
i += 1
}
return "\(base).\(String(format: "%03d", i))"
}
}
// MARK: - Primitive types
enum PrimitiveType: String, CaseIterable, Identifiable {
case cube = "Cube"
case plane = "Plane"
case uvSphere = "UV Sphere"
case icosphere = "Icosphere"
case cylinder = "Cylinder"
case cone = "Cone"
case torus = "Torus"
var id: String { rawValue }
var icon: String {
switch self {
case .cube: "cube"
case .plane: "square"
case .uvSphere: "globe"
case .icosphere: "circle"
case .cylinder: "cylinder"
case .cone: "triangle"
case .torus: "circle.circle"
}
}
}