228 lines
7.8 KiB
TypeScript
228 lines
7.8 KiB
TypeScript
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<WorkspaceId, WorkspaceIR>;
|
|
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<Record<WorkspaceId, string>>)[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<WorkspaceId, WorkspaceIR>;
|
|
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 };
|
|
}
|
|
}
|