Files
feuerwerk/src/editor/App.ts
T
adminandClaude Opus 5 6371576d37
game-maker / gate (push) Successful in 2m9s
game-maker / suite (push) Successful in 7m26s
A vector art asset is a picture now, which is stage 2 of #129 and the first stage that shows something in the browser: the asset sits in the picture picker, a sprite carries it in its Texture row and what the sprite shows has been rasterized from the shapes, the generator of #185 being registered for the type vectorart - installVectorArtPictures(app.renderer) hooks the source (core) and the generator (viewport) in once per page from App.ts, by a call and not by a module side effect, so a test may install it or leave it out; the rule lives in core and the painting in viewport - the new core/model/vectorPath.ts turns a shape into plain drawing commands (move/line/curve/close), closed rings being filled while all rings are stroked, a segment staying a line as long as neither of its ends carries a handle - a single handle is enough for a curve, the missing control point sitting on its own end, so a corner may stand next to a round side - and the way back to the first point is only drawn when it is curved, close doing it otherwise; holes go through one door only, Pixi recognising them from a GraphicsPath with checkForHoles, which tests containment rather than even-odd depth, which is enough for a ring inside a ring while without the flag the inner ring simply paints over the outer one (measured, see the counter-checks); the oversampling is the 2x from the plan - RenderTexture.create({resolution: 2}) puts 512 px on the GPU for a 256 canvas while texture.width stays the canvas size, which is the measure the sprite hangs on, and the render texture is the clip itself, so what juts out over the canvas is not in it and needs no mask; drawn pictures get the same sampler state as uploaded ones, setPictureFactory wrapping the factory for it, mirroring keep(); two measuring traps cost me runs - __gm.objectRect(id) gives the centre of the object and not its corner, so the first pass read four pixels next to the sprite and reported background while the rasterizer was correct, and the grid lines of the scene cross exactly in the centre point, so the hole check read #363940 and failed for the right reason, measuring beside it now; renderer.render() runs inside the picture request, which is safe here because every caller asks during its own widget redraw and the frame is drawn on the ticker afterwards, a nested render inside a running render pass would not be - that warning stands in the module head and in the notes; new module 47 with 8 own checks in its own lane (the picture on the GPU at double resolution measuring the canvas, the sprite showing the fill, the inner ring being a hole with the scene shining through, the later shape on top, the stroke running along the ring, a changed drawing getting a new key with the old one freed, and the sprite showing the new colour), the shapes being put into the model by mutate because the tab that draws them is stage 3, and 21 new unit tests in tests/core/vector-path.test.ts; three counter-checks hold: oversampling back to 1 fails the GPU check, checkForHoles off fails the hole check, and not drawing the curved way back fails a unit test; the full suite ran locally instead of only in CI because the change sits in TextureLibrary and touches every picture: ALL PASSED - 1165 checks, measured and carried into README and the wiki (#129)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:51:54 +02:00

