diff --git a/tests/golden/corrective/rehydration/M12-manifest-sha-drift.json b/tests/golden/corrective/rehydration/M12-manifest-sha-drift.json index 78eb1eb99..30421e20c 100644 --- a/tests/golden/corrective/rehydration/M12-manifest-sha-drift.json +++ b/tests/golden/corrective/rehydration/M12-manifest-sha-drift.json @@ -514,7 +514,7 @@ "artifact": "app", "path": "web/app/src/app/App.tsx", "originalSha256": "74216aac70992f8d879e983237ca324b998008582f0cd4658e90994cf9fae0a8", - "observedSha256": "41279de5f12071d914a185c0813f537b8db85ae229f9ce61724fa36cac7cdf31", + "observedSha256": "a9c6e52bbb062506b42f78c9b71876544e2ea38305679228d41849c105db76ac", "provenanceState": "BLOCKED_SOURCE_LOST" }, { @@ -578,7 +578,7 @@ "artifact": "app", "path": "web/app/src/app/App.tsx", "originalSha256": "043ff3a2f39ef3a1303791081b80851b427e7db5dfd9af2161926dd6f1ea9ca4", - "observedSha256": "41279de5f12071d914a185c0813f537b8db85ae229f9ce61724fa36cac7cdf31", + "observedSha256": "a9c6e52bbb062506b42f78c9b71876544e2ea38305679228d41849c105db76ac", "provenanceState": "BLOCKED_SOURCE_LOST" }, { diff --git a/web/app/index.html b/web/app/index.html index 4cec62f4b..967191c8e 100644 --- a/web/app/index.html +++ b/web/app/index.html @@ -3,9 +3,9 @@ - + - Web Blender Modeler V1 + Blender 5.2.0 LTS - Web diff --git a/web/app/src/app/App.tsx b/web/app/src/app/App.tsx index 9ad050dae..498296e0e 100644 --- a/web/app/src/app/App.tsx +++ b/web/app/src/app/App.tsx @@ -82,7 +82,7 @@ import { } from "../volume/nanovdb-viewport"; import { composePaintColorPatch, composePaintWeightPatch, createSculptMeshAttributes, createSculptStroke } from "../../../protocol/paint"; import type { SculptBrush, SculptStrokeSampleIR } from "../../../protocol/sculpt"; -import { createDefaultWebWorkspaceState, reduceUICommand, type EditorType, type UICommand, type WorkspaceId } from "../../../protocol/ui-schema"; +import { BLENDER_WORKSPACE_IDS, createDefaultWebWorkspaceState, reduceUICommand, type EditorType, type UICommand } from "../../../protocol/ui-schema"; import { parseEditorWorkflow, type EditorWorkflowIR } from "../../../protocol/editor-workflow"; import { importExternalVFontIntoMain } from "../fonts/external-vfont-import"; import { createIMECompositionState, reduceIMEComposition, shouldBlockOperatorShortcuts, type IMECompositionEvent } from "../../../protocol/ime-composition"; @@ -138,7 +138,7 @@ function StorageBudgetPanel({ budget, onCleanup }: { budget: StorageBudgetResult ["媒体", budget.mediaBytes], ["VDB", budget.vdbBytes], ] as const : []; - return
存储预算{values.map(([label, bytes]) => {label} {formatStorageBytes(bytes)})}合计 {formatStorageBytes(budget?.totalBytes ?? 0)}
; + return
Storage{values.map(([label, bytes]) => {label} {formatStorageBytes(bytes)})}{formatStorageBytes(budget?.totalBytes ?? 0)}
; } interface OperatorPaletteCommand { @@ -744,10 +744,7 @@ function ViewportPlaceholder({ snapshot, geometryBuffers, nonMeshGeometryBuffers
- {viewportError || !snapshot || snapshot.nodes.length === 0 ?
- 3D Viewport - {viewportError ? WebGL 不可用,已保留场景编辑界面:{viewportError} : Three.js WebGL2 适配器} -
: null} + {viewportError ?
3D ViewportWebGL 不可用,已保留场景编辑界面:{viewportError}
: null} {editMode && activeTool === "marquee" && marqueeDrawing ?
{marqueeBox ? : null}
: null}
@@ -1461,9 +1458,9 @@ function Properties({ client, snapshot, editorWorkflow, selectedFaceIndices, sel return (
- - - + + +

Transform

{activeNode ?

Object & Hierarchy

: 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
+
-
{workerFaultTestMode ? : null}
+
SceneViewLayer{workerFaultTestMode ? : 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.{workerRecoveryStatus === "FAILED" ? Recovery failed; retry is safe. : null}
: null} {recentProjectIssues.length > 0 ?
最近项目中有 {recentProjectIssues.length} 个条目无法验证;当前场景不会被删除。
{recentProjectIssues.map((issue) =>
{issue.project.displayName}: {recentProjectIssueMessage(issue.code)}
)}
: null} - void cleanupProjectAssets()} />
{workspaceLabel}{uiState.context.mode === "Edit" ? <>
{(["VERT", "EDGE", "FACE"] as MeshElementMode[]).map((mode) => )}
{(["MERGE", "DISSOLVE", "EXTRUDE", "INSET", "BEVEL", "LOOP_CUT", "SUBDIVIDE", "FILL", "TRIANGULATE", "RECALCULATE_NORMALS", "BRIDGE_LOOPS", "DELETE"] as MeshEditOperation[]).map((operation) => )} : <>{(["CUBE", "PLANE", "UV_SPHERE", "ICO_SPHERE", "CYLINDER", "CONE", "TORUS"] as MeshPrimitive[]).map((primitive) => )}}{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 V1Objects {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 ZoomCollection | 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"); + }); });