Blender4iOS/Sources/UV/UVPanels.swift

263 lines
8.9 KiB
Swift

// UVPanels.swift
// UV Editor viewport and Texture Bake settings panel.
import SwiftUI
// MARK: - UV Editor View
struct UVEditorView: View {
@State private var uvZoom: CGFloat = 1.0
@State private var uvOffset: CGSize = .zero
@State private var showGrid = true
@State private var uvMode: UVEditMode = .vertex
var body: some View {
GeometryReader { geo in
ZStack {
// UV background (checkerboard)
Canvas { context, size in
let cellSize: CGFloat = 20 * uvZoom
let cols = Int(size.width / cellSize) + 2
let rows = Int(size.height / cellSize) + 2
for row in 0..<rows {
for col in 0..<cols {
let isLight = (row + col) % 2 == 0
let rect = CGRect(
x: CGFloat(col) * cellSize + uvOffset.width.truncatingRemainder(dividingBy: cellSize * 2),
y: CGFloat(row) * cellSize + uvOffset.height.truncatingRemainder(dividingBy: cellSize * 2),
width: cellSize,
height: cellSize
)
context.fill(
Path(rect),
with: .color(isLight ? Color(white: 0.25) : Color(white: 0.20))
)
}
}
// UV space bounds (0,0 to 1,1 mapped to view)
let uvRect = CGRect(
x: uvOffset.width,
y: uvOffset.height,
width: size.width * uvZoom,
height: size.height * uvZoom
)
context.stroke(
Path(uvRect),
with: .color(.orange.opacity(0.4)),
lineWidth: 1
)
}
.gesture(uvDragGesture)
.gesture(uvZoomGesture)
// Toolbar overlay
VStack {
HStack(spacing: 8) {
Text("UV Editor")
.font(.caption2.weight(.medium))
.foregroundStyle(.white.opacity(0.5))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.black.opacity(0.3), in: RoundedRectangle(cornerRadius: 4))
Spacer()
// Edit mode picker
Picker("Mode", selection: $uvMode) {
ForEach(UVEditMode.allCases) { mode in
Image(systemName: mode.icon)
.tag(mode)
}
}
.pickerStyle(.segmented)
.frame(width: 120)
}
.padding(8)
Spacer()
}
// Placeholder instruction
if true {
VStack {
Spacer()
Text("Select a mesh to view UVs")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.bottom, 20)
}
}
}
.background(Color(white: 0.18))
}
}
private var uvDragGesture: some Gesture {
DragGesture()
.onChanged { value in
uvOffset = value.translation
}
}
private var uvZoomGesture: some Gesture {
MagnifyGesture()
.onChanged { value in
uvZoom = max(0.1, min(value.magnification, 5.0))
}
}
}
enum UVEditMode: String, CaseIterable, Identifiable {
case vertex = "Vertex"
case edge = "Edge"
case face = "Face"
case island = "Island"
var id: String { rawValue }
var icon: String {
switch self {
case .vertex: "circle.fill"
case .edge: "line.diagonal"
case .face: "square.fill"
case .island: "square.on.square"
}
}
}
// MARK: - Texture Bake Panel
struct TextureBakePanel: View {
@State private var bakeType: BakeType = .diffuse
@State private var resolution: Int = 1024
@State private var margin: Int = 16
@State private var selectedOnly = false
@State private var isBaking = false
@State private var progress: Double = 0
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 14) {
// Header
HStack {
Image(systemName: "square.grid.3x3.topleft.filled")
.foregroundStyle(.orange)
Text("Texture Bake")
.font(.subheadline.weight(.semibold))
}
.padding(.bottom, 4)
// Bake type
VStack(alignment: .leading, spacing: 4) {
Text("Bake Type")
.font(.caption)
.foregroundStyle(.secondary)
Picker("Type", selection: $bakeType) {
ForEach(BakeType.allCases) { type in
Text(type.rawValue).tag(type)
}
}
.pickerStyle(.menu)
.tint(.orange)
}
// Resolution
VStack(alignment: .leading, spacing: 4) {
Text("Resolution")
.font(.caption)
.foregroundStyle(.secondary)
Picker("Resolution", selection: $resolution) {
Text("512").tag(512)
Text("1024").tag(1024)
Text("2048").tag(2048)
Text("4096").tag(4096)
}
.pickerStyle(.segmented)
}
// Margin
VStack(alignment: .leading, spacing: 4) {
Text("Margin: \(margin)px")
.font(.caption)
.foregroundStyle(.secondary)
Slider(value: Binding(
get: { Double(margin) },
set: { margin = Int($0) }
), in: 0...64, step: 1)
.tint(.orange)
}
Toggle("Selected Only", isOn: $selectedOnly)
.font(.caption)
Divider()
// Target engine
VStack(alignment: .leading, spacing: 4) {
Text("Target Engine")
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
engineTag("Unity")
engineTag("Unreal")
engineTag("Godot")
}
}
Divider()
// Bake button
Button {
isBaking = true
progress = 0
// Simulate progress
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
progress += 0.02
if progress >= 1.0 {
timer.invalidate()
isBaking = false
}
}
} label: {
HStack {
Image(systemName: isBaking ? "hourglass" : "bolt.fill")
Text(isBaking ? "Baking..." : "Bake")
}
.font(.subheadline.weight(.medium))
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(isBaking ? Color.gray : Color.orange, in: RoundedRectangle(cornerRadius: 6))
.foregroundStyle(.white)
}
.disabled(isBaking)
if isBaking {
ProgressView(value: progress)
.tint(.orange)
}
}
.padding(12)
}
.background(.ultraThinMaterial)
}
private func engineTag(_ name: String) -> some View {
Text(name)
.font(.caption2.weight(.medium))
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(Color.primary.opacity(0.08), in: Capsule())
}
}
enum BakeType: String, CaseIterable, Identifiable {
case diffuse = "Diffuse"
case normal = "Normal"
case ao = "AO"
case roughness = "Roughness"
case emission = "Emission"
case combined = "Combined"
var id: String { rawValue }
}