@@ -1461,9 +1458,9 @@ function Properties({ client, snapshot, editorWorkflow, selectedFaceIndices, sel
return (
- 对象
- 修改器
- 材质
+ ▣ 对象
+ ⌕ 修改器
+ ● 材质
Transform 位置 0.000, 0.000, 0.000 旋转 0°, 0°, 0° 缩放 1.000, 1.000, 1.000
{activeNode ?
: null}
@@ -3463,7 +3460,7 @@ export function App() {
...(snapshot?.activeObjectId ? [{ id: "selectObject", label: "Select Active Object", keywords: "browser selection outliner", execute: () => selectObject(snapshot.activeObjectId!, false) }] : []),
{ id: "focusViewport", label: "Focus 3D Viewport", keywords: "browser view zoom pan orbit", execute: () => dispatchUI({ type: "setMode", mode: "Object" }) },
{ id: "focusEditor", label: "Focus Current Editor", keywords: "browser editor area region", execute: () => dispatchUI({ type: "setMode", mode: uiState.context.mode }) },
- ...(["Layout", "Modeling", "Animation"] as WorkspaceId[]).filter((id) => id !== workspace).map((id) => ({ id: `workspace.${id}`, label: `Switch to ${id}`, keywords: "workspace", execute: () => dispatchUI({ type: "switchWorkspace", workspaceId: id }) })),
+ ...BLENDER_WORKSPACE_IDS.filter((id) => id !== workspace).map((id) => ({ id: `workspace.${id}`, label: `Switch to ${id}`, keywords: "workspace", execute: () => dispatchUI({ type: "switchWorkspace", workspaceId: id }) })),
{ id: "mode.toggle", label: uiState.context.mode === "Object" ? "Enter Edit Mode" : "Exit Edit Mode", keywords: "mode tab", execute: () => dispatchUI({ type: "setMode", mode: uiState.context.mode === "Object" ? "Edit" : "Object" }) },
...(snapshot && uiState.context.mode === "Object" && isWorkbenchCommandAvailable("createPrimitive") ? ([
["CUBE", "Cube"], ["PLANE", "Plane"], ["UV_SPHERE", "UV Sphere"], ["ICO_SPHERE", "Ico Sphere"],
@@ -3513,17 +3510,17 @@ export function App() {
data-grease-pencil-selection-drawing-id={greasePencilSelection?.drawing.drawingId ?? ""}
data-diagnostic-count={diagnostics.length} data-last-diagnostic-code={diagnostics.at(-1)?.code}>
- ◈ Web Blender Modeler V1
+ Blender
{(["文件", "编辑", "渲染", "窗口", "帮助"] as const).map((menu) => { menuTriggerRefs.current[menu] = element; }} type="button" aria-haspopup="menu" aria-expanded={uiState.openMenu === menu} onClick={() => dispatchUI({ type: "toggleMenu", menu })}>{menu} {uiState.openMenu === menu ? { closeMenu(); if (label === "打开") fileInputRef.current?.click(); else if (label === "保存") void saveBlend(); else if (label === "关闭") closeProject(); else if (label === "撤销") void applyEditCommand({ type: "undo" }); else if (label === "重做") void applyEditCommand({ type: "redo" }); else if (label === "导出 GLB") void reportGLBExport(); }} /> : null} )}
- {(["Layout", "Modeling", "Animation"] as WorkspaceId[]).map((item) => dispatchUI({ type: "switchWorkspace", workspaceId: item })}>{item} )}
+ {BLENDER_WORKSPACE_IDS.map((item) => dispatchUI({ type: "switchWorkspace", workspaceId: item })}>{item} )}
+ +
- { const projectId = event.target.value; const displayName = event.target.selectedOptions[0]?.textContent ?? projectId; if (!projectId) return; void recoverCachedProject(projectId, displayName); event.currentTarget.value = ""; }}>最近项目 ({recentProjects.length}) {recentProjects.map((project) => {project.displayName} )} fileInputRef.current?.click()}>打开 关闭 void recoverCachedProject()}>恢复 void saveBlend()}>保存 void reportGLBExport()}>GLB 诊断 void applyEditCommand({ type: "undo" })}>↶ void applyEditCommand({ type: "redo" })}>↷ dispatchUI({ type: "toggleOperatorSearch", open: true })}>F3 {workerFaultTestMode ? Fault : null}
+ Scene ViewLayer { const projectId = event.target.value; const displayName = event.target.selectedOptions[0]?.textContent ?? projectId; if (!projectId) return; void recoverCachedProject(projectId, displayName); event.currentTarget.value = ""; }}>Recent ({recentProjects.length}) {recentProjects.map((project) => {project.displayName} )} fileInputRef.current?.click()}>⌕ × void recoverCachedProject()}>↺ void saveBlend()}>▣ void reportGLBExport()}>GLB ! void applyEditCommand({ type: "undo" })}>↶ void applyEditCommand({ type: "redo" })}>↷ dispatchUI({ type: "toggleOperatorSearch", open: true })}>F3 {workerFaultTestMode ? Fault : null}
route.format).join(",")} onChange={(event) => { const file = event.target.files?.[0]; if (file) { const gate = gateIOFormatFileSelection(file.name, IO_FORMAT_UI_REGISTRY); if (gate.status === "BLOCKED") setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", gate, { fileName: file.name })); else if (file.name.toLowerCase().endsWith(".blend")) void openBlendFile(file); else { const route = IO_FORMAT_UI_REGISTRY.importRoutes.find((candidate) => candidate.execution === "LOCAL" && candidate.extensions.some((extension) => file.name.toLowerCase().endsWith(extension))); if (route) void importSceneFile(file, route.format); else setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", gate, { fileName: file.name })); } } event.target.value = ""; }} />
{workerFault ?
{workerFault.source === "engine" ? "Engine Worker" : "Storage Worker"} stopped; current project list and scene are retained. void restartWorkersAndRecover()} disabled={workerRecoveryStatus === "RUNNING"}>{workerRecoveryStatus === "RUNNING" ? "恢复中..." : "重启并恢复"} {workerRecoveryStatus === "FAILED" ? Recovery failed; retry is safe. : null}
: null}
{recentProjectIssues.length > 0 ?
最近项目中有 {recentProjectIssues.length} 个条目无法验证;当前场景不会被删除。 {recentProjectIssues.map((issue) =>
{issue.project.displayName}: {recentProjectIssueMessage(issue.code)} void removeInvalidRecentProject(issue.project.projectId)}>移除引用
)}
: null}
-
void cleanupProjectAssets()} />
{workspaceLabel} dispatchUI({ type: "setMode", mode: uiState.context.mode === "Object" ? "Edit" : "Object" })}>{uiState.context.mode} Mode {uiState.context.mode === "Edit" ? <>
{(["VERT", "EDGE", "FACE"] as MeshElementMode[]).map((mode) => setMeshSelectionMode(mode)}>{mode === "VERT" ? "1 Vertex" : mode === "EDGE" ? "2 Edge" : "3 Face"} )}
Select All {(["MERGE", "DISSOLVE", "EXTRUDE", "INSET", "BEVEL", "LOOP_CUT", "SUBDIVIDE", "FILL", "TRIANGULATE", "RECALCULATE_NORMALS", "BRIDGE_LOOPS", "DELETE"] as MeshEditOperation[]).map((operation) =>
runMeshEdit(operation)}>{operation.replaceAll("_", " ")} )}
snapshot?.activeObjectId && void applyEditCommand({ type: "separateMeshFaces", objectId: snapshot.activeObjectId, faceIndices: [...meshSelection.indices], name: "Separated" })}>Separate > : <>{(["CUBE", "PLANE", "UV_SPHERE", "ICO_SPHERE", "CYLINDER", "CONE", "TORUS"] as MeshPrimitive[]).map((primitive) =>
void applyEditCommand(coreSceneCommands.createPrimitive(primitive))}>Add {primitive.replaceAll("_", " ")} )}
snapshot?.activeObjectId && void applyEditCommand({ type: "duplicateObject", objectId: snapshot.activeObjectId, offset: [0.25, 0.25, 0] })}>Duplicate snapshot?.activeObjectId && void applyEditCommand({ type: "duplicateObject", objectId: snapshot.activeObjectId, offset: [0.5, 0.5, 0], linked: true })}>Linked Duplicate snapshot?.activeObjectId && void applyEditCommand({ type: "deleteObject", objectId: snapshot.activeObjectId })}>Delete { const child = snapshot?.activeObjectId; const parent = [...selectedObjectIds].find((id) => id !== child); if (child && parent) void applyEditCommand({ type: "setParent", objectId: child, parentId: parent, keepTransform: true }); }}>Parent snapshot?.activeObjectId && void applyEditCommand({ type: "setParent", objectId: snapshot.activeObjectId, parentId: null, keepTransform: true })}>Unparent snapshot?.activeObjectId && void applyEditCommand({ type: "joinObjects", activeObjectId: snapshot.activeObjectId, objectIds: [...selectedObjectIds] })}>Join void applyEditCommand({ type: "createCollection", name: `Collection ${(snapshot?.collections.length ?? 0) + 1}` })}>New Collection >}
{uiState.context.mode === "Edit" ? `${meshSelection.greasePencilPoints?.length ?? meshSelection.indices.size} ${meshSelection.mode.toLowerCase()} selected` : `${selectedObjectIds.size} selected`} {saved ? "已保存" : "未保存"}
@@ -3532,7 +3529,7 @@ export function App() {
void applyEditCommand({ type: "setFrame", frame: value })} onCommand={(command) => void applyEditCommand(command)} />
{uiState.operatorSearchOpen ? { setSnapshot(result.snapshot); setGeometryBuffers(result.geometryBuffers); setNonMeshGeometryBuffers(result.nonMeshGeometryBuffers ?? []); setFrame(result.snapshot.frame.current); projectRevisionRef.current = result.snapshot.revision; const routeLabel = (route as unknown as { identifier?: string; id?: string }).identifier ?? (route as unknown as { id?: string }).id ?? "unknown"; setEngineStatus(`Route: ${routeLabel}`); }} /> : null}
- Web Blender Modeler V1 Objects {objectCount} · Vertices {vertexCount} · Faces {faceCount} {openProgress ? 0 ? openProgress.totalBytes : 1} value={openProgress.bytesRead ?? openProgress.fraction ?? 0} />{openProgress.message ?? "Opening"} {canCancelOpenRead ? × : null} : null}{manifestStatus} {wasmStatus} {engineStatus} {storageStatus}
+ LMB Select · MMB Rotate View · Wheel Zoom Collection | Cube · Objects {objectCount} · Vertices {vertexCount} · Faces {faceCount} {openProgress ? 0 ? openProgress.totalBytes : 1} value={openProgress.bytesRead ?? openProgress.fraction ?? 0} />{openProgress.message ?? "Opening"} {canCancelOpenRead ? × : null} : null}{manifestStatus} {wasmStatus} {engineStatus} {storageStatus} void cleanupProjectAssets()} />5.2.0 LTS
);
}
diff --git a/web/app/src/app/app-shell.css b/web/app/src/app/app-shell.css
index 5c32719ea..4e8811e26 100644
--- a/web/app/src/app/app-shell.css
+++ b/web/app/src/app/app-shell.css
@@ -1,102 +1,129 @@
:root {
- color: #e5e7eb;
- background: #18191c;
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
- font-size: 13px;
+ color: #d9d9d9;
+ background: #181818;
+ font-family: "Noto Sans", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
+ font-size: 12px;
font-synthesis: none;
}
* { box-sizing: border-box; }
-body { margin: 0; min-width: 320px; min-height: 100vh; overflow: hidden; }
-button, input { font: inherit; }
+body { margin: 0; min-width: 320px; min-height: 100vh; overflow: hidden; background: #181818; }
+button, input, select, textarea { font: inherit; letter-spacing: 0; }
button { color: inherit; border: 0; cursor: pointer; }
-button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible { outline: 2px solid #f2a15b; outline-offset: 1px; }
+button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible { outline: 2px solid #77b9f4; outline-offset: -2px; }
-.blender-app { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 38px 34px 30px minmax(0, 1fr) 24px; width: 100%; height: 100vh; min-height: 480px; background: #202124; }
-.topbar, .workspace-toolbar, .status-bar { display: flex; align-items: center; gap: 8px; padding: 0 10px; background: #27282b; border-bottom: 1px solid #111214; }
-.topbar { gap: 14px; }
-.brand { display: flex; align-items: center; gap: 7px; min-width: 120px; font-weight: 700; color: #f4f4f5; }
-.brand-mark { color: #e37a2c; font-size: 18px; }
-.menu-bar, .workspace-tabs, .topbar-actions { display: flex; align-items: center; gap: 2px; }.menu-entry { position: relative; display: inline-flex; }
-.menu-bar button, .topbar-actions button, .workspace-tab, .workspace-toolbar button { padding: 5px 8px; background: transparent; border-radius: 3px; color: #bfc2c8; }
-.menu-bar button:hover, .topbar-actions button:hover, .workspace-tab:hover, .workspace-toolbar button:hover { background: #3a3c41; color: #fff; }
-.topbar-actions button:disabled { color: #6f737b; background: transparent; cursor: default; }
-.workspace-tabs { align-self: stretch; gap: 0; }
-.workspace-tab { min-width: 82px; border-bottom: 2px solid transparent; border-radius: 0; }
-.workspace-tab.active { color: #fff; background: #313338; border-bottom-color: #e37a2c; }
-.topbar-actions { margin-left: auto; }
-.workspace-toolbar { min-width: 0; overflow-x: auto; color: #aeb2bb; border-bottom-color: #111214; white-space: nowrap; scrollbar-width: thin; }
-.mode-chip { padding: 3px 8px; color: #dedfe2; background: #35373c; border-radius: 3px; }
-.segmented { display: inline-flex; flex: none; border: 1px solid #4a4d54; border-radius: 3px; overflow: hidden; }.segmented button { border-radius: 0; border-right: 1px solid #4a4d54; }.segmented button:last-child { border-right: 0; }.segmented button.active { color: #fff; background: #a55325; }
+.blender-app { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 28px 30px minmax(0, 1fr) 20px; width: 100%; height: 100vh; min-height: 480px; background: #202020; }
+.topbar, .workspace-toolbar, .status-bar { display: flex; align-items: center; min-width: 0; background: #1d1d1d; border-bottom: 1px solid #0f0f0f; }
+.topbar { gap: 2px; padding: 0 5px; overflow: hidden; box-shadow: inset 0 -1px #303030; }
+.brand { display: flex; align-items: center; flex: none; gap: 5px; height: 100%; padding: 0 4px 0 2px; color: #f2f2f2; font-weight: 600; }
+.brand-name { font-size: 11px; }
+.brand-mark { position: relative; display: block; width: 21px; height: 14px; }
+.brand-mark::before { position: absolute; left: 1px; top: 4px; width: 13px; height: 7px; background: #ea7600; border-radius: 60% 35% 60% 35%; transform: rotate(-12deg); content: ""; }
+.brand-mark::after { position: absolute; left: 9px; top: 3px; width: 8px; height: 8px; border: 2px solid #ea7600; border-radius: 50%; background: #477eaf; content: ""; }
+.brand-orbit { position: absolute; left: 0; top: 1px; width: 10px; height: 3px; border-top: 2px solid #ea7600; transform: rotate(17deg); }
+.menu-bar, .workspace-tabs, .topbar-actions { display: flex; align-items: center; min-width: 0; gap: 1px; }.menu-entry { position: relative; display: inline-flex; flex: none; }
+.menu-bar { flex: none; }
+.menu-bar button, .topbar-actions button, .workspace-tab, .workspace-add, .workspace-toolbar button { min-height: 24px; padding: 3px 7px; color: #c9c9c9; background: transparent; border-radius: 2px; }
+.menu-bar button:hover, .topbar-actions button:hover, .workspace-tab:hover, .workspace-add:hover, .workspace-toolbar button:hover { color: #fff; background: #3a3a3a; }
+.topbar-actions button:disabled { color: #606060; background: transparent; cursor: default; }
+.workspace-tabs { flex: 1 1 auto; align-self: stretch; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
+.workspace-tabs::-webkit-scrollbar, .topbar::-webkit-scrollbar, .workspace-toolbar::-webkit-scrollbar { display: none; }
+.workspace-tab { flex: none; min-width: auto; padding-inline: 9px; border: 0; border-radius: 0; font-size: 10px; }
+.workspace-tab.active { color: #fff; background: #303030; box-shadow: inset 0 -2px #4c94c8; }
+.workspace-add { flex: none; width: 24px; padding: 0; font-size: 16px; }
+.topbar-actions { flex: none; margin-left: auto; }
+.topbar-actions button { min-width: 24px; }
+.topbar-actions select { width: 78px; height: 22px; color: #cfcfcf; background: #292929; border: 1px solid #444; border-radius: 2px; }
+.topbar-context { padding: 2px 6px; color: #bdbdbd; border-left: 1px solid #383838; white-space: nowrap; }
+.workspace-toolbar { gap: 2px; padding: 0 6px; overflow-x: auto; color: #bababa; background: #292929; white-space: nowrap; scrollbar-width: none; }
+.workspace-toolbar::before { flex: none; color: #e67e22; font-size: 14px; content: "◇"; }
+.workspace-toolbar > span:first-child { display: none; }
+.mode-chip { min-width: 86px; padding: 2px 8px; color: #e3e3e3; background: #343434 !important; border: 1px solid #484848; }
+.segmented { display: inline-flex; flex: none; height: 22px; border: 1px solid #4b4b4b; border-radius: 2px; overflow: hidden; }.segmented button { min-height: 20px; border-radius: 0; border-right: 1px solid #4b4b4b; }.segmented button:last-child { border-right: 0; }.segmented button.active { color: #fff; background: #4775a3; }
.toolbar-spacer, .header-spacer, .status-spacer { flex: 1; }
-.workspace-grid { display: grid; grid-template-columns: minmax(0, 1fr) 280px; grid-template-rows: minmax(0, 1fr) 164px; min-height: 0; gap: 1px; background: #111214; }
-.area-frame { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; background: #24262a; }
-.editor-header { display: flex; align-items: center; min-height: 28px; padding: 0 6px; background: #303238; border-bottom: 1px solid #17181b; }
+.workspace-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; grid-template-rows: minmax(180px, 1fr) minmax(220px, 2fr) 108px; min-height: 0; gap: 1px; background: #0f0f0f; }
+.area-frame { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; background: #252525; }
+.editor-header { display: flex; align-items: center; flex: none; min-height: 27px; padding: 0 4px; background: #292929; border-bottom: 1px solid #151515; box-shadow: inset 0 -1px #333; }
.editor-selector, .icon-button { display: inline-flex; align-items: center; gap: 6px; min-width: 24px; min-height: 24px; padding: 4px 6px; background: transparent; color: #e1e3e7; border-radius: 3px; }
-.editor-selector:hover, .icon-button:hover { background: #41434a; }
-.editor-icon { color: #e37a2c; font-size: 14px; }
-.icon-button { color: #a9adb5; }
+.editor-selector:hover, .icon-button:hover { background: #414141; }
+.editor-icon { color: #e67e22; font-size: 14px; }
+.icon-button { color: #aaa; }
.area-content { position: relative; flex: 1; min-height: 0; overflow: auto; }
-.viewport-area { grid-column: 1; grid-row: 1; }
+.viewport-area { grid-column: 1; grid-row: 1 / 3; }
.outliner-area { grid-column: 2; grid-row: 1; }
-.properties-area { grid-column: 2; grid-row: 2; }
-.timeline-area { grid-column: 1; grid-row: 2; }
-.viewport-placeholder { position: relative; width: 100%; height: 100%; min-height: 260px; overflow: hidden; background: #25272b; }
+.properties-area { grid-column: 2; grid-row: 2 / 4; }
+.timeline-area { grid-column: 1; grid-row: 3; }
+.viewport-area > .editor-header, .timeline-area > .editor-header { display: none; }
+.viewport-placeholder { position: relative; width: 100%; height: 100%; min-height: 260px; overflow: hidden; background-color: #393939; background-image: linear-gradient(#414141 1px, transparent 1px), linear-gradient(90deg, #414141 1px, transparent 1px); background-size: 32px 32px; }
.viewport-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
.grease-pencil-marquee-surface { position: absolute; inset: 0; z-index: 1; cursor: crosshair; touch-action: none; }
.grease-pencil-marquee-box { position: absolute; border: 1px solid #7fc4ff; background: #4c9ed126; box-shadow: 0 0 0 1px #17212aaa; pointer-events: none; }
.viewport-grid { position: absolute; inset: 0; opacity: .3; background-image: linear-gradient(#62656b 1px, transparent 1px), linear-gradient(90deg, #62656b 1px, transparent 1px); background-size: 32px 32px; transform: perspective(500px) rotateX(58deg) scale(1.7); transform-origin: 50% 100%; }
-.viewport-placeholder::after { position: absolute; inset: 48% 0 0; border-top: 1px solid #777b83; content: ""; opacity: .5; pointer-events: none; }
+.viewport-placeholder::after { position: absolute; inset: 50% 0 0; border-top: 1px solid #505050; content: ""; pointer-events: none; }
.viewport-message { position: absolute; top: 50%; left: 50%; z-index: 1; display: grid; gap: 5px; transform: translate(-50%, -50%); text-align: center; color: #c8cbd0; }
.viewport-message strong { color: #f3f4f6; font-size: 15px; }
.viewport-message span { color: #969ba4; }
-.axis-gizmo { position: absolute; top: 14px; right: 14px; z-index: 2; width: 54px; height: 54px; border: 1px solid #545860; border-radius: 50%; color: #c2c5ca; font-size: 11px; }
-.axis-gizmo span { position: absolute; padding: 2px; border-radius: 2px; background: #35373d; }.axis-x { right: -3px; top: 23px; color: #f06a6a; }.axis-y { left: 21px; top: -4px; color: #75d18d; }.axis-z { left: 3px; bottom: 4px; color: #6fa4f5; }
-.viewport-toolbar { position: absolute; top: 14px; left: 12px; z-index: 2; display: grid; gap: 3px; padding: 4px; background: #303238d9; border: 1px solid #45484f; border-radius: 4px; }.tool-button { width: 27px; height: 27px; background: transparent; color: #c7cad0; border-radius: 3px; }.tool-button:hover, .tool-button.active { color: #fff; background: #d26928; }
+.axis-gizmo { position: absolute; top: 13px; right: 16px; z-index: 2; width: 54px; height: 54px; color: #c2c5ca; font-size: 11px; }
+.axis-gizmo span { position: absolute; min-width: 18px; padding: 2px; text-align: center; border-radius: 50%; background: #353535; box-shadow: 0 0 0 1px #202020; }.axis-x { right: -3px; top: 23px; color: #ed6a6a; }.axis-y { left: 21px; top: -4px; color: #6fc47d; }.axis-z { left: 3px; bottom: 4px; color: #6e9ee8; }
+.viewport-toolbar { position: absolute; top: 12px; left: 7px; z-index: 2; display: grid; gap: 2px; padding: 3px; background: #292929e8; border: 1px solid #4a4a4a; border-radius: 3px; box-shadow: 0 2px 8px #0005; }.tool-button { width: 28px; height: 28px; background: transparent; color: #ccc; border-radius: 2px; }.tool-button:hover, .tool-button.active { color: #fff; background: #4775a3; }
.tool-button:disabled { color: #666a72; cursor: default; }.tool-button:disabled:hover { background: transparent; }
.transform-gizmo { position: absolute; left: 50%; top: 50%; z-index: 2; width: 92px; height: 92px; transform: translate(-46px, -46px); pointer-events: none; }
.transform-gizmo.handle-local::before { position: absolute; left: 42px; top: 42px; width: 8px; height: 8px; border: 1px solid #fff; border-radius: 50%; background: #24262b; box-shadow: 0 0 0 2px #24262baa; content: ""; }
.gizmo-axis { position: absolute; width: 28px; height: 28px; padding: 0; border: 2px solid currentColor; border-radius: 50%; background: #24262bcc; font-weight: 700; pointer-events: auto; touch-action: none; }
.gizmo-axis.axis-x { left: 60px; top: 32px; color: #e35b55; }.gizmo-axis.axis-y { left: 32px; top: 4px; color: #65bb70; }.gizmo-axis.axis-z { left: 32px; top: 60px; color: #5d8ee8; }
-.viewport-sidebar { position: absolute; top: 14px; right: 12px; z-index: 2; display: grid; gap: 7px; padding: 9px; color: #aeb3bc; background: #303238d9; border: 1px solid #45484f; border-radius: 4px; }.viewport-sidebar span { writing-mode: vertical-rl; }
-.outliner-content, .properties-content { padding: 8px; color: #c8cbd0; }
-.outliner-tools { display: flex; gap: 4px; margin-bottom: 8px; }.outliner-tools input { min-width: 0; flex: 1; padding: 5px 7px; color: #e4e6ea; background: #1d1f22; border: 1px solid #464950; border-radius: 3px; outline: none; }.outliner-tools button { width: 30px; background: #3a3c42; border-radius: 3px; }
-.tree-row { display: flex; align-items: center; gap: 7px; min-height: 26px; padding: 2px 5px; border-radius: 3px; }.tree-row.child { padding-left: 24px; }.tree-row.selected { color: #fff; background: #a55325; }.tree-icon { color: #d7a04b; }.tree-icon.mesh { color: #72a7dc; }.tree-action { width: 24px; min-width: 24px; height: 24px; margin-left: auto; padding: 2px 4px; color: #8f949d; background: transparent; border: 0; border-radius: 3px; }.tree-action:hover { color: #fff; background: #41434a; }
-.property-tabs { display: flex; gap: 2px; margin-bottom: 8px; border-bottom: 1px solid #42454b; }.property-tab { padding: 6px 8px; color: #aeb3bc; background: transparent; border-bottom: 2px solid transparent; }.property-tab.active { color: #fff; border-bottom-color: #e37a2c; }.property-section { padding: 8px 0; border-bottom: 1px solid #383a40; }.property-section h3 { margin: 0 0 8px; color: #eceef1; font-size: 12px; font-weight: 600; }.property-section label { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; color: #9fa4ad; }.property-section output { color: #e0e2e6; font-variant-numeric: tabular-nums; }.property-section select { min-width: 118px; padding: 3px 5px; color: #e0e2e6; background: #26282c; border: 1px solid #484c53; border-radius: 2px; }.property-section input[type="range"] { flex: 1; min-width: 78px; accent-color: #e37a2c; }.modifier-summary { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; color: #7f858e; font-size: 11px; }.modifier-summary button { padding: 4px 9px; color: #fff; background: #b45f29; border: 1px solid #dc8243; border-radius: 2px; }.modifier-summary button:disabled { color: #777c84; background: #303237; border-color: #45484e; }.swatch { width: 32px; height: 14px; background: #a35d43; border: 1px solid #d18a68; border-radius: 2px; }
+.viewport-sidebar { position: absolute; top: 72px; right: 0; z-index: 2; display: flex; gap: 1px; padding: 0; color: #c2c2c2; background: #292929e8; border: 1px solid #484848; border-right: 0; border-radius: 3px 0 0 3px; }.viewport-sidebar span { padding: 8px 5px; writing-mode: vertical-rl; }
+.outliner-content { padding: 3px 2px; color: #d0d0d0; }
+.properties-content { position: relative; display: flex; flex-direction: column; min-height: 100%; padding: 4px 7px 12px 40px; color: #d0d0d0; background: #303030; }
+.outliner-tools { display: flex; gap: 3px; margin: 0 2px 4px; }.outliner-tools input { min-width: 0; height: 23px; flex: 1; padding: 3px 7px; color: #e4e4e4; background: #181818; border: 1px solid #454545; border-radius: 3px; outline: none; }.outliner-tools button { width: 27px; background: #383838; border-radius: 2px; }
+.tree-row { display: flex; align-items: center; gap: 5px; min-height: 24px; padding: 1px 4px; border-radius: 0; }.tree-row:hover { background: #353535; }.tree-row.child { padding-left: 26px; }.tree-row.selected { color: #fff; background: #4775a3; }.tree-icon { color: #d59b44; }.tree-icon.mesh { color: #6999c8; }.tree-action { width: 24px; min-width: 24px; height: 24px; margin-left: auto; padding: 1px 3px; color: #a6a6a6; background: transparent; border: 0; border-radius: 2px; }.tree-action:hover { color: #fff; background: #4a4a4a; }
+.property-tabs { position: absolute; left: 0; top: 0; bottom: 0; display: flex; flex-direction: column; gap: 1px; width: 34px; margin: 0; padding: 4px 3px; background: #242424; border-right: 1px solid #151515; }.property-tab { display: grid; place-items: center; width: 27px; height: 27px; padding: 0; color: #aaa; background: transparent; border-radius: 2px; }.property-tab:hover { color: #fff; background: #404040; }.property-tab.active { color: #fff; background: #4775a3; }.property-tab-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }.property-section { padding: 7px 3px 9px; border-bottom: 1px solid #454545; }.property-section h3 { margin: 0 0 7px; padding: 4px 6px; color: #eee; background: #383838; border-radius: 2px; font-size: 11px; font-weight: 600; }.property-section label { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 23px; color: #b5b5b5; }.property-section output { display: block; color: #ddd; font-variant-numeric: tabular-nums; }.property-section select { min-width: 118px; padding: 3px 5px; color: #e0e0e0; background: #252525; border: 1px solid #505050; border-radius: 2px; }.property-section input[type="range"] { flex: 1; min-width: 78px; accent-color: #4775a3; }.modifier-summary { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; color: #8e8e8e; font-size: 11px; }.modifier-summary button { padding: 4px 9px; color: #fff; background: #4775a3; border: 1px solid #6193c3; border-radius: 2px; }.modifier-summary button:disabled { color: #777; background: #303030; border-color: #454545; }.swatch { width: 32px; height: 14px; background: #a35d43; border: 1px solid #d18a68; border-radius: 2px; }
+.properties-content > [data-testid="scene-context-workbench"], .properties-content > [data-testid="editor-workflow-workbench"], .properties-content > [data-testid="capability-family-workbench"] { order: 90; }
+.properties-content > [data-testid="feature-route-coverage"], .properties-content > [data-testid="release-status-workbench"] { order: 100; }
+.property-section > button { min-height: 24px; margin: 2px; padding: 3px 7px; color: #e2e2e2; background: #454545; border: 1px solid #575757; border-radius: 2px; }
+.property-section > button:hover { background: #555; }
.modifier-row { display: grid; grid-template-columns: minmax(0, 1fr) repeat(4, 26px); align-items: center; gap: 3px; min-height: 26px; }.modifier-row > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.property-section .modifier-row label { justify-content: center; gap: 2px; min-height: 20px; font-size: 10px; }
.property-section input[type="number"] { width: 72px; padding: 3px 5px; color: #e0e2e6; background: #26282c; border: 1px solid #484c53; border-radius: 2px; }.property-section input[type="text"], .property-section label > input:not([type]) { min-width: 0; width: 130px; padding: 3px 5px; color: #e0e2e6; background: #26282c; border: 1px solid #484c53; border-radius: 2px; }.property-section input[type="checkbox"] { accent-color: #e37a2c; }.property-actions { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0; }.property-actions button, .file-button { padding: 4px 7px; color: #e6e7e9; background: #393c42; border: 1px solid #4d5158; border-radius: 2px; }.property-actions button:disabled { opacity: .45; cursor: default; }.file-button { position: relative; cursor: pointer; }.file-button input { position: absolute; width: 1px; height: 1px; opacity: 0; }.material-slots { display: grid; gap: 2px; max-height: 62px; overflow: auto; color: #b9bdc5; font-size: 11px; }.delimit-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 8px; margin: 6px 0; padding: 5px 0 3px; border: 0; border-top: 1px solid #383a40; }.delimit-options legend { padding: 0 5px 0 0; color: #7f858e; font-size: 10px; }.property-section .delimit-options label { justify-content: flex-start; min-width: 0; min-height: 20px; font-size: 10px; }
.grease-pencil-canvas-2d { display: block; width: 100%; max-width: 280px; aspect-ratio: 28 / 15; margin: 7px 0; border: 1px solid #4d5158; border-radius: 3px; background: #202328; cursor: crosshair; touch-action: none; }
.grease-pencil-canvas-2d:focus-visible { outline: 2px solid #f2a15b; outline-offset: 1px; }
-.timeline-content { display: grid; grid-template-rows: 34px 20px 16px minmax(30px, 1fr); height: 100%; padding: 6px 12px; }.timeline-controls { display: flex; align-items: center; gap: 4px; overflow-x: auto; }.timeline-controls button { min-width: 28px; height: 26px; padding: 0 6px; color: #c9ccd2; background: #35373d; border-radius: 3px; white-space: nowrap; }.timeline-controls button:hover { background: #4a4d54; }.frame-number { min-width: 45px; margin-left: 10px; padding: 5px 8px; text-align: center; color: #fff; background: #181a1d; border: 1px solid #4a4d54; border-radius: 3px; }.frame-slider { width: 100%; min-height: 24px; accent-color: #e37a2c; }.timeline-scale { display: flex; justify-content: space-between; color: #aeb3bc; font-size: 11px; }.dope-sheet { display: grid; grid-template-columns: 100px minmax(120px, 1fr) 90px; align-items: center; gap: 8px; border-top: 1px solid #3a3d43; }.channel-name { overflow: hidden; color: #b8bcc4; text-overflow: ellipsis; white-space: nowrap; }.key-track { position: relative; height: 20px; background: #1e2024; border: 1px solid #373a40; }.key-dot { position: absolute; top: 5px; width: 9px; height: 9px; padding: 0; transform: translateX(-50%) rotate(45deg); background: #d6a348; border: 1px solid #f2c977; }.key-dot.active { background: #e36d2d; }.dope-sheet select { min-width: 0; color: #ddd; background: #292b30; border: 1px solid #484c53; }
-.dope-sheet input { min-width: 24px; min-height: 32px; margin-block: 4px; }.keying-set-tools > button, .timeline-markers > button { min-height: 24px; padding: 3px 7px; color: #f1f2f4; background: #41444a; border: 1px solid #656a73; border-radius: 2px; }
-.status-bar { gap: 16px; min-width: 0; min-height: 24px; overflow: hidden; color: #8f949c; font-size: 11px; border: 0; }.status-bar span:not(.status-spacer) { white-space: nowrap; }
-.storage-budget-panel { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 0 10px; overflow-x: auto; color: #aeb2bb; background: #222427; border-bottom: 1px solid #111214; white-space: nowrap; scrollbar-width: thin; }.storage-budget-panel strong { color: #e4e6e9; font-size: 11px; }.storage-budget-panel span { color: #aeb2bb; font-variant-numeric: tabular-nums; }.storage-budget-panel output { margin-left: auto; color: #e37a2c; font-variant-numeric: tabular-nums; }.storage-budget-panel button { flex: none; padding: 3px 7px; color: #d9dce1; background: #35373d; border: 1px solid #4d5158; border-radius: 3px; }.storage-budget-panel button:disabled { opacity: .45; cursor: default; }
+.timeline-content { display: grid; grid-template-rows: 27px 24px 15px 28px; height: auto; min-height: 100%; padding: 0; background: #292929; }.timeline-controls { display: flex; align-items: center; justify-content: center; gap: 1px; padding: 1px 5px; overflow-x: auto; border-bottom: 1px solid #151515; scrollbar-width: none; }.timeline-controls button { min-width: 25px; height: 24px; padding: 0 5px; color: #d0d0d0; background: transparent; border-radius: 2px; white-space: nowrap; }.timeline-controls button:hover { background: #484848; }.frame-number { min-width: 48px; height: 22px; margin-left: 8px; padding: 3px 7px; text-align: center; color: #fff; background: #464646; border: 1px solid #545454; border-radius: 2px; }.frame-slider { width: 100%; min-height: 24px; margin: 0; accent-color: #4775a3; }.timeline-scale { display: flex; justify-content: space-between; padding: 0 7px; color: #b6b6b6; background: #303030; font-size: 10px; }.dope-sheet { display: grid; grid-template-columns: 90px minmax(120px, 1fr) 78px; align-items: center; gap: 5px; padding: 2px 5px; border-top: 1px solid #444; }.channel-name { overflow: hidden; color: #c0c0c0; text-overflow: ellipsis; white-space: nowrap; }.key-track { position: relative; height: 17px; background: #1f1f1f; border: 1px solid #3d3d3d; }.key-dot { position: absolute; top: 4px; width: 8px; height: 8px; padding: 0; transform: translateX(-50%) rotate(45deg); background: #d6a348; border: 1px solid #f2c977; }.key-dot.active { background: #e36d2d; }.dope-sheet select { min-width: 0; color: #ddd; background: #292929; border: 1px solid #484848; }
+.dope-sheet input { min-width: 80px; min-height: 24px; margin-block: 4px; }.keying-set-tools > button, .timeline-markers > button { min-height: 24px; padding: 3px 7px; color: #f1f2f4; background: #41444a; border: 1px solid #656a73; border-radius: 2px; }
+.status-bar { gap: 10px; min-width: 0; min-height: 20px; padding: 0 6px; overflow: hidden; color: #aaa; background: #181818; font-size: 10px; border: 0; }.status-bar span:not(.status-spacer) { white-space: nowrap; }.status-hint { color: #888; }.runtime-status:not([data-testid="engine-status"]) { display: none; }.blender-version { color: #7fa8cc; }
+.storage-budget-panel { display: flex; align-items: center; flex: none; gap: 5px; min-width: 0; height: 100%; padding: 0 4px; color: #aaa; background: transparent; border: 0; white-space: nowrap; }.storage-budget-panel strong { color: #aaa; font-size: 10px; font-weight: 500; }.storage-budget-panel span { display: none; color: #aaa; font-variant-numeric: tabular-nums; }.storage-budget-panel output { color: #c9c9c9; font-variant-numeric: tabular-nums; }.storage-budget-panel button { flex: none; width: 19px; height: 18px; padding: 0; color: #ccc; background: #303030; border: 1px solid #444; border-radius: 2px; }.storage-budget-panel button:disabled { opacity: .45; cursor: default; }
.open-progress { display: inline-flex; align-items: center; gap: 5px; min-width: 0; max-width: 360px; }.open-progress progress { width: 72px; height: 8px; accent-color: #e37a2c; }.open-progress > span { overflow: hidden; text-overflow: ellipsis; }.open-progress button { width: 20px; height: 20px; padding: 0; color: #d6d8dc; background: #3a3c41; border-radius: 2px; }
-.worker-fault-banner { position: fixed; top: 76px; left: 50%; z-index: 12; display: flex; align-items: center; gap: 10px; max-width: calc(100vw - 24px); padding: 8px 10px; transform: translateX(-50%); color: #f1f2f4; background: #3a2923; border: 1px solid #c6753b; border-radius: 4px; box-shadow: 0 8px 24px #0008; }.worker-fault-banner span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.worker-fault-banner button { flex: none; padding: 5px 9px; color: #fff; background: #a55325; border: 1px solid #d17a42; border-radius: 3px; }.worker-fault-banner button:disabled { opacity: .6; cursor: default; }
-.recent-project-repair-banner { position: fixed; top: 76px; left: 50%; z-index: 11; display: grid; gap: 6px; width: min(560px, calc(100vw - 24px)); padding: 9px 11px; transform: translateX(-50%); color: #f1f2f4; background: #332d22; border: 1px solid #b98a42; border-radius: 4px; box-shadow: 0 8px 24px #0008; }.recent-project-repair-list { display: grid; gap: 4px; max-height: 160px; overflow: auto; }.recent-project-repair-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; color: #d9d1bd; }.recent-project-repair-item > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.recent-project-repair-item button { flex: none; padding: 4px 8px; color: #fff; background: #765729; border: 1px solid #c49c58; border-radius: 3px; }.recent-project-repair-item button:hover { background: #92703a; }
+.worker-fault-banner { position: fixed; top: 62px; left: 50%; z-index: 12; display: flex; align-items: center; gap: 10px; max-width: calc(100vw - 24px); padding: 8px 10px; transform: translateX(-50%); color: #f1f2f4; background: #3a2923; border: 1px solid #c6753b; border-radius: 3px; box-shadow: 0 8px 24px #0008; }.worker-fault-banner span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.worker-fault-banner button { flex: none; padding: 5px 9px; color: #fff; background: #8d4b28; border: 1px solid #c6753b; border-radius: 2px; }.worker-fault-banner button:disabled { opacity: .6; cursor: default; }
+.recent-project-repair-banner { position: fixed; top: 62px; left: 50%; z-index: 11; display: grid; gap: 6px; width: min(560px, calc(100vw - 24px)); padding: 9px 11px; transform: translateX(-50%); color: #f1f2f4; background: #332d22; border: 1px solid #b98a42; border-radius: 3px; box-shadow: 0 8px 24px #0008; }.recent-project-repair-list { display: grid; gap: 4px; max-height: 160px; overflow: auto; }.recent-project-repair-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; color: #d9d1bd; }.recent-project-repair-item > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.recent-project-repair-item button { flex: none; padding: 4px 8px; color: #fff; background: #765729; border: 1px solid #c49c58; border-radius: 2px; }.recent-project-repair-item button:hover { background: #92703a; }
.file-input-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
-.operator-search { position: fixed; top: 56px; left: 50%; z-index: 10; width: min(480px, calc(100vw - 24px)); padding: 8px; transform: translateX(-50%); background: #303238; border: 1px solid #545860; border-radius: 4px; box-shadow: 0 12px 30px #0008; }
+.operator-search { position: fixed; top: 64px; left: 50%; z-index: 10; width: min(480px, calc(100vw - 24px)); padding: 7px; transform: translateX(-50%); background: #292929; border: 1px solid #5a5a5a; border-radius: 3px; box-shadow: 0 12px 30px #0008; }
.main-menu-popover { position: absolute; top: 100%; left: 0; z-index: 14; display: grid; min-width: 128px; padding: 4px; background: #303238; border: 1px solid #545860; border-radius: 3px; box-shadow: 0 8px 20px #0008; }.main-menu-popover button { padding: 6px 10px; text-align: left; color: #d9dce1; background: transparent; border-radius: 2px; white-space: nowrap; }.main-menu-popover button:hover, .main-menu-popover button:focus-visible { color: #fff; background: #a55325; }
.operator-search input { width: 100%; padding: 8px 10px; color: #f4f5f6; background: #1d1f22; border: 1px solid #5a5e66; border-radius: 3px; outline: none; }
.operator-results { display: grid; gap: 2px; margin-top: 6px; }.operator-results button { padding: 8px 10px; text-align: left; color: #d9dce1; background: transparent; border-radius: 3px; }.operator-results button:hover { color: #fff; background: #a55325; }
.outliner-tools input:focus-visible, .operator-search input:focus-visible { outline: 2px solid #f2a15b; outline-offset: 1px; }
.file-button:has(input:focus-visible) { outline: 2px solid #f2a15b; outline-offset: 1px; }
@media (max-width: 900px) {
- .topbar { min-width: 0; overflow-x: auto; scrollbar-width: none; }
- .topbar::-webkit-scrollbar { display: none; }
- .topbar > *, .topbar-actions button { flex: none; white-space: nowrap; }
- .topbar-actions { margin-left: 0; }
+ .blender-app { grid-template-rows: 28px 30px minmax(0, 1fr) 20px; }
+ .topbar { min-width: 0; overflow: hidden; }
+ .topbar > *, .topbar-actions button { white-space: nowrap; }
+ .brand-name, .topbar-context, .topbar-actions button:nth-of-type(2), .topbar-actions button:nth-of-type(3), .topbar-actions button:nth-of-type(5), .topbar-actions button:nth-of-type(6), .topbar-actions button:nth-of-type(7), .topbar-actions button:nth-of-type(8) { display: none; }
+ .topbar-actions { flex: none; margin-left: 0; }
+ .topbar-actions select { width: 62px; }
.menu-bar { display: none; }
- .workspace-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 150px; }
+ .workspace-tabs .workspace-tab:nth-child(n+4) { display: none; }
+ .workspace-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 108px; }
.outliner-area, .properties-area { display: none; }
.viewport-area, .timeline-area { grid-column: 1; }
.viewport-area { grid-row: 1; }
.timeline-area { grid-row: 2; }
- .workspace-tab { min-width: 64px; }
- .status-bar span:nth-child(2), .status-bar span:nth-child(4) { display: none; }
- .status-bar { overflow-x: auto; scrollbar-width: thin; }
- .timeline-content { padding-inline: 8px; }
+ .workspace-tab { min-width: auto; padding-inline: 8px; }
+ .status-hint, .status-bar > [data-testid="scene-stats"], .storage-budget-panel strong, .storage-budget-panel output, .blender-version { display: none; }
+ .status-bar { gap: 5px; overflow: hidden; }
+ .runtime-status[data-testid="engine-status"] { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.timeline-controls { gap: 2px; }
.timeline-controls button { min-width: 24px; padding-inline: 4px; font-size: 11px; }
.frame-number { min-width: 38px; margin-left: 4px; padding-inline: 5px; }
}
+
+@media (min-width: 901px) and (max-width: 1280px) {
+ .brand-name, .topbar-context { display: none; }
+ .workspace-grid { grid-template-columns: minmax(0, 1fr) 290px; }
+ .workspace-tab { padding-inline: 7px; }
+}
diff --git a/web/protocol/ui-schema.ts b/web/protocol/ui-schema.ts
index 71eb1c440..75ecbcd32 100644
--- a/web/protocol/ui-schema.ts
+++ b/web/protocol/ui-schema.ts
@@ -1,4 +1,18 @@
-export type WorkspaceId = "Layout" | "Modeling" | "Animation";
+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";
export type BlenderMode = "Object" | "Edit" | "Pose";
@@ -75,11 +89,7 @@ function workspace(id: WorkspaceId): WorkspaceIR {
}
export function createDefaultWebWorkspaceState(): WebWorkspaceState {
- const workspaces = {
- Layout: workspace("Layout"),
- Modeling: workspace("Modeling"),
- Animation: workspace("Animation"),
- } satisfies Record;
+ const workspaces = Object.fromEntries(BLENDER_WORKSPACE_IDS.map((id) => [id, workspace(id)])) as Record;
return {
workspaces,
context: {
diff --git a/web/tests/e2e/responsive-layout.spec.ts b/web/tests/e2e/responsive-layout.spec.ts
index f296c0e46..1e689a9ed 100644
--- a/web/tests/e2e/responsive-layout.spec.ts
+++ b/web/tests/e2e/responsive-layout.spec.ts
@@ -47,4 +47,41 @@ test.describe("M7-14 responsive layout", () => {
expect(report.rootScrollWidth, JSON.stringify(report)).toBeLessThanOrEqual(report.viewportWidth);
});
}
+
+ test("desktop mirrors Blender 5.2 Layout workspace regions", async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/");
+ await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
+
+ await expect(page.locator(".workspace-tab")).toHaveText([
+ "Layout", "Modeling", "Sculpting", "UV Editing", "Texture Paint", "Shading",
+ "Animation", "Rendering", "Compositing", "Geometry Nodes", "Scripting",
+ ]);
+ const geometry = await page.evaluate(() => {
+ const rect = (selector: string) => {
+ const bounds = document.querySelector(selector)?.getBoundingClientRect();
+ if (!bounds) throw new Error(`Missing ${selector}`);
+ return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, bottom: bounds.bottom };
+ };
+ const propertyTabs = document.querySelector(".property-tabs");
+ return {
+ viewport: rect(".viewport-area"),
+ outliner: rect(".outliner-area"),
+ properties: rect(".properties-area"),
+ timeline: rect(".timeline-area"),
+ status: rect(".status-bar"),
+ propertyTabsDirection: propertyTabs ? getComputedStyle(propertyTabs).flexDirection : "missing",
+ };
+ });
+
+ expect(geometry.viewport.width / 1440).toBeGreaterThan(0.75);
+ expect(geometry.outliner.x).toBeCloseTo(geometry.viewport.width + 1, 0);
+ expect(geometry.outliner.height).toBeLessThan(geometry.properties.height);
+ expect(geometry.properties.bottom).toBeCloseTo(geometry.status.y, 0);
+ expect(geometry.timeline.x).toBe(0);
+ expect(geometry.timeline.width).toBeCloseTo(geometry.viewport.width, 0);
+ expect(geometry.timeline.height).toBeGreaterThanOrEqual(96);
+ expect(geometry.timeline.height).toBeLessThanOrEqual(120);
+ expect(geometry.propertyTabsDirection).toBe("column");
+ });
});