144 lines
4.2 KiB
Swift
144 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"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|