// ViewportView.swift // A single viewport pane — MTKView wrapper with gesture handling. import SwiftUI import MetalKit // MARK: - Single Viewport Pane struct ViewportPane: View { let renderer: ViewportRenderer let label: String var body: some View { GeometryReader { geo in ZStack(alignment: .topLeading) { MetalViewRepresentable( renderer: renderer, size: geo.size ) .ignoresSafeArea() .gesture(orbitGesture) .gesture(zoomGesture) .onTapGesture(count: 2) { renderer.camera.reset() } // Viewport label Text(label) .font(.caption2.weight(.medium)) .foregroundStyle(.white.opacity(0.5)) .padding(.horizontal, 8) .padding(.vertical, 4) .background(.black.opacity(0.3), in: RoundedRectangle(cornerRadius: 4)) .padding(8) } } } private var orbitGesture: some Gesture { DragGesture(minimumDistance: 1) .onChanged { value in renderer.camera.orbit( deltaX: Float(value.translation.width) * 0.15, deltaY: Float(value.translation.height) * 0.15 ) } } private var zoomGesture: some Gesture { MagnifyGesture() .onChanged { value in renderer.camera.zoom(scale: Float(value.magnification)) } } } // MARK: - UIViewRepresentable struct MetalViewRepresentable: UIViewRepresentable { let renderer: ViewportRenderer let size: CGSize func makeCoordinator() -> ViewportCoordinator { ViewportCoordinator(renderer: renderer) } func makeUIView(context: Context) -> MTKView { guard let device = renderer.device ?? MTLCreateSystemDefaultDevice() else { fatalError("Metal is not supported on this device.") } let mtkView = MTKView(frame: .zero, device: device) mtkView.delegate = context.coordinator mtkView.colorPixelFormat = .bgra8Unorm_srgb mtkView.depthStencilPixelFormat = .depth32Float mtkView.clearColor = MTLClearColor(red: 0.224, green: 0.224, blue: 0.224, alpha: 1.0) mtkView.preferredFramesPerSecond = 120 mtkView.enableSetNeedsDisplay = false mtkView.isPaused = false mtkView.framebufferOnly = false mtkView.isMultipleTouchEnabled = true return mtkView } func updateUIView(_ mtkView: MTKView, context: Context) { let scale = mtkView.contentScaleFactor mtkView.drawableSize = CGSize( width: size.width * scale, height: size.height * scale ) } } // MARK: - Coordinator final class ViewportCoordinator: NSObject, MTKViewDelegate { let renderer: ViewportRenderer init(renderer: ViewportRenderer) { self.renderer = renderer } func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { renderer.viewportSize = SIMD2(Float(size.width), Float(size.height)) } func draw(in view: MTKView) { guard let drawable = view.currentDrawable, let descriptor = view.currentRenderPassDescriptor else { return } renderer.draw(drawable: drawable, passDescriptor: descriptor) } }