diff --git a/API.md b/API.md index e5da3ab..ee4d40d 100644 --- a/API.md +++ b/API.md @@ -20,9 +20,8 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT | 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 | -| GET | `plugins` | `[{file,bytes,updatedAt,active}]` — every `.js` in `data/plugins/`, sorted by file name, switched on or not | -| GET | `plugins/{file}` | the module source as `text/javascript` (`nosniff`, `no-cache`); 403 while the plugin is switched off, 404 for anything but a bare file name there — see [Plugins](Plugins) | -| PUT | `plugins/{file}` | `{active}` → the entry as above. Switches a plugin on or off, server-wide, stored in `data/plugins/plugins.json`; 422 when `active` is not a boolean | +| 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) | **Everything but `auth/*` needs a session** and answers **401** `{"error":"Sign in first"}` without one. Projects belong to an account: `projects` lists only the caller's own, and somebody else's @@ -30,6 +29,9 @@ project answers **404**, not 403 — that an id exists is already more than a st Images, fonts, sounds and plugins are shared between accounts; being signed in is still the price of admission. See [Accounts](Accounts). +Which editor plugins a project switches on is the project's own field `plugins` (#119); the server +has no switch of its own and hands out every plugin file it has. + Every project document carries `formatVersion` (`PROJECT_FORMAT_VERSION` in `core/model/Project.ts`, currently 2). There are no load-time migrations: `serialize.ts` reads what it recognises and drops the rest. diff --git a/Architecture.md b/Architecture.md index 4fa626b..40db1b9 100644 --- a/Architecture.md +++ b/Architecture.md @@ -68,7 +68,7 @@ ``` web/index.html Vite source page (dev) index.html built page (generated, served) -src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), 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), 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, the outlined cells as polygons – an autotile's outlines cut into its quarters and pieced together per cell like its picture –, and where one sits in the world), 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), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead) +src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), 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), 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, the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, and where one sits in the world), 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), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead) 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 (with the side a one-way collider blocks from), 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/commands/ undoable commands, CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts src/core/graph/ graph model, NodeRegistry/NodeContext/values, interpreter, dropTarget, nested (behaviors as nodes), @@ -78,7 +78,9 @@ src/core/graph/ graph model, NodeRegistry/NodeContext/values, interpreter lookup (what a node may look up: behaviors, state machines), keyRepeat; nodes/{events,flow,data,action,variable}/.ts src/core/particles/ nodes (the catalogue), compile (a graph → one function per output), emission (which particles are alive at a second — a pure rule, no state) -src/core/store/ serialize (untrusted JSON → Project), tilemapCells (the compact form of a layer's cells in the file), http (one API call), ProjectStore/HttpProjectStore, +src/core/store/ serialize (untrusted JSON → Project), projectPlugins (which plugins a project switches on, read off the raw + file before it is deserialized, #119), tilemapCells (the compact form of a layer's cells in the file), + http (one API call), ProjectStore/HttpProjectStore (load takes a `prepare` that sees the raw JSON first), 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 @@ -106,10 +108,10 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run), EditorRun (run in editor), state, actions, shortcuts, debugHook, textProbe (finding widgets by their text — the ground floor of the test hook, shared with the sign-in screen), auth/LoginScreen + auth/PasswordDialog + auth/loginRules (the sign-in screen and the change-password dialog in the canvas, and what either may complain about, #177/#182), autosave, - polygonEdit (corners of an outline: what is under the pointer, whether a point is inside, put / move / take away – scene view and Texture tab), + polygonEdit (corners of an outline: what is under the pointer, whether a point is inside, put / move / take away — scene view and Texture tab), fileImport (drop/paste) → assetImport (one upload pipeline, an ImportKind per sort) ← textureImport / fontImport / soundImport, FilePicker (the shared "+ → Texture / Font / Sound" dialog; ImagePicker/FontPicker/SoundPicker only describe their row), - framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time), AssetTabEditor (the shared shell of the asset tabs), TabHeader (the bar across the top of a tab – asset tabs, Plugins tab, NodeBoard), + framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time), AssetTabEditor (the shared shell of the asset tabs), TabHeader (the bar across the top of a tab — asset tabs, Plugins tab, NodeBoard), TabStrip + tabRow (the tab row and what it does when the tabs outgrow it; plugin tabs are inserted at run time), assetTabs (which asset kind opens into which tab, and so which kind has an editor at all), TabButton (how a tab is drawn: the card of the one showing, plain weight, no pressed fill, #176), @@ -117,7 +119,9 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run), after a close, a tab going on to another document — pure, so checkable without a browser). One editor per kind serves every open document of that kind: `Editor` points it at the one on screen (`AssetTabEditor.showAsset`), and it keeps each document's view through `saveView`/`restoreView` -src/editor/plugins/ startPlugins (list, import, host) → loader (PluginManager: order, checks, switching on and off, what became of each file), pluginApi (API v1, the per-plugin scope), PluginsPane (the Plugins tab), NodeBoard (api.ui) +src/editor/plugins/ startPlugins (list, import, host; runs before the first project) → loader (PluginManager: syncTo makes the + running plugins match what the project asks for, order, checks, what became of each file), pluginApi + (API v1, the per-plugin scope), PluginsPane (the Plugins tab), NodeBoard (api.ui) src/editor/viewport/ WorldViewport base → FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas; GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/ src/editor/inspector/ Inspector panel, host (what a section builds into), field factory, previewHandlers (scrub → preview → command), valueSpecs @@ -126,11 +130,11 @@ src/editor/inspector/ Inspector panel, host (what a section builds into), field every variable), portSection, aspectAction, objectQueries/vectorRows/parentRow src/editor/panels/ AssetPanel, assetGroups (groups by type and their order, #176), assetIcons (one SVG per asset type in icons/, shared by the panel, the + menu, the tabs and Scene objects, - plus the List and Game tabs' own two, which stand for no asset), + plus the List, Game and Plugins tabs' own three, which stand for no asset), assetMenu (the two groups of the + menu), HierarchyPanel, Toolbar, TilemapPanel (tools, tiles and layers of the one selected tilemap, beside the scene view, #142; painting itself is SceneViewport) -src/editor/graph/ GraphEditor (header, breadcrumb), GraphCanvas, graphView (zoom limits, grid, panAndZoom – shared with NodeBoard and the Tile tab's TileCanvas), EdgeLayer (curves and fold marks), graphMenus, +src/editor/graph/ GraphEditor (header, breadcrumb), GraphCanvas, graphView (zoom limits, grid, panAndZoom — shared with NodeBoard and the Tile tab's TileCanvas), EdgeLayer (curves and fold marks), graphMenus, NodeView, NodePalette + paletteEntries (which also ranks a search), folding (hidden links, collapsed nodes), Breadcrumb + trail (the path into nested behaviors), beads (the run's pulses), AnnotationView + annotationDrag (groups and comments: what a drag carries, each thing moved once) src/editor/container/ ContainerEditor (header), ContainerCanvas (a SceneViewport on a template) @@ -151,7 +155,8 @@ api/ PHP backend: index.php (routing and the sign-in gate), lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php, lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/) data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads, - data/plugins/ editor plugins and plugins.json (which are switched on); filled from plugins/ by the build and the seeder + data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written + in each project (#119) tests/ Vitest unit tests tools/ seed-*, e2e (steps in tools/e2e/, rig in tools/e2e/lib/), smoke-*, lib/, assets/ (the walk demo's sheet) ``` diff --git a/Concepts.md b/Concepts.md index c3a912e..8379093 100644 --- a/Concepts.md +++ b/Concepts.md @@ -77,8 +77,9 @@ one kind can be open at once — two textures, three behaviors — each with its - The tab shows the **icon** of its kind, then the name, so a behavior and a texture both called `Player` stay apart; the `»` menu lists hidden documents the same way. **Scene** and **Game** - carry an icon too (#176) — the scene one from the asset panel, a play sign for the Game tab. - Only **Plugins** and a plugin's own tab go without one. A long name is cut with an + carry an icon too (#176) — the scene one from the asset panel, a play sign for the Game tab, + a puzzle piece for **Plugins** (#119). Only a plugin's own tab goes without one, as it brings + no picture of its own. A long name is cut with an ellipsis. Renaming an asset renames its tab. - A document tab always shows its own asset. Selecting something elsewhere never changes it; there is no picker in the tab's header any more. Selecting what lives only inside a document — a node, diff --git a/Development.md b/Development.md index 8f26f0f..f908476 100644 --- a/Development.md +++ b/Development.md @@ -13,7 +13,7 @@ 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 # an account; every tool signs in (see Accounts) -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1126 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1129 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 diff --git a/Plugins.md b/Plugins.md index 6135a9d..59e1c2b 100644 --- a/Plugins.md +++ b/Plugins.md @@ -1,14 +1,15 @@ -A **plugin** extends the editor. It is one JavaScript file – an ES module – in `data/plugins/` on the -server, and the editor loads every file there when it starts. v1 is deliberately small: it offers what +A **plugin** extends the editor. It is one JavaScript file — an ES module — in `data/plugins/` on the +server, and the editor loads the ones the open project asks for. v1 is deliberately small: it offers what the demo plugin needs (read the assets, add a tab, show nodes on a board) and nothing it does not. **Where plugins come from.** A file is put into `data/plugins/` by hand; there is no upload yet -(#116). A file put there is **off** until someone switches it on in the **Plugins** tab (#114). The plugins that ship with the repository +(#116). A file put there is **off** in every project until someone switches it on in the **Plugins** +tab (#114, #119). The plugins that ship with the repository live in `plugins/` and are copied to `data/plugins/` by `npm run build` and by `tools/seed-e2e.mjs`; -a file of the same name is replaced, anything else in the folder is left alone, and a shipped -plugin is switched on – the build only when the state does not name it yet, the seeder always. -`data/plugins/` is not in the repository. The setting is **server-wide**: every project on the server sees the same -plugins. Plugins run **in the editor only**, never in a running game or an export. +a file of the same name is replaced, anything else in the folder is left alone. +`data/plugins/` is not in the repository. Which plugins run is decided **by the project** (#119): +the folder says what is available, the open project says what is switched on. Plugins run **in the +editor only**, never in a running game or an export. ## The format @@ -34,9 +35,19 @@ the same reason `api.tabs.add` accepts only widgets from `api.ui`. ## Loading -The editor asks `GET ?r=plugins` for the list, then imports each **switched-on** file through -`GET ?r=plugins/{file}` (see [API](API)) – **in file-name order**, one after the other, after the -project has loaded. A plugin that is off is never fetched, so none of its code runs. Nothing a plugin does wrong stops the editor or the other plugins. Each of these +The editor asks `GET ?r=plugins` for the list, fetches the project file, and imports each file the +**project** names through `GET ?r=plugins/{file}` (see [API](API)) — **in file-name order**, one +after the other. Only then is the project read into the model. + +That order is the point of #119: what a plugin registers has to exist before the project is +deserialized, because anything the editor does not know at that moment is dropped as unknown. So a +plugin can bring node types and asset kinds along later on, and a project using them still opens. + +Opening another project switches over: the plugins it does not name are deactivated first, then its +own are activated. A plugin the project does not name is never fetched, so none of its code runs. +A name that is not a file on the server is passed over in silence. + +Nothing a plugin does wrong stops the editor or the other plugins. Each of these leaves the plugin inactive, with a line in the console and a notice in the toolbar: - the file does not load (a syntax error, a failed request), @@ -51,32 +62,37 @@ no tab behind. ## Switching a plugin on and off The **Plugins** tab lists every file in `data/plugins/` with its name, version, file and state, each -with a switch. The state is stored on the server in `data/plugins/plugins.json` -(`{"demo-plugin.js": {"active": true}}`), so it holds for everyone who opens this editor, for every -project. +with a switch. The switch belongs to the **open project**: it is kept in the project file as +`"plugins": ["demo-plugin.js"]`, so it travels with the project and says nothing about any other +one. A new project switches on nothing. A switch takes effect at once, without reloading the editor: -- **On** stores it, then loads and activates the plugin – after every plugin that is running - already, whatever the file names say. -- **Off** stores it, calls the plugin's `deactivate()`, and then takes away what it registered - through the API: its tabs, their content and its `change` listeners. Whoever was looking at one - of its tabs lands on Scene. +- **On** writes it into the project, then loads and activates the plugin — after every plugin that + is running already, whatever the file names say. +- **Off** writes it into the project, calls the plugin's `deactivate()`, and then takes away what it + registered through the API: its tabs, their content and its `change` listeners. Whoever was + looking at one of its tabs lands on Scene. +- It is an **undo step** like any other change to the project, and undo switches the plugin back. - A plugin that is off has no name on its row until it has run once in this session: reading a name out of it would mean running it. -- What the plugin did outside the API – a timer it started, something it wrote to the server – the +- What the plugin did outside the API — a timer it started, something it wrote to the server — the editor cannot take back. That is the same right-to-everything as in **Limits** below. -- When the server does not store the switch, nothing changes and the toolbar says why. +- When the project does not take the switch, nothing changes and the toolbar says why. + +Since the switch is the project's, the server can no longer know whether a file is in use: it hands +out **every** plugin it has, and only the editor decides what runs. That was decided in #117 and is +of no consequence while nothing secret lives in a plugin; #115 and #116 have to take it into account. ## API v1 | Member | What it does | |---|---| | `api.version` | `1` | -| `api.project.assets()` | The assets of the open project in list order, as `{ id, name, type }` – copies, so writing to them changes nothing. | +| `api.project.assets()` | The assets of the open project in list order, as `{ id, name, type }` — copies, so writing to them changes nothing. | | `api.project.on('change', fn)` | Calls `fn` whenever the asset list changes: an asset created, renamed or deleted, by hand, by undo or by loading another project. A change that leaves ids, names and types alone (moving an object, editing a property) does not call it. Returns a function that unsubscribes. A listener that throws is reported and stays subscribed. Any event but `'change'` throws. | -| `api.tabs.add({ id, label, create })` | Adds a tab after the fixed ones (Scene, Game, Plugins), ahead of the open documents; it stays for as long as the plugin is on. `create()` is called once and must return a widget from `api.ui`. Returns `false`, with a console warning, when the id is taken – by a built-in tab (`scene`, `graph`, …) or a plugin that came earlier; `create` is not called then. A missing `id` or `label`, or a `create` that is not a function, throws. | -| `new api.ui.NodeBoard()` | A read-only node graph: `setNodes([{ id, title, x, y }])` replaces what it shows, `fit()` frames it. It has a **Fit** button and a node count above the canvas; a drag with any button pans, the wheel zooms about the pointer. The nodes are drawn by the graph editor's own node view, empty but for their title – no ports, no editing, no selection. The first `setNodes` with anything in it frames the nodes once the tab has a size; after that the view stays where the user put it. A title that is not text is empty, a position that is not a number is 0, and something that is not a list throws. | +| `api.tabs.add({ id, label, create })` | Adds a tab after the fixed ones (Scene, Game, Plugins), ahead of the open documents; it stays for as long as the plugin is on. `create()` is called once and must return a widget from `api.ui`. Returns `false`, with a console warning, when the id is taken — by a built-in tab (`scene`, `graph`, …) or a plugin that came earlier; `create` is not called then. A missing `id` or `label`, or a `create` that is not a function, throws. | +| `new api.ui.NodeBoard()` | A read-only node graph: `setNodes([{ id, title, x, y }])` replaces what it shows, `fit()` frames it. It has a **Fit** button and a node count above the canvas; a drag with any button pans, the wheel zooms about the pointer. The nodes are drawn by the graph editor's own node view, empty but for their title — no ports, no editing, no selection. The first `setNodes` with anything in it frames the nodes once the tab has a size; after that the view stays where the user put it. A title that is not text is empty, a position that is not a number is 0, and something that is not a list throws. | ## The demo plugin @@ -86,7 +102,7 @@ creating, renaming or deleting an asset shows at once. e2e step 38 checks exactl ## Limits -- **A plugin runs with every right the editor has** – it is code on the page, and the `api` object +- **A plugin runs with every right the editor has** — it is code on the page, and the `api` object is a convenience, not a boundary: nothing stops a plugin from calling the PHP API itself. Accepted for v1, open in #115. - No writing to the project, no node types of its own, no asset types, no inspector rows, no hooks