User management: signing in is required before anything is loaded, the sign-in screen is drawn in the same canvas (user name, password as bullets, Enter moves on and submits) and the editor is built only after it; the API gained auth/session, auth/login and auth/logout while every other route answers 401 without a session, projects carry an owner and a foreign one comes back as 404, accounts live in data/users.json with a password hash and sessions as files in data/sessions with a sliding 30 days, so a build under the open tab does not sign anybody out; Logout sits at the far right of the toolbar and, as asked in the issue, does not reload the page: Editor.dispose() takes the editor down while the canvas stays, which is why the autosave, the focus handlers, the window listeners of the tilemap panel, the shortcuts and the plugins all got a way back; the tools sign in as e2e (tools/lib/auth.mjs, password from GAME_MAKER_E2E_PASSWORD or the git-ignored data/e2e-password.txt) and the browser gets the same session by cookie, new e2e module 45 covers the screen, the wrong pair, the bullets, logout without a reload and signing in again, new unit tests for HttpAuthStore and for what a session leaves behind (#177)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60ea56a2ce
commit
8e4bb6e1f9
38 files changed
+1530
-139
No files matched your search
+2
-66
@@ -33,6 +33,7 @@ import { currentSoundPicker } from './sound/SoundPicker';
|
||||
import { currentImagePicker } from './texture/ImagePicker';
|
||||
import { currentTilemapWidgets } from './panels/TilemapPanel';
|
||||
import type { EditorState, TilemapTool } from './state';
|
||||
import { rectOf, textProbe } from './textProbe';
|
||||
|
||||
/**
|
||||
* Installs `window.__gm`, which the check scripts in tools/ use to find widgets by their
|
||||
@@ -45,72 +46,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
const p = widget.getGlobalPosition();
|
||||
return { x: p.x + widget.w / 2, y: p.y + widget.h / 2 };
|
||||
};
|
||||
/** The overlap of two global rectangles; an empty one when they do not meet. */
|
||||
const intersect = (a: Rectangle, b: Rectangle): Rectangle => {
|
||||
const x = Math.max(a.x, b.x);
|
||||
const y = Math.max(a.y, b.y);
|
||||
return new Rectangle(
|
||||
x,
|
||||
y,
|
||||
Math.max(0, Math.min(a.right, b.right) - x),
|
||||
Math.max(0, Math.min(a.bottom, b.bottom) - y),
|
||||
);
|
||||
};
|
||||
const rectOf = (widget: { getGlobalPosition(): { x: number; y: number }; w: number; h: number }) => {
|
||||
const p = widget.getGlobalPosition();
|
||||
return { x: p.x, y: p.y, w: widget.w, h: widget.h };
|
||||
};
|
||||
/**
|
||||
* Every text under `root` that is really on screen, in drawing order (the topmost last), handed
|
||||
* to `visit` with the centre a click on it would use.
|
||||
*
|
||||
* A row scrolled out of its list is still `visible` – only masked away – and reporting it would
|
||||
* hand a check a point outside the panel, where the click lands on whatever is underneath. That
|
||||
* is how a step that passes on a short list fails on a long one (#39 in CI), so a text whose
|
||||
* centre falls outside the clipping it sits in counts as not being there at all.
|
||||
*
|
||||
* Every hook that reads text goes through here. A `locate` that refuses a scrolled-away row
|
||||
* while `overlayTexts` still lists it is worse than either rule alone: a check picks a row off
|
||||
* the one hook and then cannot find it with the other.
|
||||
*
|
||||
* The clip is the box of the *widget* that owns the mask, not the mask itself: Pixi hands a
|
||||
* graphics that serves as a mask to the mask effect, and `getBounds()` on it then reports an
|
||||
* empty rectangle at the origin – which would throw away every masked text in the editor.
|
||||
*/
|
||||
const eachText = (root: Container, visit: (node: Text, point: { x: number; y: number }) => void): void => {
|
||||
const walk = (node: Container, clip: Rectangle | null, owner: Widget | null): void => {
|
||||
if (!node.visible) return;
|
||||
const holder = node instanceof Widget ? node : owner;
|
||||
let inner = clip;
|
||||
if (node.mask && holder) {
|
||||
const r = rectOf(holder);
|
||||
const box = new Rectangle(r.x, r.y, r.w, r.h);
|
||||
inner = clip ? intersect(clip, box) : box;
|
||||
}
|
||||
if (node instanceof Text) {
|
||||
// Global bounds, not `width`/`height`: those are unscaled, and inside the graph canvas
|
||||
// (which is scaled by the zoom) that put the point right of the text it was meant to hit.
|
||||
const box = node.getBounds();
|
||||
const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
if (!inner || inner.contains(point.x, point.y)) visit(node, point);
|
||||
return;
|
||||
}
|
||||
for (const child of node.children) walk(child, inner, holder);
|
||||
};
|
||||
walk(root, null, null);
|
||||
};
|
||||
/** Centres of every visible text equal to `text`, in drawing order (the topmost last). */
|
||||
const locateAll = (text: string): { x: number; y: number }[] => {
|
||||
const hits: { x: number; y: number }[] = [];
|
||||
eachText(app.stage, (node, point) => {
|
||||
if (node.text === text) hits.push(point);
|
||||
});
|
||||
return hits;
|
||||
};
|
||||
const locate = (text: string, last = false): { x: number; y: number } | null => {
|
||||
const hits = locateAll(text);
|
||||
return (last ? hits.at(-1) : hits[0]) ?? null;
|
||||
};
|
||||
const { eachText, locate, locateAll } = textProbe(app);
|
||||
/** Fill colour of a visible text, as `#rrggbb` – what tells an override label from a plain one. */
|
||||
const textColor = (text: string): string | null => {
|
||||
let found: string | null = null;
|
||||
|
||||
Reference in new issue
Block a user