3a68067079
Dragging a point in the vector art tab keeps up with the mouse (#129)
adminandClaude Opus 52026-09-20 21:55:02 +02:00
3d7ad65f34
Vector art has a tab to draw in, which is stage 3 of #129admin2026-09-20 21:34:14 +02:00
6371576d37
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)
adminandClaude Opus 52026-09-20 20:51:54 +02:00
bdb3f066cf
The asset type "vector art" exists as a model, which is stage 1 of #129 and shows nothing in the browser yet on purpose: a vectorart asset is not placeable, sits in the + menu and in the asset panel with its own icon (a curve between two points) and has no tab, so it behaves like a font - selectable, its rows in the inspector, a double click leading nowhere - because the tab is stage 3 and half an asset would only be in the way; the data model in Asset.ts is a shape built from rings (even-odd, so a shape may have holes, answer 6 in the issue), a ring being points with optional bezier handles in/out stored as an offset from the point and a closed flag (an open path is stroke only, answer 4), plus fill colour, opacity and an optional stroke (answer 5) - the two handles are kept apart instead of mirrored, because what "smooth" means is the editor's business and not the model's; canvas width, height and the filter are properties, so they get an inspector row and undo like any other, while the shapes are intrinsic data like the frames of a texture; the revision that #185 hands to the picture path is a fingerprint of what is drawn, not a counter, which is exactly why that key took a string token - no command can forget to bump it - and the name of a shape is deliberately left out of it, renaming changing no pixel; six undo commands in the new vectorArtCommands.ts (add a shape numbered in order, remove one which comes back to its old place - unlike a tilemap layer the last one may go too -, rename, move, write the rings as one step per gesture with the list copied, and set the style where stroke: null takes it away) and the pure function shapeOrderTarget for the four buttons of the planned inspector list (one forward, to front, one back, to back), which also serves dragging in that list; loading from a project file is strict - a point without numbers falls away, a broken handle leaves a corner behind, duplicate shape ids fall away like duplicate layer ids, a stroke of width 0 is no stroke - while a ring with a single point, which is what someone is drawing right now, stays; three findings on the way: JSON.stringify as the fingerprint was wrong because the same drawing came back from a project file as {points, closed} and was built as {closed, points}, so a reload would have rasterized the same picture under a new key (the fingerprint writes its fields in a fixed order now and a test measures that case), toNumber in serialize.ts turns any nonsense into 0 so the Number.isFinite checks ran into nothing and {x: 'far'} became the left edge instead of vanishing (shapes read numbers strictly via asFinite, 0 being a place on the canvas here), and "Vector art" would have been both a menu entry and the panel heading, which an existing test catches because a click by text would hit the heading - the group is called "Vector drawings" now while the menu entry stays "Vector art"; 20 new unit tests in tests/core/vectorart.test.ts and three counter-checks hold: the fingerprint back on JSON.stringify fails 2, reading the numbers loosely again fails 4, and not copying the ring list fails 1; no e2e check and the check sum stays 1157, only module 04 counting 19 asset types instead of 18 (#129)
adminandClaude Opus 52026-09-20 20:29:35 +02:00
cb1804dcf2
A picture in the editor may now be drawn instead of uploaded, which is the piece #129 (Vector art) and #130 (Pixel editor) both need and neither could build alone: TextureRef.file has been a file name on the server, isTextureFile() checking a sha256 hash plus an extension and TextureLibrary fetching it over HTTP, so an asset that paints its own image had nowhere to put itself - the key now also takes the form gen:<assetId>@<revision>, which an uploaded file can never look like, and everything that only passes a ref along (sprite, tile, tilemap mesh, particle source, shaders, aspect ratio) is untouched because it still sees a Texture; the revision is a string, not a counter, which is the one deviation from the plan in the issue and costs nothing - gen:a1@7 and gen:a1@c0ffee go through the same door, so a generator may fingerprint its own content instead of relying on every command to remember to bump a number; two registries keyed by asset type as a plain string, so a plugin may bring its own type - registerPictureSource(type, read) in the new core/model/generated.ts tells the model canvas size and revision and resolvePicture() builds the ref from it (no rect, no slice, smooth from props.filter like a texture), and registerPictureGenerator(type, draw) in the new viewport/generatedPictures.ts hands out the finished texture, projectPictureFactory(() => state.project) tying the two together per session in App.ts; invalidation is a rule instead of guesswork at twenty call sites: GeneratedPictures<T> holds one picture per asset keyed on the revision and frees the old one as soon as the new is drawn, TextureLibrary destroying it and dropping the crops cut from it, while a generator that cannot draw yet is not remembered but asked again next time - the class imports Pixi as a type only, so the whole bookkeeping is measurable without a GPU; carried along are the picture picker (a drawn asset sits among the textures by name, without frames), the delete warning (counts tiles and animations for it too) and the asset context menu, where "Create sprite from picture" now covers it while "Create animation from texture" stays texture-only because it needs frames; one finding while probing - crop() could not have cut a generated texture at all, a RenderTexture having no source.resource to draw back from and drawImage running onto null, so it is handed out whole, a drawn picture having no sheet whose neighbouring row could bleed in; 18 new unit tests, 10 in tests/core/generated-picture.test.ts (building and splitting the key, an uploaded file is not one, no registry means no picture, resolving to canvas size, a new revision is a new key, a canvas without a size, the filter, the sprite size, picker order) and 8 in tests/viewport/generated-pictures.test.ts (drawing once, freeing the old revision and only for its own asset, null is not remembered, no key means nothing drawn, a session change gives everything back, peek), and three counter-checks hold: dropping generated assets from resolvePicture fails 5 of the 10 core tests, not freeing the old revision fails 1 viewport test, and narrowing the picker back to textures fails 3; no e2e check and the check sum stays 1157, because without a generator the groundwork shows nothing in the browser - that evidence comes with #129 (#185)
adminandClaude Opus 52026-09-20 20:09:42 +02:00
eac50e4194
Moving against a tilemap whose cells block with an outline no longer costs more than a whole frame: an outlined cell is its own polygon, split into convex parts, so a map that is a handful of merged rectangles when built from plain solid tiles is hundreds of solids under one entity - and Move brought every one of them into world coordinates and tested it exactly, including the ones on the far side of the map, which measured 148 ms per Move call on a 20x20 map with 16-corner outlines (34 ms at 8 corners, 0.4 ms with plain solid tiles), matching the report that standing still holds 60 fps while anything that moves stutters; the fix is a broad phase in two stages that decides nothing and only leaves out what cannot be reached - a move never ends further from its start than it is long, because sliding always shortens the rest, so every collider of the mover gets a rectangle grown by the move length and an obstacle whose rectangle does not meet it is never tested, a ray taking the rectangle around itself, and that rectangle is carried back into the map's own units with invert(m) and turned into cell coordinates (cellsWithin), a rotated map through its own axes, so a far cell is not brought into world form in the first place - solidShapesOf was O(cells) per call, which was the second cost block and the reason the 60x60 case still grew; CollisionSystem.overlaps takes the same range, so the editor run asks a map only for the cells within reach of the other side each frame, two maps narrowing nothing, and a Move obstacle now falls away at build time when its layer does not meet the mover's (#152) instead of inside the loop; the measurements after: 148 -> 0.26 ms, 34 -> 0.27 ms, 60x60 20 -> 3.5 ms, plain tiles unchanged; this is no regression of the recent commits - the same benchmark in worktrees against 947a6b1 (#141), 6a31f5e, 4f55f85 and ae3aa7d gives the same 85-148 ms, the cost has been there since outlined cells arrived with #141; new pure functions shapeBounds/grownBounds/boundsMeet in collider.ts and cellsWithin/cellsMeet in tilemapCollision.ts, 11 new unit tests in tests/core/broadphase.test.ts (the rectangles of all four shapes including a rotated box measuring its diagonal reach, touching counts as meeting, cellsWithin straight and turned by 90 degrees, a map handing out 400 cells without a range and under 10 with one, an 800 unit move from far away still stopping at the first cell, a rotated map blocking where its one painted cell was turned to, a ray across the whole map, and the editor run seeing a collider on a far cell), and four counter-checks hold: dropping the cell filter fails the counting test, padding without the move length fails 4 move tests, ignoring the rotation in cellsWithin fails 3, and a wrong range in the detection fails the touching test; one blunt test on the way - the first rotated map was painted on every cell, where even a wrongly computed cell range blocks and the counter-check stayed green, so exactly one cell is painted now and the test measures where it turned to; no e2e check and the check sum stays 1157, because a timing check in the browser would flutter under CI load while modules 40 and 42 cover the correctness anyway, the speed evidence being the node benchmark (#184)
adminandClaude Opus 52026-09-20 19:25:53 +02:00
ffdf0a8ddb
The new node "Raycast all" answers with every collider along the ray instead of only the nearest one: it carries the same inputs and settings as Raycast (Ignore owner, Hit triggers, Layers from #158) plus "max hits" where 0 means all, and it hands back parallel lists sorted by distance - objects, colliders, points x/y, normals x/y, distances - next to count, so index i of every list speaks about the same hit; a collider counts once no matter how many convex parts its shape is built from, while a tilemap counts per cell it is hit in, and the filter sits in Solids.raycastAll like the one of #158, the one path the editor run and Play both take, so all 12 new unit tests run through describe.each against both systems; the node needed the value system to learn something it could not do - a graph list carried numbers, text, booleans and bundles but no object handles, asList turned an object into its name, so objects/colliders would have been lists of names, which loses the object when two carry the same name, and collider children usually do; Value now knows a travelling list that may hold handles and "Item at" gives such an item out as an object, ready for Destroy, Apply force or Name of, while the single door into a stored file - writeTyped/asTypeOf - runs the list through the new storedList() so every object becomes its name again and nothing living reaches a project file, which is its own unit test over JSON.stringify; three list nodes follow the handles: "Index of" compares objects by identity because a name is not the object of that name, "Set item" and "Fill" write an object as an object, and "Join text" prints the names instead of the innards; this decision was mine to make because the question ran into the MCP timeout - the alternative was a list of names plus "Object by name", which is the smaller change but lossy; 4 new e2e checks in the new block 43e (shield and floor in one go reading 2 and "145, 280", the second object answering to its name, which only a real object in a list can do, "max hits" 1 stopping at the nearest, and the node showing the row) and 18 new unit tests, 12 in raycast.test.ts, 4 on the value system and 2 in the node catalog; two counter-checks hold: not counting tilemap cells one by one drops 2 unit tests, and answering with the nearest hit only drops module 43 with 2 FAILs; one finding on the way - the node read "maxHits" while every other input carries a spoken label, because int() took the key as its label, so it has a third parameter now, and the e2e check caught that, no unit test did (#157)
adminandClaude Opus 52026-09-20 18:54:12 +02:00
0fdde6cb1e
The Raycast node filters by collision layers: the node gained a "Layers" setting, a tickbox per collision layer of the project - the same field kind the collider uses for "Collides with" - and the ray only hits what sits on a ticked layer, tilemaps counting as colliders; the default is every layer, and a graph without the param answers exactly as before, which is its own unit test; the check is one-sided on purpose, unlike the symmetric rule of #152: a ray is not a collider, so it never reads the collidesWith of the thing it hits - a collider that meets nothing is still seen by a ray - because who a ray may look at is the ray's business, and a unit test holds that case; the filter sits in Solids.raycast in front of the intersection test, which is the one path the editor run and Play both take, so every unit test runs through describe.each against both systems; NodeView did not know the field kinds layer/layers, so the node would have read "Layers: 65533" - it now shows the name of the layer, or "All layers"; 5 new e2e checks in the new block 43d (a shield on its own layer between the caster and the floor stops the ray, the "Layers" row on the node, the tickbox menu with the project's layers, the ray reaching 280 instead of 145 units once the shield's layer is unticked, and the mask read as a name) and 6 new unit tests, three against each system; two counter-checks hold: disarming the filter line drops 6 unit tests and the unticking check in module 43, and switching the formatting off makes the node check fail with the raw bits; two findings from measuring came out of it and stand in the wiki and the notes - __gm.rays() only measures while the Scene tab is in front, so a node selection blinds it and the block measures through a Log node on distance instead, and a running behavior instance keeps the graph it started with, so the editor run casts with the old mask until the instance is built anew (#158)
adminandClaude Opus 52026-09-20 18:15:35 +02:00
ae3aa7d3bd
Refactoring round 5 covers the code since round 4 without changing behaviour: four identical copies of dot() and three of "nearest point on a segment" stood in the collision math, in the hit area of the collider outlines and in the polygon handles, so they moved into the new module src/core/util/vec.ts; the warning line, the busy state, info() and the caret reset were written out line by line in both the sign-in screen and the password dialog and are now FormStatus; the closing row "Cancel plus a confirming button, optionally one on the left" was built by hand in FormDialog, FilePicker, OpenProjectDialog and PasswordDialog and is now dialogButtons(); the header bar was drawn by the toolbar, the TabHeader and the Breadcrumb each on its own and is now drawHeaderBar(); AddColliderCommand and AddRigidbodyCommand share a base, each subclass only builds its own asset; Editor.ts derives the panes from assetEditors instead of repeating eight entries and clamps its splitters with clamp(); jscpd over src falls from 0.19 % to 0.14 %; dead code is gone - listScenes() has been orphaned since the Scene picker fell away (#183), DEFAULT_FILTER (#152) was never used, the two test hooks projectPickerRowAt/rowPosition and particleCompiles are read by no tool, and four functions lost an export they did not need; seven comments that told history ("used to", "no longer", "since #...") describe the state of things now, and that this changed nothing was proved by a rebuild - the bundle is md5 identical, b4ad0dfe... before and after; the README said Logout sits at the far right of the toolbar, which has been the Account menu since #182, and wiki/Architecture.md was missing src/core/util/ altogether; 4 new unit tests cover vec.ts, and switching the clamp on the segment off makes exactly one of them fail while none of the 76 neighbouring tests notice, so that clamping was covered nowhere before (#85)
adminandClaude Opus 52026-09-20 17:24:39 +02:00
da35f10d1a
The undo and redo arrows read as arrows now, and the run buttons sit on the middle of the window: the first pair was a three-quarter circle with a triangle at the end of the arc, which at 16 units puts the head exactly where the arc already is - the two melt into one another and the icon reads as a comma, which is what the screenshot showed - so both are drawn as a flat head standing free beside its tail, a triangle pointing left with a tail curving away to the right and down (redo mirrored), and they were checked in the real build, not only in an SVG renderer, because ImageMagick draws these paths a good deal more kindly than Pixi does; the play/pause pair was off centre because the row has three flexible children - the two spacers around it and the notice label, which is flexible since #177 so that the bar does not overflow at 1280 px - and each takes a third of what is left, so Toolbar.layout() now moves the pair onto the middle of the bar after super.layout(), clamped between the redo edge and the start of the notice so that a narrow window pushes the pair back instead of laying two captions on top of each other; one new e2e check in 46a measures that middle against innerWidth/2 (1148 checks), and the README gained the screenshot of the editor
adminandClaude Opus 52026-09-20 16:51:29 +02:00
60d0146bde
The topbar is icons only: the leading Project dropdown, the Scene picker and the labelled Play/Pause buttons are gone and in their place stand five icon buttons - menu, undo, redo on the left, play and pause in the middle - while Notice, the save state, fps and the Account menu keep the right side; the Play button swaps its icon for a stop square while the project runs and Pause swaps for a second play sign while it is held (Button.setIcon()), and iconSpace() drops the gap between icon and label when a button has no label, which is what kept a 24 px icon button 3 px off its own centre; the menu carries New project, Open project..., a separator and Project settings..., where the picker (src/editor/OpenProjectDialog.ts) preselects the open project and marks it "open", opens on a double click and puts Delete on the left of Cancel and Open - it closes itself before it asks whether to delete, because two modals on top of each other would have left the question standing over a list it no longer speaks about - and the settings dialog (src/editor/ProjectSettingsDialog.ts) holds the name, the "Play starts" switch (Start scene / Active scene, a preference of this browser and so no command) and the collision layers from #152, which retires CollisionLayersDialog.ts and the "Collision layers..." entry; what Apply actually writes is the pure function settingsChanges(), so renaming and the layers are each their own undo step and each drop out when nothing differs; the Scene dropdown falls away without replacement, the active scene is switched by double clicking it in the Assets panel, which is the way that was there anyway; new module 46 runs last with 9 checks (no label left in the bar, the run buttons in edit mode, the four menu entries, the picker with the open project preselected, Cancel leaving the project standing, the settings dialog on the state of the project, renaming with undo and redo through the icon buttons) and it neither opens nor deletes a project, because that would pull the project out from under every earlier module; modules 8, 18 and 35 and the harness follow the new bar, 6 unit tests cover settingsChanges and 3 the icons (#183)
adminandClaude Opus 52026-09-20 16:34:14 +02:00
defd4a86d2
The live dt in the graph is not always three decimals: the check in module 18 that reads the value an output last carried asked for /^0\.\d{3} dt$/, but formatNumber trims trailing zeros, so a frame of exactly 50 ms reads as "0.05" - that is why the suite for 5ba796a was red in CI with this single FAIL while the same check is green on this machine, where the frame time never lands on a round number; the check now asks for any number of decimals, which is what it was ever about, and the reason stands as a comment next to it (#119)
adminandClaude Opus 52026-09-20 15:41:43 +02:00
4f55f85ad4
Collision layers and masks: a collider carries a layer and a mask of the layers it meets, and the pair only meets when both masks name the other's layer - the rule is one pure module (src/core/model/collisionLayers.ts) with layerBit, layersMeet and the names a project starts with, so the three places that have to agree read the same function: the editor run through CollisionSystem, the Solids sweep per pair and Rapier, where the mask becomes the collider's interaction groups; the project file gained collisionLayers: ["Default"] (default one layer, no formatVersion bump), the collider schema two new kinds - layer as a dropdown of the project's layers and layerMask as a menu of tickboxes that stays open while it is ticked (Menu.ts learned rows that do not close on a pick) - and the names are edited through "Collision layers..." in the toolbar's Project menu, not a Project tab, which is the decision I made myself because the question ran into the MCP timeout: the dialog (src/editor/CollisionLayersDialog.ts) holds up to 16 names, Add layer appends "Layer <n>" and Apply writes all of them as one undo step (SetCollisionLayersCommand); a mask read from a file goes through a check that refuses anything that is not a finite number, because Number(null) is 0 and a collidesWith: null would silently have meant "meets nothing"; 9 new e2e checks in module 8 cover the menu entry, the dialog, Add layer, Apply and its undo, both inspector rows, the tickbox menu, and the mover walking straight through the crate that 8c blocked on once their layers no longer meet, and 21 new unit tests cover the rule, the schema and the command (#152)
adminandClaude Opus 52026-09-20 15:31:15 +02:00
5ba796a2b6
Plugins belong to the project, and they load before it: the project file gained the field plugins: ["demo-plugin.js"] (default empty, no formatVersion bump), the Plugins tab's switch writes into it as an ordinary undo step (SetPluginActiveCommand) instead of into a server-wide plugins.json, and the start order is turned around - startPlugins only fetches the list of files, then editor.boot() runs and ProjectStore.load(id, prepare) hands the raw JSON through before it is deserialized, where pluginsOf(raw) reads the list and PluginManager.syncTo() activates exactly those; that is the point of the issue, because what a plugin registers has to stand there before normalizeGraph would throw it away as unknown (#126, #127), a project switch first stops what the new project does not name and then starts what it does, a file the server does not have is passed over silently, and after signing out the manager is dead so a late event cannot start anything in a torn-down session; on the server plugins.json, PluginStore::setActive, isActive, the route PUT ?r=plugins/{file} and the 403 for inactive modules are gone and GET ?r=plugins only answers {file,bytes,updatedAt}, which makes every plugin file fetchable - the consequence accepted in #117, written down in the wiki for #115 and #116; publishPlugins(root) only copies files now, the seeder switches the demo plugin on in the fixture project, and the Plugins tab carries a puzzle piece as its icon while only a plugin's own tab still goes without one (the knob and the socket are cubic curves, not arcs, which Pixi's parser draws as a spike after an L); 3 new e2e checks in module 38 and 5 new unit tests cover the project as the owner of the switch, its undo, the order, pluginsOf against crude input and an unknown file name (#119)
adminandClaude Opus 52026-09-20 14:24:27 +02:00
c830c7a3aa
Changing the password: the Logout button at the far right of the toolbar became an Account menu that names the signed-in account in an entry of its own that cannot be picked, with Change password... above Logout, and the new dialog (src/editor/auth/PasswordDialog.ts) asks for the current password, the new one and its repetition, all three as bullets, and stays standing when the server refuses so that it can say why, unlike the other form dialogs; the rules for empty, too short, new equal to old and a differing repetition sit as the pure function passwordProblem() next to loginProblem(), the API gained POST ?r=auth/password which answers 204, insists on the current password although a session is there, enforces a minimum length of 8 in UserStore::MIN_PASSWORD_LENGTH and again in the client so the mask can answer without asking the server, and the new SessionStore::destroyAllFor($user, $keep) ends every other session of the account while the tab that changed the password keeps its own; the placeholders admin/admin and user/user stay as they are, which the length check does not touch because it only runs when a password is set through the API; e2e 45 grew by 8 checks for the menu, the dialog, both refusals and signing in with the old and the new password, and it sets the e2e account back at the end - a run dying in between leaves it on <password>-tmp, which a make-user.php call repairs (#182)
adminandClaude Opus 52026-09-20 13:46:40 +02:00
1ef497fa17
e2e 45 no longer hangs on the project name: the two checks that opened the editor compared the project's name against "E2E fixture", which is what the seeder writes locally, while CI creates the shell with the key from tools/ci/project-shells.mjs and so calls the same project "fixture" - that is why the run for 8e4bb6e was red with two FAILs although signing in, the editor and the project all worked; both checks now read the project id and the seeded scene name "Level 1", which say the same thing in both places, and since the firewall in front of nockal.com stopped turning every non-2xx into its own 418 page (measured today: the API's 401 comes through with its body) the wrong-password check asks for the wording again instead of just for any message, with the unit test for a page made up in between kept but no longer named after that firewall (#177)
adminandClaude Opus 52026-09-20 13:27:42 +02:00
8e4bb6e1f9
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)
adminandClaude Opus 52026-09-20 12:50:52 +02:00
60ea56a2ce
Open asset greyed out and real tab cards: Open asset in the scene object menu is only pickable for kinds with an editor of their own, the table of which asset opens which tab moved from Editor.ts into the new module assetTabs.ts with hasAssetEditor so the menu and the editor read the same list, Select asset stays as it was; tabs are drawn as index cards (new module TabButton.ts): the one showing is a light grey card instead of the aggressive blue, rounded above and square below, its name in plain weight and measured bold so it never clips itself, and a mouse down on the tab already showing keeps its colour; the padding left and right grew to 6 px in one place for both kinds of tab, Scene and Game carry an icon too (the scene one from the asset panel, a new game.svg play sign), Plugins and a plugin's own tab have none; new test hook menuEnabled next to menuRows and menuIcons, e2e 01 checks the greyed out entry on a sprite row, 14a-ii the card and the icons of the fixed tabs, 44a the icon at the wider padding (#176)
adminandClaude Opus 52026-09-20 11:40:01 +02:00
4b5b91eaaa
Open asset in the scene object menu: the context menu of a scene object row has an Open asset entry below Select asset that does the same as a double click on the asset in the asset panel, so it selects the asset and opens its tab; kinds without an editor of their own (sprite, camera, text, collider) are only selected, just like the double click there; e2e 01 right clicks a behavior row and checks the open tab (#176)
adminandClaude Opus 52026-09-20 10:16:37 +02:00
e3ebf44593
Asset panel grouped by kind with icons: assets are listed under a header per kind (Sprites, Behaviors, ...) with the count on the right, empty groups are not shown, every group starts folded when a project is opened, a click on the header folds and unfolds it, selecting an asset elsewhere unfolds its group and folding the group of the selected asset clears the selection; every asset kind has an SVG icon in src/editor/panels/icons/ drawn before the name, the pure rule for grouping and sorting lives in assetGroups.ts; the same icons replace the kind word in document tabs and the overflow menu (a list icon for the List tab, which also fixes the close x lying over the end of the active tab's text), stand before every entry of the + menu (Button got an icon option) and on the right of scene object rows instead of the type name; the colour swatch next to sprite names is gone from both panels; the SVG loader moved to src/ui/svg.ts for both icon sets; test hook assetRow unfolds the group and scrolls to the row, assetRowShown only reads, new hook menuIcons; e2e 4g and 4h, 14 and 44a check the icons (#176)
adminandClaude Opus 52026-09-19 18:36:20 +02:00
1c212b1ccc
Scene view symbols come from SVG files: the behavior, container, rigidbody, camera and particles icons live in src/viewport/icons/ and are changed by editing the file and running npm run build; they are bundled, parsed once and drawn into a Graphics by the new module symbolIcons.ts, currentColor becomes the theme color of the symbol, the origin sits in the middle with a centered viewBox and the same units as before, so the symbols are pixel for pixel unchanged; what depends on the object (camera frame, collider outline, one-way cone) stays in code; unit tests keep the files to what Pixi's SVG parser and a browser draw alike (no transform, no polygon, fill always given), e2e 5c reads the five symbols in the theme color (#175)
adminandClaude Opus 52026-09-19 17:07:56 +02:00
25ec7879a0
Several transitions between the same pair of states: make transition onto a state that is already connected in that direction adds another transition, which gets its own name right away (Transition 2, Transition 3, skipping names that already leave the state), so two unnamed transitions between one pair never exist; renaming is refused when another transition between the same pair in the same direction already carries the name, the empty name included, while equal names to other targets are only warned about as before; the behavior graph keeps Already connected; self transitions may repeat too, each further loop drawn as a ring around the previous one with the preview showing the next ring, and transition labels that would cover another one slide along their own edge until they are free (pure function freeLabelSpot); test hook transitionLabelAt also returns width and height, e2e 26e6 (#162)
adminandClaude Opus 52026-09-19 16:30:52 +02:00
010b6a7f6c
A state can transition to itself: make transition and a second click on the same state connects a self transition, taking it fires On state exited and then On state entered every time, for local and global machines, while Set state into the current state stays silent as before; only states may connect to themselves, every other node is still refused, one self transition per state; the loop is drawn over the middle of the top edge with its arrow pointing down and its name at the apex, the preview already shows it while armed, and the inspector lists it once under Transitions out; new test hook transitionLabelAt, e2e 26e5 (#163)
adminandClaude Opus 52026-09-19 16:10:02 +02:00
cc285af10a
Tabs are open documents: Scene, Game and Plugins stay as fixed tabs, every other asset opens in its own tab (double click or Enter in the asset panel, on creation, from the list button), several assets of one kind can be open at once, a tab is labelled with its kind dimmed before the name, cut at 160 px and renamed with its asset, closes with its x or a middle click and goes away when its asset is deleted; a tab always shows its own document instead of following the selection, the picker in the tab head is gone, zoom and scroll are kept per document for the session and dropped on close, a double click on a call node stays in the same tab with breadcrumbs per tab; one editor per kind is switched between documents, the placement rules live in the pure module documents.ts, and the test hook showTab stays as a compatibility path; new e2e module 44 documents (#171)
adminandClaude Opus 52026-09-19 15:49:54 +02:00
4f546c0e08
Number fields drop trailing zeros: formatNumber still rounds to at most 3 decimals and then removes zeros at the end, so 100.000 reads 100 and 0.500 reads 0.5, everywhere (inspector, graph and shader nodes, duration and fps in the animation and tile tabs); a field's own decimals is now an upper limit, stored values are unchanged, -0 still shows as 0, and the graph's separate integer rule is gone (#172)
adminandClaude Opus 52026-09-19 14:29:00 +02:00
47eea2f6e4
Each tilemap layer has its own Z: an offset added to the Z of the placed tilemap, like a child's Z to its parent, default 0 keeps the previous order (layers right after their map, in list order), a negative Z sorts a layer below the map, on a tie layers keep their list order and follow the hierarchy against other objects; editor scene and game use the same order through the new pure function drawSlots, a tilemap draws into one container per layer, and the tilemap panel gains a Z field for the selected layer with one undo step per change (#174)
adminandClaude Opus 52026-09-19 14:03:20 +02:00
5385d9e152
The one-way tolerance angle is a field: a one-way collider and a one-way tile gain One-way angle (0 to 90 degrees, default 45), a contact blocks when its normal is within that angle of the blocking side, without a value the limit is bit-identical to before, and the rule holds for Move, Raycast and dynamic bodies in Rapier; tiles with different angles never merge into one rectangle, the scene view draws the angle as a cone in the middle of the thick side, and the inspector shows Blocks from and One-way angle only while One-way is on, through the new schema option shownWith (#159)
adminandClaude Opus 52026-09-19 13:13:29 +02:00
d4ea04d4d6
The scene view outlines a tilemap's collision only along its outer edges: where two pieces meet, their shared stretch runs once in each direction and is not drawn, so an autotile lake is one outline without cross strokes at the quarters, merged rectangles of plain tiles and one-way tiles follow the same rule, the bridges from splitting concave outlines disappear and a piece doubled on two layers is outlined once; fill and collision stay as they were, and the result is cached per tilemap and rebuilt only when its collision data changes (#173)
adminandClaude Opus 52026-09-19 12:54:53 +02:00
6a31f5e0ad
One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
adminandClaude Opus 52026-09-19 11:36:11 +02:00
947a6b1977
Collision outlines for autotiles: a frame holds several outlines (click adds a corner to the active one, Ctrl/Cmd-click starts a new one, clicking another outline makes it active, Delete removes the active outline and never the frame in collision mode), an autotile's outlines are drawn over the whole block, cut at the quarters and each cell takes the pieces of exactly the quarters its image is made of, so edges, outer corners and notches follow automatically; a tile of the block with nothing drawn blocks fully, cells of only full quarters merge back into rectangles, collision scale does not apply to an outlined autotile, and the previous format with one flat outline still loads (#141)
adminandClaude Opus 52026-09-19 01:56:20 +02:00
32057a96fc
Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
adminandClaude Opus 52026-09-18 22:56:25 +02:00
1b5e386b60
Move with collision reacts along the contact normal: it travels to the surface, stays flush and slides the rest of the move along it with Coulomb friction (the mean of both colliders, a tile counts as 0.5), a circle now rolls over an edge instead of catching on it, pushed dynamic bodies move off along the normal, Move gains the outputs hit, normal x and normal y, an object without its own collider moves freely, and editor run and play share the same path through Solids; contact point and normal come from our own f64 geometry (contactBetween), because Rapier's castShape lands 3e-6 px inside the wall in f32 (#150)
adminandClaude Opus 52026-09-18 21:52:11 +02:00
d931e66f92
e2e 41b finds the ball's bounces by its turning points instead of frames within a pixel of the ground: the ball lands at ~9 px per frame, so whether a drawn frame catches the landing depended on where it fell between two physics steps, and CI missed the first landing and read the second bounce as the first (#147)
adminandClaude Opus 52026-09-18 20:55:20 +02:00
328f978d3c
Set gravity survives a re-read of the model during play: World.setSceneProp remembers what the run itself set on the scene and syncFromModel lays it back over the copied scene props, so a project event mid-play no longer resets the gravity to the saved 980; the unit test re-reads the model mid-run and e2e 41c forces a project event right after play starts (#147)
adminandClaude Opus 52026-09-18 20:08:09 +02:00
0ff3c8b742
Physics stage 1b: six new nodes act on dynamic bodies in play (Apply force over the frame, independent of the frame rate, Apply impulse, Set velocity, Set angular velocity, Get velocity, Set gravity for the rest of the run), a node on an object without a dynamic body and moving a dynamic body directly each log a hint once per object, and Move with collision now pushes dynamic bodies with a mass-dependent push while the mover stays flush; Move keeps its own move-and-slide instead of Rapier's character controller, which measurably snags on seams between colliders (#147)
adminandClaude Opus 52026-09-18 19:19:21 +02:00
78899b1ace
Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
adminandClaude Opus 52026-09-18 18:11:43 +02:00
d02bcca266
Rapier 2D (@dimforge/rapier2d-compat 0.20) comes in as the physics engine: the editor loads it once in the background right after opening a project from its own chunk, so the main bundle grows by 0.19 KB gzip and the first frame does not wait; loadPhysics, physicsStatus and the 100 px = 1 m conversion live in src/runtime/physics/engine.ts, __gm.physics() reports the state, unit tests pin free fall and a box resting on a box, and e2e module 41 pins that Rapier is ready from its own chunk without console noise (#145)
adminandClaude Opus 52026-09-18 16:56:47 +02:00
6d7d0005cf
The tab headers are one TabHeader, the node board and the tile board pan and zoom through one helper, reordering tile pictures and tilemap layers goes through one moveItem, and a tilemap cell is one named type (#85)
adminandClaude Opus 52026-09-18 16:01:12 +02:00
c91c18f495
The loader keeps no migrations any more: an asset of the old type autotile (before #131) is an unknown type and dropped, and a tilemap layer with the cell map of the first version (before #134) loads empty; every project is saved in the current format, and the two tests now pin that the old shapes are not read (#137)
adminandClaude Opus 52026-09-18 15:12:07 +02:00
bffa508ee7
A tile gains Collision scale: below 1 each blocking cell pulls in on the sides without a blocking neighbour and stays flush towards its neighbours, so a wall of bushes becomes a narrower wall without gaps and the inner corner of an L closes cleanly; above 1 the cell grows at its open sides; on several layers the largest value counts, and the merged rectangles work in fractions of a cell with exactly the old result at 1 (#140)
adminandClaude Opus 52026-09-18 15:00:07 +02:00
4728f4fab5
A placed tilemap grows and shrinks at every edge: the new GRID EDGES block in the inspector adds or removes one column or row at Left, Top, Right or Bottom as one undo step, shifting the cells of all layers and moving the placement through its own matrix so the content stays in place on screen; removed painted cells are kept outside the grid and return when the edge grows again, and the Columns and Rows fields of a placement keep the content in place too (#136)
adminandClaude Opus 52026-09-18 13:58:03 +02:00
0b32d3cbf3
The tilemap panel gains Fill and Pick: Fill floods the connected cells of the active layer that carry the same tile (edge neighbours only, bounded by the grid) as one undo step, Pick takes the tile of the clicked cell on the active layer, and holding Alt picks while any tool is active (#133)
adminandClaude Opus 52026-09-18 12:31:19 +02:00
c2fd536aaf
The texture field only offers pictures: each texture with its frames, no animations any more; an animation id left in an older project shows as a missing reference (#143)
adminandClaude Opus 52026-09-18 12:14:01 +02:00
5046ddb810
The tilemap painting tools move out of the inspector into their own panel beside the scene view, shown only while exactly one placed tilemap is selected in the scene tab; the free viewports keep their content in place on screen when they are resized (#142)
adminandClaude Opus 52026-09-18 11:46:23 +02:00