115 lines
3.4 KiB
Swift
115 lines
3.4 KiB
Swift
|
|
// 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>(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)
|
||
|
|
}
|
||
|
|
}
|