// 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: 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()) } } }