// 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(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" } } }