From 55bfa7521a881e26e9b01dedbc0f66d85e3708e7 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Fri, 18 Sep 2026 07:29:58 +0200 Subject: [PATCH] The assets and scene objects panels in the left column are split by a draggable splitter that keeps its ratio when the window is resized Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/editor/Editor.ts | 23 ++++++++++++++++++++--- src/editor/debugHook.ts | 6 ++++++ tools/e2e/14-overflow.mjs | 27 +++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc86b0c2..409f13f0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1006 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1009 checks (GM_STEPS=20 for one step) node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs node tools/seed-actor.mjs && node tools/smoke-actor.mjs diff --git a/src/editor/Editor.ts b/src/editor/Editor.ts index 582a7e1d..65a69b51 100644 --- a/src/editor/Editor.ts +++ b/src/editor/Editor.ts @@ -115,6 +115,8 @@ const MAX_RIGHT_WIDTH = 600; // The floor is the console's own header: dragged all the way shut it stays visible to drag open. const MIN_CONSOLE_HEIGHT = Panel.HEADER_HEIGHT; const MAX_CONSOLE_HEIGHT = 600; +// Either panel of the left column keeps at least its header, for the same reason as the console. +const MIN_PANEL_HEIGHT = Panel.HEADER_HEIGHT; /** * Composes toolbar, panels, viewports and graph editor into the one application that edits and @@ -151,6 +153,8 @@ export class Editor implements PlayHost { readonly leftSplitter = new Splitter('x'); readonly rightSplitter = new Splitter('x'); readonly consoleSplitter = new Splitter('y'); + /** Between the asset panel and the scene objects below it. */ + readonly columnSplitter = new Splitter('y'); private readonly workspace: HStack; private readonly centre: VStack; private readonly tabs: TabStrip; @@ -165,6 +169,8 @@ export class Editor implements PlayHost { private leftWidth = 250; private rightWidth = 350; private consoleHeight = 300; + /** The asset panel's part of the left column's height; the scene objects get the rest. */ + private assetsShare = 0.5; constructor( app: Application, @@ -219,11 +225,22 @@ export class Editor implements PlayHost { root.add(this.toolbar); this.workspace = new HStack(); - this.leftColumn = new VStack({ gap: 1 }); + this.leftColumn = new VStack(); this.assets = new AssetPanel(state, textures, fonts, sounds); - this.leftColumn.add(this.assets, 1); + this.leftColumn.add(this.assets, this.assetsShare); installFileImport(app.canvas, { state, textures, fonts, sounds }); - this.leftColumn.add(this.hierarchy, 1); + this.leftColumn.add(this.columnSplitter); + this.leftColumn.add(this.hierarchy, 1 - this.assetsShare); + // A share rather than a height, so a taller or shorter window keeps the split the user chose. + this.columnSplitter.onDrag = (delta) => { + const room = this.leftColumn.h - Splitter.THICKNESS; + if (room <= 0) return; + const floor = Math.min(0.5, MIN_PANEL_HEIGHT / room); + this.assetsShare = clamp(this.assetsShare + delta / room, floor, 1 - floor); + this.assets.flex = this.assetsShare; + this.hierarchy.flex = 1 - this.assetsShare; + this.leftColumn.relayout(); + }; this.centre = new VStack(); this.tabs = new TabStrip( diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index 573a158c..1c97ad3d 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -264,6 +264,12 @@ export function installDebugHook(app: Application, state: EditorState, editor: E tabRow: () => editor.tabRow(), /** The dividers left and right of the centre column – dragging them narrows the tab row. */ panelSplitters: () => ({ left: centreOf(editor.leftSplitter), right: centreOf(editor.rightSplitter) }), + /** The left column: asset panel on top, scene objects below, and the divider between them. */ + leftColumn: () => ({ + assets: rectOf(editor.assets), + objects: rectOf(editor.hierarchy), + splitter: centreOf(editor.columnSplitter), + }), viewportCenter: () => centreOf(viewport), /** Pans the Scene tab to a world point – a step's own lane, away from what earlier steps left. */ lookAt: (x: number, y: number) => viewport.lookAt(x, y), diff --git a/tools/e2e/14-overflow.mjs b/tools/e2e/14-overflow.mjs index 66178ef0..40d5362f 100644 --- a/tools/e2e/14-overflow.mjs +++ b/tools/e2e/14-overflow.mjs @@ -108,6 +108,33 @@ export async function overflow(t) { JSON.stringify({ hidden: back.hidden, narrow: narrow.hidden }), ); + // 14b-iii. The divider between asset panel and scene objects moves the height from one to the + // other, and dragged shut either keeps its header to grab it by again. + const column = () => json('__gm.leftColumn()'); + const near = (a, b) => Math.abs(a - b) <= 1; + const start = await column(); + await drag(start.splitter.x, start.splitter.y, start.splitter.x, start.splitter.y + 80, { steps: 6 }); + const lower = await column(); + check( + 'dragging the left column’s divider down gives the assets 80 px the scene objects lose', + near(lower.assets.h, start.assets.h + 80) && near(lower.objects.h, start.objects.h - 80), + JSON.stringify({ start, lower }), + ); + await drag(lower.splitter.x, lower.splitter.y, lower.splitter.x, lower.splitter.y - 2000, { steps: 10 }); + const shut = await column(); + check( + 'dragged all the way up the asset panel stops at its header', + near(shut.assets.h, 30) && shut.objects.y > shut.assets.y, + JSON.stringify(shut), + ); + await drag(shut.splitter.x, shut.splitter.y, shut.splitter.x, start.splitter.y, { steps: 10 }); + const restored = await column(); + check( + 'and dragged back it returns to the split it started with', + near(restored.assets.h, start.assets.h) && near(restored.objects.h, start.objects.h), + JSON.stringify({ start, restored }), + ); + // 14c. Every tab, because each one owns a different set of controls. for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list', 'particles']) { await showTab(tab);