// WorkspaceManager.swift // Controls which workspace layout is active and panel sizing/visibility. import Observation import SwiftUI // MARK: - Workspace definitions enum WorkspaceType: String, CaseIterable, Identifiable { case modeling = "Modeling" case uvTexture = "UV / Texture Bake" case greasePencil = "Grease Pencil" case animation = "Animation" case rendering = "Rendering" case sculpting = "Sculpting" var id: String { rawValue } var icon: String { switch self { case .modeling: "cube" case .uvTexture: "square.grid.3x3.topleft.filled" case .greasePencil: "pencil.tip" case .animation: "play.rectangle" case .rendering: "sun.max.fill" case .sculpting: "paintbrush.pointed.fill" } } /// Default panel configuration for each workspace. var defaultPanels: [PanelSlot] { switch self { case .modeling: [ .init(.toolSidebar, edge: .leading, size: 0.04), .init(.viewport3D, edge: .center, size: 0.66), .init(.outliner, edge: .trailing, size: 0.15), .init(.properties, edge: .trailing, size: 0.15), ] case .uvTexture: [ .init(.toolSidebar, edge: .leading, size: 0.04), .init(.viewport3D, edge: .center, size: 0.40), .init(.uvEditor, edge: .center, size: 0.36), .init(.textureBake, edge: .trailing, size: 0.20), ] case .greasePencil: [ .init(.brushPanel, edge: .leading, size: 0.06), .init(.gpCanvas, edge: .center, size: 0.64), .init(.gpLayers, edge: .trailing, size: 0.16), .init(.onionSkin, edge: .trailing, size: 0.14), ] case .animation: [ .init(.toolSidebar, edge: .leading, size: 0.04), .init(.viewport3D, edge: .center, size: 0.70), .init(.outliner, edge: .trailing, size: 0.12), .init(.properties, edge: .trailing, size: 0.14), ] case .rendering: [ .init(.viewport3D, edge: .center, size: 0.70), .init(.renderSettings, edge: .trailing, size: 0.30), ] case .sculpting: [ .init(.brushPanel, edge: .leading, size: 0.06), .init(.viewport3D, edge: .center, size: 0.76), .init(.properties, edge: .trailing, size: 0.18), ] } } } // MARK: - Panel identity enum PanelID: String, Identifiable, CaseIterable { case toolSidebar = "Tools" case viewport3D = "3D Viewport" case uvEditor = "UV Editor" case textureBake = "Texture Bake" case brushPanel = "Brushes" case gpCanvas = "GP Canvas" case gpLayers = "Layers" case onionSkin = "Onion Skin" case outliner = "Outliner" case properties = "Properties" case renderSettings = "Render" case timeline = "Timeline" var id: String { rawValue } } enum PanelEdge { case leading, center, trailing, bottom } struct PanelSlot: Identifiable { let id: PanelID let edge: PanelEdge var size: CGFloat // proportion of total width (0…1) var isVisible: Bool = true var minSize: CGFloat = 0.04 init(_ id: PanelID, edge: PanelEdge, size: CGFloat, minSize: CGFloat = 0.04) { self.id = id self.edge = edge self.size = size self.minSize = minSize } } // MARK: - Manager @Observable final class WorkspaceManager { var activeWorkspace: WorkspaceType { didSet { panels = activeWorkspace.defaultPanels } } var panels: [PanelSlot] // Bottom panels (shared across workspaces) var showTimeline: Bool = false var timelineHeight: CGFloat = 0.2 init(workspace: WorkspaceType = .modeling) { self.activeWorkspace = workspace self.panels = workspace.defaultPanels } // MARK: - Panel queries func panel(_ id: PanelID) -> PanelSlot? { panels.first { $0.id == id } } var leadingPanels: [PanelSlot] { panels.filter { $0.edge == .leading && $0.isVisible } } var centerPanels: [PanelSlot] { panels.filter { $0.edge == .center && $0.isVisible } } var trailingPanels: [PanelSlot] { panels.filter { $0.edge == .trailing && $0.isVisible } } // MARK: - Panel mutation func togglePanel(_ id: PanelID) { guard let idx = panels.firstIndex(where: { $0.id == id }) else { return } panels[idx].isVisible.toggle() } func resizePanel(_ id: PanelID, to newSize: CGFloat) { guard let idx = panels.firstIndex(where: { $0.id == id }) else { return } panels[idx].size = max(panels[idx].minSize, min(newSize, 0.8)) } func switchWorkspace(to type: WorkspaceType) { withAnimation(.easeInOut(duration: 0.25)) { activeWorkspace = type } } }