Blender4iOS/Sources/GreasePencil/GPPanels.swift

240 lines
7.2 KiB
Swift

// GPPanels.swift
// SwiftUI panels for the Grease Pencil workspace.
import SwiftUI
// MARK: - Brush Panel
struct GPBrushPanel: View {
@State private var selectedBrush: GPBrushType = .draw
@State private var brushSize: Float = 25
@State private var brushStrength: Float = 1.0
@State private var brushColor: Color = .black
var body: some View {
VStack(spacing: 8) {
// Brush type selector
ForEach(GPBrushType.allCases) { brush in
Button {
selectedBrush = brush
} label: {
Image(systemName: brush.icon)
.font(.system(size: 16))
.frame(width: 36, height: 36)
.background(
selectedBrush == brush
? Color.orange.opacity(0.25)
: Color.clear,
in: RoundedRectangle(cornerRadius: 6)
)
}
.tint(selectedBrush == brush ? .orange : .secondary)
}
Divider().padding(.horizontal, 8)
// Color
ColorPicker("", selection: $brushColor)
.labelsHidden()
.frame(width: 36, height: 36)
Divider().padding(.horizontal, 8)
// Quick presets
VStack(spacing: 4) {
Text("Size")
.font(.system(size: 8))
.foregroundStyle(.secondary)
Text("\(Int(brushSize))")
.font(.system(size: 10, design: .monospaced))
.foregroundStyle(.primary)
}
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 4)
.background(.ultraThinMaterial)
}
}
enum GPBrushType: String, CaseIterable, Identifiable {
case draw = "Draw"
case fill = "Fill"
case erase = "Erase"
case tint = "Tint"
case smooth = "Smooth"
case grab = "Grab"
var id: String { rawValue }
var icon: String {
switch self {
case .draw: "pencil.tip"
case .fill: "paintbrush.fill"
case .erase: "eraser"
case .tint: "eyedropper"
case .smooth: "wand.and.stars"
case .grab: "hand.point.up.left"
}
}
}
// MARK: - Layer Panel
struct GPLayerPanel: View {
@State private var layers: [GPLayerInfo] = [
.init(name: "Fills", isVisible: true, isActive: false),
.init(name: "Lines", isVisible: true, isActive: true),
.init(name: "Sketch", isVisible: false, isActive: false),
]
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Header
HStack {
Text("Layers")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Spacer()
Button {
let new = GPLayerInfo(
name: "Layer \(layers.count + 1)",
isVisible: true,
isActive: false
)
layers.insert(new, at: 0)
} label: {
Image(systemName: "plus")
.font(.caption2)
}
.tint(.secondary)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
Divider()
// Layer list
ScrollView {
LazyVStack(spacing: 0) {
ForEach(layers) { layer in
GPLayerRow(layer: layer) {
// Select
for i in layers.indices {
layers[i].isActive = (layers[i].id == layer.id)
}
}
}
}
}
Spacer()
}
.background(.ultraThinMaterial)
}
}
struct GPLayerInfo: Identifiable {
let id = UUID()
var name: String
var isVisible: Bool
var isActive: Bool
}
struct GPLayerRow: View {
let layer: GPLayerInfo
let onSelect: () -> Void
var body: some View {
Button(action: onSelect) {
HStack(spacing: 8) {
// Color swatch
RoundedRectangle(cornerRadius: 2)
.fill(layer.isActive ? Color.orange : Color.gray.opacity(0.3))
.frame(width: 4, height: 24)
Text(layer.name)
.font(.caption)
.foregroundStyle(layer.isActive ? .primary : .secondary)
Spacer()
Image(systemName: layer.isVisible ? "eye" : "eye.slash")
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(layer.isActive ? Color.orange.opacity(0.08) : .clear)
}
.buttonStyle(.plain)
}
}
// MARK: - Onion Skin Panel
struct OnionSkinPanel: View {
@State private var enabled = true
@State private var framesBefore: Double = 3
@State private var framesAfter: Double = 1
@State private var opacity: Double = 0.5
@State private var colorBefore: Color = .red.opacity(0.3)
@State private var colorAfter: Color = .green.opacity(0.3)
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Onion Skin")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Spacer()
Toggle("", isOn: $enabled)
.labelsHidden()
.scaleEffect(0.7)
}
.padding(.horizontal, 10)
.padding(.top, 8)
if enabled {
VStack(alignment: .leading, spacing: 10) {
SliderRow(label: "Before", value: $framesBefore, range: 0...10)
SliderRow(label: "After", value: $framesAfter, range: 0...5)
SliderRow(label: "Opacity", value: $opacity, range: 0...1)
HStack(spacing: 12) {
ColorPicker("Before", selection: $colorBefore)
.font(.caption2)
ColorPicker("After", selection: $colorAfter)
.font(.caption2)
}
}
.padding(.horizontal, 10)
}
Spacer()
}
.background(.ultraThinMaterial)
}
}
struct SliderRow: View {
let label: String
@Binding var value: Double
let range: ClosedRange<Double>
var body: some View {
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(label)
.font(.system(size: 10))
.foregroundStyle(.secondary)
Spacer()
Text(String(format: range.upperBound > 1 ? "%.0f" : "%.2f", value))
.font(.system(size: 10, design: .monospaced))
}
Slider(value: $value, in: range)
.tint(.orange)
}
}
}