Document admin-only deletes, file modes and Esc as Cancel (#85)

Brings the pages in line with refactoring round 8 of the code.

- API: deleting an image, font or sound is for administrators only, a
  bad upload answers 422, private files are 0640 and the web server
  answers 403 for them, and saves take turns on a lock file. The line
  that said there was no authentication yet is gone.
- Home: the "Not yet" list named several scenes in one run, auth and
  graph comments and groups, which all exist.
- Concepts, Keyboard, Vector art: Escape on a question is its Cancel,
  deleting a project leads back to the picker, and Delete removes
  shapes picked in the list.
- Architecture: AtomicFile, routeFiles, HashedFileStore, the lock
  file, removeSelection and removeShapes, objectName and groupName,
  and the Escape of a Dialog.
- Development: the check sum, and the race check in
  accounts.test.php needs pcntl.
- Eight pages lose the 38 characters Gitea flagged as ambiguous, and
  quoted labels follow the editor's American spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adminandClaude Opus 5.5 committed 2026-09-29 18:13:11 +02:00
1 parent 496a2118b9
commit 7995918921
14 files changed
+82 -61

No files matched your search

+15 -9
@@ -20,14 +20,14 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
| DELETE | `projects/{id}/release` | 204, the link is dead; **404** when the project was not released |
| GET | `releases/{token}` | `{name,publishedAt,project}` — the released copy, **without a session**: the one route besides `auth/*` open to everybody. `project.id` is the token, not the project's id. **404** for an unknown or taken-down token |
| GET | `textures` | `[{file,name,width,height,bytes,updatedAt}]` — what the image picker offers |
| POST | `textures&name=hero.png` | **raw image bytes** → `{file,name,width,height,bytes}` (201) |
| DELETE | `textures/{file}` | 204 |
| POST | `textures&name=hero.png` | **raw image bytes** → `{file,name,width,height,bytes}` (201); **422** for bytes that are no readable image |
| DELETE | `textures/{file}` | 204. Administrators only: a project of another account may wear the file. The editor never deletes one |
| GET | `fonts` | `[{file,name,bytes,updatedAt}]` — what the font picker offers |
| POST | `fonts&name=Serif.ttf` | **raw font bytes** → `{file,name,bytes}` (201) |
| DELETE | `fonts/{file}` | 204 |
| POST | `fonts&name=Serif.ttf` | **raw font bytes** → `{file,name,bytes}` (201); **422** for bytes that are no font |
| DELETE | `fonts/{file}` | 204. Administrators only |
| GET | `sounds` | `[{file,name,bytes,updatedAt}]` — what the sound picker offers |
| POST | `sounds&name=jump.wav` | **raw sound bytes** → `{file,name,bytes}` (201) |
| DELETE | `sounds/{file}` | 204 |
| POST | `sounds&name=jump.wav` | **raw sound bytes** → `{file,name,bytes}` (201); **422** for bytes that are no sound |
| DELETE | `sounds/{file}` | 204. Administrators only |
| POST | `pixels` | **raw cel bytes** → `{file,bytes}` (201) — the pixels of one layer on one frame of [pixel art](Pixel-Art); **422** for anything that is not a cel. No listing and no delete: a cel is found by the name in the project |
| GET | `plugins` | `[{file,bytes,updatedAt}]` — every `.js` in `data/plugins/`, sorted by file name |
| GET | `plugins/{file}` | the module source as `text/javascript` (`nosniff`, `no-cache`); 404 for anything but a bare file name there — see [Plugins](Plugins) |
@@ -40,8 +40,9 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
one. Projects belong to an account: `projects` lists only the caller's own, and somebody else's
project answers **404**, not 403 — that an id exists is already more than a stranger should learn.
An **administrator** (#180, #179) reads, saves and deletes every project with the same routes, and
a save keeps the project's `owner`; their `projects` list stays their own. The `users` routes
answer anybody but an administrator **403** `{"error":"Administrators only"}`.
a save keeps the project's `owner`; their `projects` list stays their own. The `users` routes and
deleting a stored image, font or sound answer anybody but an administrator **403**
`{"error":"Administrators only"}`.
Images, fonts, sounds, cels and plugins are shared between accounts; being signed in is still the price
of admission. See [Accounts](Accounts).
@@ -86,4 +87,9 @@ the server strips directories, control characters and anything not valid UTF-8,
pickers show; an upload that carries no `name` leaves the known one alone rather than erasing it.
A file uploaded before names were kept simply has `""`, and reads as it always did.
There is no authentication yet; `data/` is world-readable. Both are phase-2 topics.
**What the web server hands out as files** is only images, fonts, sounds and cels. Everything else
under `data/` (projects, snapshots, releases, accounts, sessions) is written readable for PHP alone
(`0640`, `AtomicFile::HIDDEN`), so nginx answers **403** for it and the sign-in cannot be walked
around. Every file the API writes goes through `AtomicFile`: a temp file beside it, its mode, then a
rename, so no reader sees half a file or a file with the wrong mode. Saves of one project take turns
on a lock file beside it (`{id}.json.lock`), which stays when the save is done.
+20 -9
@@ -42,7 +42,8 @@
the node palette uses. Entries can be grouped with `{ separator: true }` — a line, dropped when
a search flattens the list or when it would open, close or double up the menu. A field that
opens a menu is a `Dropdown`: value flush left, chevron drawn flush right rather than written
into the label.
into the label. A `Dialog` (a question with buttons) takes Escape as its plain button, the one
neither primary nor danger: Escape and Cancel lead back to the same place.
Every label that can be wider than its control is cut with an ellipsis (`truncate` in
`Label.ts`, shared by `Label`, `ListRow` and `Button`). A `Button` keeps the full text and
measures from it — only what is drawn is shortened — and a subclass that puts something else
@@ -75,7 +76,8 @@
web/index.html Vite source page (dev) index.html built page (generated, served)
web/play.html a released game (#266) play.html built page (generated, served)
src/main.ts, play.ts the two entries; page.ts is the canvas both are (the WebGL context, root, resize)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types, and the walks over a nested value: `mapNested`, a struct field taken out or renamed), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject (and what a list calls one:
`objectName`, `groupName`), Properties (value types, and the walks over a nested value: `mapNested`, a struct field taken out or renamed), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips; a cubic Bezier at `t` and cut in two at `t` — shared by vector art, its Boolean operations and the curve grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance)
src/core/commands/ undoable commands (pixelArtCommands: one command runs any pixel art edit and keeps the state before it, #130), CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), MoveCommand base (undone by moving back: tile picture, tilemap layer, vector shape), vectorArtCommands (#129: the shapes of a drawing, one gesture one command), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
@@ -96,8 +98,9 @@ src/core/store/ serialize (untrusted JSON → Project), projectPlugins (w
SnapshotStore/HttpSnapshotStore (the snapshots of a project, #231),
ReleaseStore/HttpReleaseStore (a project's released copy and the link to it, #266),
AuthStore/HttpAuthStore (signing in and out, #177; changing the password, #182),
FileStore (what the server holds of one kind) + HttpFileStore (one generic client over the three
file collections) — TextureStore, FontStore and SoundStore are its type parameters, nothing else
FileStore (what the server holds of one kind: upload and list, no delete) + HttpFileStore (one
generic client over the three file collections) — TextureStore, FontStore and SoundStore are its
type parameters, nothing else
src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview
src/runtime/ GameRun (one run: start, tick, `Load scene` — shared by Play and a released game, #266),
World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection),
@@ -156,7 +159,9 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run),
ReleaseDialog + releases (the link behind "Release…", what the dialog says and warns about, #266),
ProjectSettingsDialog + settingsChanges (name, the start-scene switch and the collision
layers in one dialog; the pure rule of what Apply has to carry out),
EditorRun (run in editor), state, actions, shortcuts, clipboard (#240: copy and paste of
EditorRun (run in editor), state, actions (what several places do alike: `removeSelection`
is Delete for every kind of selection, one case each, `removeShapes` the shapes of a vector
art), shortcuts, clipboard (#240: copy and paste of
objects and graph pieces — where a paste lands, into which parent, what a graph may take;
the snapshot is `core/model/clipboard`, the paste `core/commands/pasteCommands`),
objectGroups (#241: the group entries of the object menu and the group row's menu;
@@ -274,12 +279,18 @@ src/editor/list/ InlineList (a list's items drawn where the list stands, a
src/editor/variables/ VariablesEditor (the Variables tab: every variable of an owner, or one list),
target (the document of a tab, how much of a list the inspector draws)
src/editor/console/ log (ring buffer, argument formatting, the console.* capture), ConsolePanel (the tail under the tabs)
api/ PHP backend: index.php (routing and the sign-in gate),
lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php,
api/ PHP backend: index.php (routing and the sign-in gate; `routeFiles` serves images, fonts and
sounds alike, and only an administrator deletes one),
lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php
(Store: saves of one project take turns on `{id}.json.lock`, which stays;
the image, font and sound stores extend HashedFileStore — hashed names, keeping,
listing, deleting — and add only which bytes they take and what a record says),
lib/AtomicFile.php (every file the API writes: temp file, mode, rename; `HIDDEN` 0640 for
projects, snapshots, releases, accounts and sessions, so the web server does not hand them
out as files, `SERVED` for the uploads),
lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/)
lib/SnapshotStore.php (#231: data/snapshots/{projectId}/, one copy of the project per file)
lib/ReleaseStore.php (#266: data/releases/{token}.json and which token a project has, both 0640
so the web server does not hand them out as files)
lib/ReleaseStore.php (#266: data/releases/{token}.json and which token a project has)
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads,
data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written
in each project (#119)
+4 -4
@@ -25,9 +25,9 @@ its slant.
## Shapes
- **Box** — `width` × `height`.
- **Box** — `width` x `height`.
- **Circle** — `radius`.
- **Capsule** (#151) — fills `width` × `height` and is rounded along the longer side: its radius is
- **Capsule** (#151) — fills `width` x `height` and is rounded along the longer side: its radius is
half the shorter side. As wide as tall it is a circle. The shape for a figure that should slide
over edges instead of catching on them.
- **Polygon** (#151) — `points`, a list of `{x, y}` corners in the collider's own units, so they
@@ -48,7 +48,7 @@ its slant.
like a polygon. The corners are edited in the Texture tab, not in the scene view. Without a frame,
with a whole texture instead of a frame, or on a frame nobody outlined, the collider is drawn
**red** as a box and collides with nothing: the frame's box when there is one, else its own
`width` × `height`.
`width` x `height`.
Box against box, circle against circle and box against circle keep their own exact tests. Every
pair with a capsule or a polygon goes one common way (`core/properties/convex.ts`): each shape is a
@@ -75,7 +75,7 @@ object stands for its collider children, a collider for itself. The geometry liv
which a turned parent turns as well — until one runs into a solid collider; there the object stops
flush (`exitDistance` in `collider.ts`: the travel at which the shapes stop overlapping). What is left
of the move then goes on **along the surface**: the part into it is dropped, and the part along it
is shortened by **friction × the part into it** (Coulomb; the pair's friction is the average of both
is shortened by **friction times the part into it** (Coulomb; the pair's friction is the average of both
colliders, a solid tile brings its own, see [Physics](Physics#the-material)). The contact's normal decides which way "along" is, so a circle that
runs into an edge rolls up over it, an object pressed onto a slope slides down it unless the pair
is rough enough to hold it (friction ≥ tan of the slope), and one pressed against a wall slides
+3 -2
@@ -104,7 +104,7 @@ no snapshot is made. **Restore** asks first, then keeps the project as it is now
its own, `Before restoring <name>`, and writes the snapshot back as a new revision — so a restore
can itself be taken back. The project is loaded again, which starts the undo history over, and a
second tab still on the old revision is told the project was changed elsewhere. **Delete** asks,
then removes the one picked. A snapshot is only a copy of the project document: images, fonts,
then removes the one picked. Cancel on either question, or Esc, leads back to the list. A snapshot is only a copy of the project document: images, fonts,
sounds and pixel art are stored by their content and never removed, so everything it names is
still there. Deleting the project deletes its snapshots. Whoever can open a project can use its
snapshots, an administrator included.
@@ -139,7 +139,8 @@ out lives in the menu now: the project list in *Open project…*, the project na
**start-scene switch** (*Start scene* / *Active scene*, see **Start scene**), the **While playing**
preset (see above), the **symbol scale**
of the scene view's icons (see [Scene editing](Scene-editing)) and the collision layers in
*Project settings…*. The active scene is picked where the scenes are — by double-clicking
*Project settings…*. A project is deleted in *Open project…*: **Delete** asks first, and Cancel, Esc
and the deletion itself all lead back to the project list. The active scene is picked where the scenes are — by double-clicking
one in the **Assets** panel. The **Assets** panel's `+` creates an asset and opens its tab; its menu lists the types in
two alphabetical groups, first what can be placed into a scene, then what only lives in the library
(#43) — the line between them answers the question that follows right after "which type?".
+4 -2
@@ -13,10 +13,12 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
php tools/make-user.php <name> <password> [admin|user] # an account; every tool signs in (see Accounts)
php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job)
php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job);
# the saves-at-once check needs pcntl, which the CI image
# has and the web server's container has not (SKIP there)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1580 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1586 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+3 -3
@@ -47,10 +47,10 @@ into issues #1-#50, closed ones included, each carrying the commits it was filed
## Not yet
Several scenes in one run (a `Load scene` node — screens are containers
today), auth, touch-optimised layout, graph comments/groups. Sound has no mixer, no fade and no
A touch-optimised layout. Sound has no mixer, no fade and no
`Set volume` on a playing voice (see [Sounds](Sounds)). A frame is always stretched to
the sprite's `width` x `height`: no nine-slice. Animations have no events on the
timeline and no preview in the Scene tab. Slicing cuts a regular grid only — no detection of where the sprites actually
sit on the sheet. Unreferenced image files are never collected — deliberately, since the image
picker offers them again; nothing deletes a stored image except `DELETE ?r=textures/{file}`.
picker offers them again; nothing deletes a stored image except `DELETE ?r=textures/{file}`, which
only an administrator may call.
+3 -2
@@ -1,6 +1,7 @@
`Ctrl+Z` / `Ctrl+Shift+Z` / `Ctrl+Y` undo/redo (`Cmd` works wherever `Ctrl` does) · `Delete` or
`Backspace` remove the selection (objects, nodes, frames, keys or a row of the Animation tab, a graph group or comment — the box only, its nodes stay, and a band that caught boxes and nodes together removes both in one step; an asset asks first) · `Ctrl+G` create
`Backspace` remove the selection (objects, nodes, frames, the shapes of a vector art, keys or a row of the Animation tab, a graph group or comment — the box only, its nodes stay, and a band that caught boxes and nodes together removes both in one step; an asset asks first) · `Ctrl+G` create
container from the selection · `Ctrl+Enter` play, and pause/resume while playing · `Esc` stop ·
`Esc` on a question does what its Cancel does ·
`Ctrl+C` copy the selected objects or graph nodes, `Ctrl+V` paste them
under the pointer ([Scene editing](Scene-editing), [Behaviors](Behaviors)) · `Ctrl+V` import an image from
the clipboard as a texture when nothing copied fits the tab (ignored while a text field has focus) · Animation tab: `Ctrl+C` copy
@@ -16,4 +17,4 @@ milliseconds, the index and count ports of list and text nodes) set `NumberField
`decimals: 0` and round to whole numbers · viewport: wheel zooms in whole steps (1/8, 1/4, 1/2,
then 1 to 6, 8, 12, 16, 24 screen pixels per world pixel; see [Scene editing](Scene-editing)),
middle-drag or `Shift`+drag pans, drag on empty space rubber-bands, `Ctrl`/`Shift`-click toggles an
object in the selection (hierarchy rows too) · tilemap panel: holding `Alt` switches to Pick – a click takes that cell's tile from the active layer – and releasing it brings back the tool before ([Tilemaps](Tilemaps)) · Vector art tab: `Alt`-click a point to make it symmetric and straight again, double-click takes it away, `Esc` ends the line the Pen is drawing, `Delete` removes the selected shape ([Vector art](Vector-Art)).
object in the selection (hierarchy rows too) · tilemap panel: holding `Alt` switches to Pick — a click takes that cell's tile from the active layer — and releasing it brings back the tool before ([Tilemaps](Tilemaps)) · Vector art tab: `Alt`-click a point to make it symmetric and straight again, double-click takes it away, `Esc` ends the line the Pen is drawing, `Delete` removes the selected shapes, picked on the canvas or in the list ([Vector art](Vector-Art)).
+2 -2
@@ -141,8 +141,8 @@ body's colliders, since the group sits on the collider itself.
| Node | What it does |
|---|---|
| **Apply force** | Pushes with a force (units × kg per second²) for the length of the frame, so from `On update` it pushes steadily at any frame rate. A heavier body speeds up less. |
| **Apply impulse** | A kick at once (units × kg per second): the velocity changes by impulse ÷ mass. A jump, a hit, an explosion. |
| **Apply force** | Pushes with a force (units · kg per second²) for the length of the frame, so from `On update` it pushes steadily at any frame rate. A heavier body speeds up less. |
| **Apply impulse** | A kick at once (units · kg per second): the velocity changes by impulse ÷ mass. A jump, a hit, an explosion. |
| **Set velocity** | Sets the speed in units per second, whatever the body weighs. For one direction only, feed the other from `Get velocity`. |
| **Set angular velocity** | Sets the turning speed in degrees per second, counted like the rotation. A body with *Fixed rotation* does not turn, and does not start to later either. |
| **Get velocity** | Speed (x, y) and turning speed (degrees/s); 0 for an object without a dynamic body. |
+1 -1
@@ -33,7 +33,7 @@ A click picks the colour to paint with.
| Control | What it does |
|---|---|
| Colour field | Changes the picked colour of the palette, one undo step per change |
| **+ Colour** | Adds a copy of the picked colour at the end and picks it (up to 255 colours) |
| **Add** | Adds a copy of the picked colour at the end and picks it (up to 255 colours) |
| **Remove** | Takes the picked colour out of the palette |
| **Create palette** | Only while the picture names no palette: makes one and names it |
+1 -1
@@ -26,7 +26,7 @@ signature, so nothing a request claims can decide what is written.
| Row | Meaning |
| --- | --- |
| **Volume** | How loud this sound is by itself, 0–1. |
| **Volume** | How loud this sound is by itself, 0-1. |
| **Loop** | Whether it is music: it repeats until something stops it. |
The preview plays through the very same library a running game uses — same decode, same device —
+3 -3
@@ -75,18 +75,18 @@ wearing *any* of its frames, not just the ones naming the texture directly.
A frame can carry **outlines** a tile made of it blocks with (#141; see
[Tilemaps](Tilemaps#outlined-tiles)), and a collider of the shape *Frame collision* too (#193; see
[Colliders](Colliders#shapes)). Several, because an autotile's block holds several shapes –
[Colliders](Colliders#shapes)). Several, because an autotile's block holds several shapes —
its bordered tile and its notches. Switch **Collision** on in the Texture tab's header: a press
then edits the selected frame's outlines instead of the frames.
- **Click** inside the frame to add a corner after the last one of the *active* outline – that is
- **Click** inside the frame to add a corner after the last one of the *active* outline — that is
how an outline is drawn from nothing. The active one is drawn heavier; on a frame just selected it
is the last outline.
- **Ctrl+click** (Cmd+click) starts a new outline there.
- **Drag** a corner to move it, **press on an edge** to put a corner there, **double-click** a
corner to take it away. Each of these makes that outline the active one, and so does a click
inside another finished outline.
- **Delete** removes the active outline whole – in collision mode never the frame.
- **Delete** removes the active outline whole — in collision mode never the frame.
Corners land on whole pixels inside the frame and are stored relative to its top-left corner, so
they move with the frame. Every step is one undo step. The outlines of all frames are always shown;
+11 -11
@@ -29,7 +29,7 @@ edge.
A placed map grows and shrinks at **any** edge (#136). Under its properties the inspector shows
**GRID EDGES** with a row each for **Left**, **Top**, **Right** and **Bottom**: **+** adds one
column or row at that edge, **−** removes one. Every click is one undo step.
column or row at that edge, **-** removes one. Every click is one undo step.
- **The painted cells stay where they are in the scene.** Cell `0,0` is always the top-left
corner, so at the left and top edge the cells of **every layer** move one column or row in; and
@@ -256,16 +256,16 @@ The outline in the scene view shows the drawn-in or grown shapes.
A tile whose first picture is a frame with **collision outlines** (drawn in the Texture tab, see
[Textures](Textures#collision-outline)) blocks with those outlines instead of its whole square
(#141): a slope, a rock, a tree trunk, the shore of a lake. Such cells are not merged with the
rectangles – they bring their own polygons – but the scene view draws them together with the
rectangles — they bring their own polygons — but the scene view draws them together with the
rectangles, along the outer edge only.
**A plain tile** (kind *None*) lays all its outlines over the cell: scaled from the frame onto the
cell, and drawn in or out about the cell's centre by the tile's collision scale.
**A floor or wall autotile** is drawn from four quarters of its block per cell, picked by the
neighbours – so its outlines are drawn over the *whole block* and cut along the same quarters, and
neighbours — so its outlines are drawn over the *whole block* and cut along the same quarters, and
every cell takes the pieces of exactly the quarters its picture is made of. An outline that follows
the drawing continues across cells the way the drawing does: outline the bordered 2 × 2 tile of a
the drawing continues across cells the way the drawing does: outline the bordered 2x2 tile of a
floor a little inside its shore, and every lake painted with it blocks a little inside its shore,
at straight edges and outer corners alike.
@@ -274,7 +274,7 @@ at straight edges and outer corners alike.
anything is drawn in a tile of the block, only what is drawn blocks there, and a quarter can be
left open on purpose.
- Drawing past a tile's edge is harmless: what reaches into another quarter is cut off there.
- A cell drawn only from quarters that block whole – the inside of a lake – is a plain square and
- A cell drawn only from quarters that block whole — the inside of a lake — is a plain square and
is merged with its neighbours like any other.
- The collision scale does not apply to an autotile with outlines: the outline is the shape. An
autotile without one blocks as its square, with its collision scale, as before.
@@ -304,13 +304,13 @@ There are no trigger tiles yet: a tile blocks or it does not.
Merged rectangles are few, outlined cells are many: every painted cell of an outlined tile is a
polygon of its own, and every one of those is cut into convex pieces that are tested against each
other piece by piece. A map of 20 × 20 outlined cells is 400 solids under one entity, where the
other piece by piece. A map of 20x20 outlined cells is 400 solids under one entity, where the
same map of plain solid tiles is a handful of rectangles.
That is why a move, a ray and the editor run's detection each ask the map only for **the cells of
the rectangle they can reach** (#184): the rectangle is taken back into the map's own units –
a turned map through its own axes – and turned into cell coordinates, so cells outside it are never
worked out in world units at all. Measured on a map of 20 × 20 outlined cells of 16 corners each,
the rectangle they can reach** (#184): the rectangle is taken back into the map's own units —
a turned map through its own axes — and turned into cell coordinates, so cells outside it are never
worked out in world units at all. Measured on a map of 20x20 outlined cells of 16 corners each,
one `Move` cost 148 ms before and 0.26 ms after; with 8 corners, 34 ms and 0.27 ms. Nothing about
the result changes — what is left out could not have been met.
@@ -328,7 +328,7 @@ again, so a map that only stood there cost a frame. Now:
- The same cells hand out the very same rectangles and polygons. The physics tells by that, and by
the map's place and settings, that it need not work the map's pieces out again every tick.
Measured on a map of 39 × 18, 8 × 8 and 43 × 20 cells on three layers, with an animated water
Measured on a map of 39x18, 8x8 and 43x20 cells on three layers, with an animated water
tile and an outlined hill tile (5 outlines), script time per frame on the main thread went from
17.7 to 5.8 ms with nothing selected, from 26.2 to 8.6 ms with the map selected and from 29.9 to
8.7 ms in Play. That is 52, 38 and 33 fps before and 60 fps in all three after.
@@ -362,7 +362,7 @@ is an empty cell, and a single cell drops the count:
```
Cells outside the grid are stored as well, so a map that was made smaller gets them back when it
grows again. Measured: a full map of 100 × 100 cells on two layers went from 736 KB to 11 KB.
grows again. Measured: a full map of 100x100 cells on two layers went from 736 KB to 11 KB.
The map of the first version (#128) is not read any more (#137): such a layer loads empty.
## What it does not do yet
+7 -7
@@ -9,10 +9,10 @@ A tile is one of four **kinds**:
| Kind | Block | Tiles it produces | What it does |
|---|---|---|---|
| **None (1 × 1)** | 1 tile | 1 | the picture as it is, repeated in every painted cell — the default |
| **Floor (2 × 3)** | 2 tiles wide, 3 tall | 47 | an **autotile**: edges, convex corners and **notches** (inner corners) |
| **Wall (2 × 2)** | 2 tiles wide, 2 tall | 16 | an **autotile**: edges and convex corners, no notches |
| **Terrain (6 × 4)** | 6 tiles wide, 4 tall | 47 | an **autotile** in the layout of the common top-down sheets, see [below](#terrain) |
| **None (1x1)** | 1 tile | 1 | the picture as it is, repeated in every painted cell — the default |
| **Floor (2x3)** | 2 tiles wide, 3 tall | 47 | an **autotile**: edges, convex corners and **notches** (inner corners) |
| **Wall (2x2)** | 2 tiles wide, 2 tall | 16 | an **autotile**: edges and convex corners, no notches |
| **Terrain (6x4)** | 6 tiles wide, 4 tall | 47 | an **autotile** in the layout of the common top-down sheets, see [below](#terrain) |
A plain tile (*None*) ignores its neighbours. The autotile kinds draw their own edges: paint a
patch of grass onto a map and the borders towards everything else — the rounded corners, the
@@ -24,7 +24,7 @@ A block is a picture cut into **quarter tiles**. For a plain tile the block is t
its four quarters are drawn where they are. The two autotile kinds are the two shapes the classic
RPG-Maker sheets use.
The bottom 2 × 2 tiles of either block are one fully bordered tile: its middle four quarters are
The bottom 2x2 tiles of either block are one fully bordered tile: its middle four quarters are
the inside, the ring around them the borders and the convex corners. A floor block has one tile row
more; its **right** tile holds the four notches — the piece for a corner whose two sides are joined
but whose diagonal is not. The tile to the left of them is a lone tile, the same picture the four
@@ -82,7 +82,7 @@ asks for; a picture that does not fit is kept and named, never silently dropped.
like any other picture — it has no frames, so it is always the whole canvas —, or
[pixel art](Pixel-Art), whole or one of its frames. Cutting frames is
the [Textures](Textures) tab, by hand or with **Slice…** where the sheet is a regular grid.
- **Kind** — *None (1 × 1)*, *Floor (2 × 3)*, *Wall (2 × 2)* or *Terrain (6 × 4)*. Chosen, not guessed: a sheet whose rows alternate
- **Kind** — *None (1x1)*, *Floor (2x3)*, *Wall (2x2)* or *Terrain (6x4)*. Chosen, not guessed: a sheet whose rows alternate
between the two shapes (`Outside_A4` in RPG Maker) would be read wrong either way, and the choice
is one click.
- **Frames per second** and **Playback** (*Loop* or *Ping-pong*) — how the pictures run. One
@@ -103,7 +103,7 @@ asks for; a picture that does not fit is kept and named, never silently dropped.
cell of this tile is sorted as if it stood that many rows further down (#228): a treetop at 1
sorts with the trunk below it. See [Tilemaps](Tilemaps#sorting-by-y).
- A tile whose picture is a frame with **collision outlines** blocks with them instead of its
square (#141) – an autotile with its outlines cut into the same quarters as its picture; see
square (#141) — an autotile with its outlines cut into the same quarters as its picture; see
[Tilemaps](Tilemaps#outlined-tiles).
Kind, rate, playback, the solid switch, the collision scale and the sort offset are ordinary properties, so the inspector shows them like any other asset's
+5 -5
@@ -10,7 +10,7 @@ not placed in a scene — it is worn by something that is.
## The canvas
Every drawing has a canvas, **256 × 256** units by default, set in the inspector as the rows
Every drawing has a canvas, **256x256** units by default, set in the inspector as the rows
**Canvas width** and **Canvas height** (up to 4096). It is two things at once:
- **The frame.** Whatever a shape reaches past the edge is not in the picture. There is no mask and
@@ -73,7 +73,7 @@ With **Paint** the Point menu makes way for **Handles**, see [Gradients and text
- **Ctrl+click** (Cmd on a Mac) adds a shape to the selection or takes it out again. The shapes
selected alongside show their outline; only the one clicked last shows its points and handles.
A plain click selects that one shape alone.
- **Delete** removes the selected shapes.
- **Delete** removes the selected shapes, also when they were picked in the list of the inspector.
The point pressed last — or put down last with the Pen — is **picked** and drawn filled. The
**Point** menu in the toolbar shows its type and turns it into another one, as one undo step:
@@ -132,19 +132,19 @@ outline:
| Type | Fill | Outline | Rows under it |
|---|---|---|---|
| **Solid colour** | yes | yes | Fill, or Line colour |
| **Solid color** | yes | yes | Fill, or Line color |
| **Linear gradient** | yes | yes | the stop bar and the picked stop |
| **Radial gradient** | yes | yes | the stop bar and the picked stop |
| **Texture** | yes | no | Texture |
- **Switching** is one undo step. A new gradient runs from the shape's colour (or the line's) to
white, across the middle of the shape. Changing between linear and radial keeps the colours and
lays the gradient out anew. The solid colour stays on the shape underneath, so **Solid colour**
lays the gradient out anew. The solid colour stays on the shape underneath, so **Solid color**
brings back exactly the colour it had.
- **The stop bar** shows the gradient over a checkerboard, with a marker under it for every colour
stop. A click on a marker picks that stop, dragging it moves it along. A click on the bar anywhere
else adds a stop there, in the colour the gradient already has at that place, so adding one
changes nothing until it is edited. **Stop colour**, **Stop opacity** and **Stop position** edit
changes nothing until it is edited. **Stop color**, **Stop opacity** and **Stop position** edit
the picked stop; **Remove stop** takes it out, down to the two a gradient needs.
- **Texture** offers the uploaded textures and their frames. Drawings are left out: a drawing filled
with a drawing, itself included, would have to be rasterised while it is being rasterised.