export const BLENDER_WORKSPACE_IDS = [ "Layout", "Modeling", "Sculpting", "UV Editing", "Texture Paint", "Shading", "Animation", "Rendering", "Compositing", "Geometry Nodes", "Scripting", ] as const; export type WorkspaceId = typeof BLENDER_WORKSPACE_IDS[number]; export type EditorType = | "3D Viewport" | "Outliner" | "Properties" | "Timeline" | "Dope Sheet" | "Image Editor" | "Render Result" | "Shader Editor" | "Compositor" | "Geometry Nodes" | "File Browser" | "Spreadsheet" | "Text Editor" | "Python Console" | "Info"; export type BlenderMode = "Object" | "Edit" | "Pose" | "Sculpt" | "Texture Paint"; export interface RegionIR { id: string; kind: "header" | "main" | "toolbar" | "sidebar" | "footer"; visible: boolean; } export interface AreaIR { id: string; editor: EditorType; regions: RegionIR[]; rect: { x: number; y: number; width: number; height: number }; maximized: boolean; } export interface WorkspaceIR { id: WorkspaceId; name: string; areas: AreaIR[]; activeAreaId: string; revision: number; } export interface UIContextIR { workspaceId: WorkspaceId; activeAreaId: string; activeEditor: EditorType; mode: BlenderMode; activeObjectId: string | null; selection: string[]; viewLayer: string; pinnedData: string | null; revision: number; } export interface WebWorkspaceState { workspaces: Record; context: UIContextIR; operatorSearchOpen: boolean; openMenu: string | null; sidebarVisible: boolean; } export type UICommand = | { type: "switchWorkspace"; workspaceId: WorkspaceId } | { type: "setMode"; mode: BlenderMode } | { type: "setActiveArea"; areaId: string } | { type: "toggleOperatorSearch"; open?: boolean } | { type: "toggleMenu"; menu?: string } | { type: "toggleSidebar"; visible?: boolean }; function regions(): RegionIR[] { return [ { id: "header", kind: "header", visible: true }, { id: "main", kind: "main", visible: true }, { id: "toolbar", kind: "toolbar", visible: true }, { id: "sidebar", kind: "sidebar", visible: true }, ]; } function area(id: string, editor: EditorType, x: number, y: number, width: number, height: number): AreaIR { return { id, editor, regions: regions(), rect: { x, y, width, height }, maximized: false }; } function workspaceAreas(id: WorkspaceId): AreaIR[] { const right = [ area("outliner", "Outliner", 0.822, 0.819, 0.176, 0.179), area("properties", "Properties", 0.822, 0.022, 0.176, 0.794), ]; switch (id) { case "Layout": return [area("viewport", "3D Viewport", 0.001, 0.095, 0.819, 0.903), area("timeline", "Timeline", 0.001, 0.022, 0.819, 0.07), ...right]; case "Modeling": case "Sculpting": return [area("viewport", "3D Viewport", 0.001, 0.022, 0.819, 0.976), ...right]; case "UV Editing": case "Texture Paint": return [area("image-editor", "Image Editor", 0.001, 0.022, 0.409, 0.976), area("viewport", "3D Viewport", 0.412, 0.022, 0.408, 0.976), ...right]; case "Shading": return [ area("file-browser", "File Browser", 0.001, 0.511, 0.151, 0.487), area("viewport", "3D Viewport", 0.153, 0.511, 0.667, 0.487), area("image-editor", "Image Editor", 0.001, 0.022, 0.151, 0.487), area("shader-editor", "Shader Editor", 0.153, 0.022, 0.667, 0.487), ...right, ]; case "Animation": return [ area("camera-view", "3D Viewport", 0.001, 0.435, 0.291, 0.563), area("viewport", "3D Viewport", 0.294, 0.435, 0.526, 0.563), area("dope-sheet", "Dope Sheet", 0.001, 0.022, 0.819, 0.41), ...right, ]; case "Rendering": return [area("render-result", "Render Result", 0.001, 0.095, 0.819, 0.903), area("timeline", "Timeline", 0.001, 0.022, 0.819, 0.07), area("properties", "Properties", 0.822, 0.022, 0.176, 0.976)]; case "Compositing": return [area("image-editor", "Image Editor", 0.001, 0.552, 0.819, 0.446), area("properties", "Properties", 0.822, 0.552, 0.176, 0.446), area("compositor", "Compositor", 0.001, 0.071, 0.997, 0.476), area("timeline", "Timeline", 0.001, 0.022, 0.997, 0.046)]; case "Geometry Nodes": return [ area("spreadsheet", "Spreadsheet", 0.001, 0.552, 0.341, 0.446), area("viewport", "3D Viewport", 0.343, 0.552, 0.477, 0.446), area("geometry-nodes", "Geometry Nodes", 0.001, 0.095, 0.819, 0.452), area("timeline", "Timeline", 0.001, 0.022, 0.819, 0.07), ...right, ]; case "Scripting": return [ area("viewport", "3D Viewport", 0.001, 0.626, 0.291, 0.372), area("console", "Python Console", 0.001, 0.185, 0.291, 0.438), area("info", "Info", 0.001, 0.022, 0.291, 0.16), area("text-editor", "Text Editor", 0.294, 0.022, 0.526, 0.976), area("outliner", "Outliner", 0.822, 0.807, 0.176, 0.191), area("secondary-outliner", "Outliner", 0.822, 0.598, 0.176, 0.206), area("properties", "Properties", 0.822, 0.022, 0.176, 0.573), ]; } } function workspace(id: WorkspaceId): WorkspaceIR { const areas = workspaceAreas(id); const preferred = ({ "UV Editing": "image-editor", "Texture Paint": "image-editor", Shading: "shader-editor", Animation: "dope-sheet", Rendering: "render-result", Compositing: "compositor", "Geometry Nodes": "geometry-nodes", Scripting: "text-editor", } as Partial>)[id]; return { id, name: id, areas, activeAreaId: preferred ?? "viewport", revision: 0 }; } function defaultMode(id: WorkspaceId): BlenderMode { if (id === "Modeling" || id === "UV Editing") return "Edit"; if (id === "Sculpting") return "Sculpt"; if (id === "Texture Paint") return "Texture Paint"; return "Object"; } export function createDefaultWebWorkspaceState(): WebWorkspaceState { const workspaces = Object.fromEntries(BLENDER_WORKSPACE_IDS.map((id) => [id, workspace(id)])) as Record; return { workspaces, context: { workspaceId: "Layout", activeAreaId: "viewport", activeEditor: "3D Viewport", mode: "Object", activeObjectId: "BasicCube", selection: ["BasicCube"], viewLayer: "ViewLayer", pinnedData: null, revision: 0, }, operatorSearchOpen: false, openMenu: null, sidebarVisible: true, }; } export function reduceUICommand(state: WebWorkspaceState, command: UICommand): WebWorkspaceState { switch (command.type) { case "switchWorkspace": { const next = state.workspaces[command.workspaceId]; return { ...state, context: { ...state.context, workspaceId: command.workspaceId, activeAreaId: next.activeAreaId, activeEditor: next.areas.find((area) => area.id === next.activeAreaId)?.editor ?? "3D Viewport", mode: defaultMode(command.workspaceId), revision: state.context.revision + 1, }, }; } case "setMode": return { ...state, context: { ...state.context, mode: command.mode, revision: state.context.revision + 1 } }; case "setActiveArea": { const active = state.workspaces[state.context.workspaceId].areas.find((area) => area.id === command.areaId); if (!active) return state; return { ...state, context: { ...state.context, activeAreaId: active.id, activeEditor: active.editor, revision: state.context.revision + 1 }, }; } case "toggleOperatorSearch": return { ...state, operatorSearchOpen: command.open ?? !state.operatorSearchOpen, openMenu: command.open === false ? state.openMenu : null }; case "toggleMenu": return { ...state, openMenu: state.openMenu === command.menu ? null : command.menu ?? null, operatorSearchOpen: null === command.menu ? state.operatorSearchOpen : false }; case "toggleSidebar": return { ...state, sidebarVisible: command.visible ?? !state.sidebarVisible }; } }