140 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Application } from 'pixi.js';
import type { AuthStore } from '../core/store/AuthStore';
import { HttpAuthStore } from '../core/store/HttpAuthStore';
import { HttpFileStore } from '../core/store/HttpFileStore';
import { HttpProjectStore } from '../core/store/HttpProjectStore';
import type { StoredTexture, UploadedTexture } from '../core/store/TextureStore';
import { loadPhysics } from '../runtime/physics/engine';
import { requestRender, type VStack } from '../ui';
import { fontLibrary } from '../viewport/FontLibrary';
import { soundLibrary } from '../viewport/SoundLibrary';
import { projectPictureFactory } from '../viewport/generatedPictures';
import { textureLibrary } from '../viewport/TextureLibrary';
import { installVectorArtPictures } from '../viewport/vectorArtPicture';
import { LoginScreen } from './auth/LoginScreen';
import { installLoginHook } from './auth/loginHook';
import { consoleLog } from './console/log';
import { installDebugHook } from './debugHook';
import { Editor } from './Editor';
import { startPlugins } from './plugins/startPlugins';
import { EditorState } from './state';
const API_ENDPOINT = '/game-maker/api/index.php';
/**
* Where uploaded images live: the API writes them, the browser reads them straight off disk as
* static files named by their content hash. Fonts and sounds work the same way, one directory over.
*/
const TEXTURE_BASE = '/game-maker/data/textures';
const FONT_BASE = '/game-maker/data/fonts';
const SOUND_BASE = '/game-maker/data/sounds';
/**
* Shows the sign-in screen until somebody is signed in, and answers with their name. Returns
* right away when the session from an earlier visit is still good – which is what lets a
* deployment replace the editor under an open tab without asking for the password again.
*/
async function signedInUser(app: Application, root: VStack, auth: AuthStore): Promise<string> {
const existing = await auth.session();
if (existing !== null) return existing;
let done: (user: string) => void = () => {};
const screen = new LoginScreen(auth, (user) => done(user));
const removeHook = installLoginHook(app, screen);
const user = await new Promise<string>((resolve) => {
done = resolve;
root.add(screen, 1);
root.resize(app.screen.width, app.screen.height);
screen.focusFirstField();
});
removeHook();
root.removeChild(screen);
// The editor sizes `root` itself once it is built, but `resize` lays out only when the size
// changed – and the sign-in screen has already set it to the size of the window. Forgetting the
// size here is what makes that call count again; without it the whole editor sits unlaid-out in
// the corner.
root.resize(0, 0);
screen.destroy({ children: true });
return user;
}
/**
* Wires state, stores and the editor composition together and opens the first project.
*
* Signing out does not reload the page (#177): it takes the editor down and puts the sign-in
* screen back, in the same canvas with the same renderer. So this is a loop rather than a straight
* line – every round is one account's session. Only the first round is awaited, because the caller
* has more to set up once the editor is up; the rounds after it run on their own.
*/
export async function startEditor(app: Application, root: VStack): Promise<void> {
const auth = new HttpAuthStore(API_ENDPOINT);
textureLibrary.configure(TEXTURE_BASE);
fontLibrary.configure(FONT_BASE);
soundLibrary.configure(SOUND_BASE);
// An image or a font arrives long after the click that asked for it, and the widget waiting for
// it repaints without telling anyone: both libraries are a wake-up of their own. Registered once
// for the page, not per session – the files they hold are shared by every account anyway.
textureLibrary.onChange(() => requestRender());
fontLibrary.onChange(() => requestRender());
// Vector art draws its own picture and needs the renderer to do it (#129). Registered once for
// the page like the libraries above: which project it is asked about comes from the factory below.
installVectorArtPictures(app.renderer);
const openSession = async (): Promise<void> => {
const user = await signedInUser(app, root, auth);
// Who is signed in goes into the window title: the toolbar is crowded enough, and the title is
// also what tells two tabs on two accounts apart.
document.title = `Game Maker – ${user}`;
const state = new EditorState();
// Drawn pictures (#185) are looked up in the project this session holds; the next session
// installs its own, which is also what lets go of everything the old one had drawn.
textureLibrary.setPictureFactory(projectPictureFactory(() => state.project));
const editor = new Editor(
app,
root,
state,
new HttpProjectStore(API_ENDPOINT),
new HttpFileStore<UploadedTexture, StoredTexture>(API_ENDPOINT, 'textures'),
new HttpFileStore(API_ENDPOINT, 'fonts'),
new HttpFileStore(API_ENDPOINT, 'sounds'),
{
user,
logOut: () =>
void endSession(editor).catch((error: unknown) => {
console.error('[auth] Signing out failed:', error);
}),
// Straight through to the store: the server checks the old password, and the session this
// tab holds is the one it keeps – so there is nothing to do here afterwards.
changePassword: (current, next) => auth.changePassword(current, next),
},
);
installDebugHook(app, state, editor);
// Before the project, not after it (#119): which plugins run is written in the project file,
// and `boot()` hands that list over before it reads the file into the model.
await startPlugins(API_ENDPOINT, state, editor);
await editor.boot();
loadPhysics().catch((error: unknown) => console.error('[physics] Rapier could not be loaded:', error));
};
/**
* Ends the session the Logout button belongs to: the editor writes what is unsaved and lets go
* of everything, then the server drops the session, and the sign-in screen comes back. The
* editor goes first, so its last save still travels with a session the server accepts.
*/
const endSession = async (editor: Editor): Promise<void> => {
await editor.dispose();
document.title = 'Game Maker';
// The console panel is page-wide, the lines in it are not: what one account's project printed
// is none of the next one's business.
consoleLog.clear();
try {
await auth.logOut();
} catch (error) {
console.error('[auth] Signing out did not reach the server:', error);
}
await openSession();
};
await openSession();
}