Blender4iOS/Sources/Viewport/MultiViewportView.swift

197 lines
5.9 KiB
Swift
Raw Normal View History

2026-04-10 21:34:23 -07:00
// MultiViewportView.swift
// Feature 18: Split the viewport into 1, 2, or 4 panes.
// Each pane has its own camera. All share the same SceneGraph.
import SwiftUI
import Metal
// MARK: - Layout modes
enum ViewportLayout: String, CaseIterable, Identifiable {
case single = "Single"
case splitH = "Split Horizontal"
case splitV = "Split Vertical"
case quad = "Quad"
var id: String { rawValue }
var icon: String {
switch self {
case .single: "rectangle"
case .splitH: "rectangle.split.2x1"
case .splitV: "rectangle.split.1x2"
case .quad: "rectangle.split.2x2"
}
}
}
// MARK: - Preset camera angles
enum CameraPreset {
case perspective, top, front, right
func configure(_ camera: OrbitalCamera) {
switch self {
case .perspective:
camera.azimuth = Float.pi * 0.25
camera.elevation = Float.pi * 0.2
camera.distance = 7.0
case .top:
camera.azimuth = 0
camera.elevation = Float.pi * 0.499
camera.distance = 10.0
case .front:
camera.azimuth = 0
camera.elevation = 0
camera.distance = 10.0
case .right:
camera.azimuth = Float.pi * 0.5
camera.elevation = 0
camera.distance = 10.0
}
}
}
// MARK: - Multi-Viewport View
struct MultiViewportView: View {
let renderers: [ViewportRenderer]
let layout: ViewportLayout
// Divider positions (0...1)
@State private var hSplit: CGFloat = 0.5
@State private var vSplit: CGFloat = 0.5
private let dividerThickness: CGFloat = 2
private let dividerHitArea: CGFloat = 16
var body: some View {
GeometryReader { geo in
switch layout {
case .single:
singleLayout()
case .splitH:
splitHorizontal(size: geo.size)
case .splitV:
splitVertical(size: geo.size)
case .quad:
quadLayout(size: geo.size)
}
}
.clipped()
}
// MARK: - Layouts
@ViewBuilder
private func singleLayout() -> some View {
pane(index: 0, label: "Perspective")
}
@ViewBuilder
private func splitHorizontal(size: CGSize) -> some View {
let leftWidth = size.width * hSplit - dividerThickness * 0.5
let rightWidth = size.width * (1 - hSplit) - dividerThickness * 0.5
HStack(spacing: 0) {
pane(index: 0, label: "Perspective")
.frame(width: max(leftWidth, 40))
dividerView(axis: .vertical, size: size)
pane(index: 1, label: "Right")
.frame(width: max(rightWidth, 40))
}
}
@ViewBuilder
private func splitVertical(size: CGSize) -> some View {
let topHeight = size.height * vSplit - dividerThickness * 0.5
let bottomHeight = size.height * (1 - vSplit) - dividerThickness * 0.5
VStack(spacing: 0) {
pane(index: 0, label: "Perspective")
.frame(height: max(topHeight, 40))
dividerView(axis: .horizontal, size: size)
pane(index: 1, label: "Front")
.frame(height: max(bottomHeight, 40))
}
}
@ViewBuilder
private func quadLayout(size: CGSize) -> some View {
let leftW = size.width * hSplit - dividerThickness * 0.5
let rightW = size.width * (1 - hSplit) - dividerThickness * 0.5
let topH = size.height * vSplit - dividerThickness * 0.5
let botH = size.height * (1 - vSplit) - dividerThickness * 0.5
VStack(spacing: 0) {
HStack(spacing: 0) {
pane(index: 0, label: "Top")
.frame(width: max(leftW, 40), height: max(topH, 40))
dividerView(axis: .vertical, size: size)
pane(index: 1, label: "Perspective")
.frame(width: max(rightW, 40), height: max(topH, 40))
}
.frame(height: max(topH, 40))
dividerView(axis: .horizontal, size: size)
HStack(spacing: 0) {
pane(index: 2, label: "Front")
.frame(width: max(leftW, 40), height: max(botH, 40))
dividerView(axis: .vertical, size: size)
pane(index: 3, label: "Right")
.frame(width: max(rightW, 40), height: max(botH, 40))
}
.frame(height: max(botH, 40))
}
}
// MARK: - Pane helper
@ViewBuilder
private func pane(index: Int, label: String) -> some View {
if index < renderers.count {
ViewportPane(renderer: renderers[index], label: label)
} else {
Color(white: 0.15)
.overlay(Text("No renderer").foregroundStyle(.secondary))
}
}
// MARK: - Draggable divider
@ViewBuilder
private func dividerView(axis: Axis, size: CGSize) -> some View {
Rectangle()
.fill(Color.white.opacity(0.15))
.frame(
width: axis == .vertical ? dividerThickness : nil,
height: axis == .horizontal ? dividerThickness : nil
)
.contentShape(Rectangle().size(
width: axis == .vertical ? dividerHitArea : size.width,
height: axis == .horizontal ? dividerHitArea : size.height
))
.gesture(
DragGesture(minimumDistance: 1)
.onChanged { value in
switch axis {
case .vertical:
let newH = value.location.x / size.width
hSplit = min(max(newH, 0.15), 0.85)
case .horizontal:
let newV = value.location.y / size.height
vSplit = min(max(newV, 0.15), 0.85)
}
}
)
}
}