Blender4iOS/Sources/UI/PropertiesPanel.swift
2026-04-10 21:34:23 -07:00

265 lines
8.1 KiB
Swift

// PropertiesPanel.swift
// Right-side properties inspector, driven by SceneGraph selection.
import SwiftUI
struct PropertiesPanel: View {
@Binding var isExpanded: Bool
let sceneGraph: SceneGraph
var body: some View {
if isExpanded {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
panelHeader
if let obj = sceneGraph.selectedObject {
// Transform section
PropertySection(title: "Transform", icon: "move.3d") {
TransformEditor(transform: obj.transform)
}
// Mesh info (if mesh type)
if let mesh = obj.meshHandle {
PropertySection(title: "Mesh", icon: "cube") {
InfoRow(label: "Vertices", value: "\(mesh.vertexCount / 3 * 3)")
InfoRow(label: "Edges", value: "\(mesh.edgeCount)")
InfoRow(label: "Faces", value: "\(mesh.faceCount)")
}
}
// Object info
PropertySection(title: "Object", icon: "info.circle") {
InfoRow(label: "Type", value: obj.type.rawValue)
InfoRow(label: "Visible", value: obj.isVisible ? "Yes" : "No")
}
// Modifiers (placeholder)
PropertySection(title: "Modifiers", icon: "wrench") {
Text("No modifiers")
.font(.caption)
.foregroundStyle(.secondary)
}
} else {
Text("No selection")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.top, 20)
}
}
.padding(12)
}
.frame(width: 260)
.background(.ultraThinMaterial)
}
}
private var panelHeader: some View {
HStack {
if let obj = sceneGraph.selectedObject {
Image(systemName: obj.type.icon)
.foregroundStyle(.orange)
Text(obj.name)
.font(.subheadline.weight(.semibold))
} else {
Image(systemName: "questionmark.square.dashed")
.foregroundStyle(.secondary)
Text("Properties")
.font(.subheadline.weight(.semibold))
}
Spacer()
Button {
withAnimation(.easeInOut(duration: 0.2)) {
isExpanded = false
}
} label: {
Image(systemName: "sidebar.trailing")
.font(.caption)
}
.tint(.secondary)
}
}
}
// MARK: - Outliner
struct OutlinerPanel: View {
let sceneGraph: SceneGraph
var body: some View {
VStack(alignment: .leading, spacing: 2) {
HStack {
Text("Scene Collection")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Spacer()
}
.padding(.horizontal, 8)
.padding(.top, 8)
ScrollView {
LazyVStack(spacing: 0) {
ForEach(sceneGraph.rootObjects) { obj in
OutlinerRow(object: obj, sceneGraph: sceneGraph)
}
}
}
}
.frame(maxWidth: .infinity)
.background(.ultraThinMaterial)
}
}
struct OutlinerRow: View {
let object: SceneObject
let sceneGraph: SceneGraph
var body: some View {
Button {
sceneGraph.select(object)
} label: {
HStack(spacing: 6) {
Image(systemName: object.children.isEmpty ? "circle.fill" : "chevron.right")
.font(.system(size: 7))
.foregroundStyle(.secondary)
.frame(width: 12)
Image(systemName: object.type.icon)
.font(.caption2)
.foregroundStyle(object.isSelected ? .orange : .secondary)
Text(object.name)
.font(.caption)
.foregroundStyle(object.isSelected ? .primary : .secondary)
Spacer()
// Visibility toggle
Button {
object.isVisible.toggle()
} label: {
Image(systemName: object.isVisible ? "eye" : "eye.slash")
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 5)
.background(object.isSelected ? Color.orange.opacity(0.12) : .clear)
}
.buttonStyle(.plain)
}
}
// MARK: - Transform Editor
struct TransformEditor: View {
let transform: Transform
var body: some View {
VStack(alignment: .leading, spacing: 6) {
TransformRow(
label: "Location",
x: transform.position.x,
y: transform.position.y,
z: transform.position.z
)
TransformRow(
label: "Rotation",
x: transform.rotation.x * 180 / .pi,
y: transform.rotation.y * 180 / .pi,
z: transform.rotation.z * 180 / .pi
)
TransformRow(
label: "Scale",
x: transform.scale.x,
y: transform.scale.y,
z: transform.scale.z
)
}
}
}
// MARK: - Property sub-views
struct PropertySection<Content: View>: View {
let title: String
let icon: String
@ViewBuilder let content: Content
@State private var expanded = true
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Button {
withAnimation(.easeInOut(duration: 0.15)) {
expanded.toggle()
}
} label: {
HStack(spacing: 6) {
Image(systemName: expanded ? "chevron.down" : "chevron.right")
.font(.system(size: 9))
Image(systemName: icon)
.font(.caption2)
Text(title)
.font(.caption.weight(.semibold))
Spacer()
}
.foregroundStyle(.primary)
}
if expanded {
content
.padding(.leading, 16)
}
}
}
}
struct TransformRow: View {
let label: String
let x: Float
let y: Float
let z: Float
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.system(size: 10))
.foregroundStyle(.secondary)
HStack(spacing: 4) {
field("X", value: x, color: .red)
field("Y", value: y, color: .green)
field("Z", value: z, color: .blue)
}
}
}
private func field(_ axis: String, value: Float, color: Color) -> some View {
HStack(spacing: 2) {
Text(axis)
.font(.system(size: 9, weight: .bold))
.foregroundStyle(color)
Text(String(format: "%.3f", value))
.font(.system(size: 10, design: .monospaced))
}
.padding(.horizontal, 6)
.padding(.vertical, 3)
.background(Color.primary.opacity(0.06), in: RoundedRectangle(cornerRadius: 3))
}
}
struct InfoRow: View {
let label: String
let value: String
var body: some View {
HStack {
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Text(value)
.font(.caption.weight(.medium).monospacedDigit())
}
}
}