2026-04-10 21:34:23 -07:00
|
|
|
// ContentView.swift
|
2026-04-10 21:41:12 -07:00
|
|
|
// Root layout — workspace header, dynamic panel layout, status bar.
|
2026-04-10 21:34:23 -07:00
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
struct ContentView: View {
|
|
|
|
|
@State private var context = MetalContext()
|
2026-04-10 21:41:12 -07:00
|
|
|
@State private var workspace = WorkspaceManager()
|
2026-04-10 21:34:23 -07:00
|
|
|
@State private var currentMode: EditMode = .object
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
GeometryReader { geo in
|
|
|
|
|
VStack(spacing: 0) {
|
2026-04-10 21:41:12 -07:00
|
|
|
// ── Workspace header ──
|
|
|
|
|
WorkspaceHeader(
|
|
|
|
|
workspace: workspace,
|
2026-04-10 21:34:23 -07:00
|
|
|
currentMode: $currentMode,
|
|
|
|
|
context: context
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-10 21:41:12 -07:00
|
|
|
// ── Dynamic workspace layout ──
|
|
|
|
|
WorkspaceLayoutView(
|
|
|
|
|
workspace: workspace,
|
|
|
|
|
context: context
|
|
|
|
|
)
|
2026-04-10 21:34:23 -07:00
|
|
|
|
2026-04-10 21:41:12 -07:00
|
|
|
// ── Status bar ──
|
2026-04-10 21:34:23 -07:00
|
|
|
StatusBar(mode: currentMode, sceneGraph: context.sceneGraph)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.ignoresSafeArea(.keyboard)
|
|
|
|
|
.preferredColorScheme(.dark)
|
2026-04-10 21:41:12 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Workspace Header
|
|
|
|
|
|
|
|
|
|
struct WorkspaceHeader: View {
|
|
|
|
|
let workspace: WorkspaceManager
|
|
|
|
|
@Binding var currentMode: EditMode
|
|
|
|
|
let context: MetalContext
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
// App icon
|
|
|
|
|
Image(systemName: "cube.fill")
|
|
|
|
|
.font(.title3)
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
|
|
|
|
Divider().frame(height: 20)
|
|
|
|
|
|
|
|
|
|
// Workspace tabs
|
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
|
ForEach(WorkspaceType.allCases) { ws in
|
|
|
|
|
WorkspaceTab(
|
|
|
|
|
type: ws,
|
|
|
|
|
isActive: workspace.activeWorkspace == ws
|
|
|
|
|
) {
|
|
|
|
|
workspace.switchWorkspace(to: ws)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Divider().frame(height: 20)
|
|
|
|
|
|
|
|
|
|
// Mode picker
|
|
|
|
|
Menu {
|
|
|
|
|
ForEach(EditMode.allCases) { mode in
|
|
|
|
|
Button {
|
|
|
|
|
currentMode = mode
|
2026-04-10 21:34:23 -07:00
|
|
|
} label: {
|
2026-04-10 21:41:12 -07:00
|
|
|
Label(mode.rawValue, systemImage: mode.icon)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
}
|
|
|
|
|
} label: {
|
|
|
|
|
Label(currentMode.rawValue, systemImage: currentMode.icon)
|
|
|
|
|
.font(.caption.weight(.medium))
|
|
|
|
|
.padding(.horizontal, 8)
|
|
|
|
|
.padding(.vertical, 5)
|
|
|
|
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 5))
|
|
|
|
|
}
|
2026-04-10 21:34:23 -07:00
|
|
|
|
2026-04-10 21:41:12 -07:00
|
|
|
Divider().frame(height: 20)
|
|
|
|
|
|
|
|
|
|
// Add mesh menu
|
|
|
|
|
Menu {
|
|
|
|
|
ForEach(PrimitiveType.allCases) { prim in
|
2026-04-10 21:34:23 -07:00
|
|
|
Button {
|
2026-04-10 21:41:12 -07:00
|
|
|
context.addPrimitive(prim)
|
2026-04-10 21:34:23 -07:00
|
|
|
} label: {
|
2026-04-10 21:41:12 -07:00
|
|
|
Label(prim.rawValue, systemImage: prim.icon)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
} label: {
|
|
|
|
|
Label("Add", systemImage: "plus.circle")
|
|
|
|
|
.font(.caption.weight(.medium))
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:41:12 -07:00
|
|
|
Button { context.deleteSelected() } label: {
|
|
|
|
|
Image(systemName: "trash")
|
|
|
|
|
.font(.caption)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
.tint(.secondary)
|
2026-04-10 21:34:23 -07:00
|
|
|
|
2026-04-10 21:41:12 -07:00
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
// Timeline toggle
|
|
|
|
|
Button {
|
|
|
|
|
withAnimation(.easeInOut(duration: 0.2)) {
|
|
|
|
|
workspace.showTimeline.toggle()
|
|
|
|
|
}
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "play.rectangle")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(workspace.showTimeline ? .orange : .secondary)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.padding(.vertical, 6)
|
|
|
|
|
.background(.ultraThinMaterial)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 21:34:23 -07:00
|
|
|
|
2026-04-10 21:41:12 -07:00
|
|
|
// MARK: - Workspace Tab
|
|
|
|
|
|
|
|
|
|
struct WorkspaceTab: View {
|
|
|
|
|
let type: WorkspaceType
|
|
|
|
|
let isActive: Bool
|
|
|
|
|
let action: () -> Void
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Button(action: action) {
|
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
|
Image(systemName: type.icon)
|
|
|
|
|
.font(.system(size: 10))
|
|
|
|
|
Text(type.rawValue)
|
|
|
|
|
.font(.system(size: 11, weight: isActive ? .semibold : .regular))
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
.padding(.horizontal, 10)
|
|
|
|
|
.padding(.vertical, 5)
|
|
|
|
|
.background(
|
|
|
|
|
isActive
|
|
|
|
|
? Color.orange.opacity(0.15)
|
|
|
|
|
: Color.clear,
|
|
|
|
|
in: RoundedRectangle(cornerRadius: 5)
|
|
|
|
|
)
|
|
|
|
|
.foregroundStyle(isActive ? .orange : .secondary)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
2026-04-10 21:41:12 -07:00
|
|
|
.buttonStyle(.plain)
|
2026-04-10 21:34:23 -07:00
|
|
|
}
|
|
|
|
|
}
|