diff --git a/API.md b/API.md index 23a682e..f9106fd 100644 --- a/API.md +++ b/API.md @@ -10,6 +10,9 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT | GET | `textures` | `[{file,width,height,bytes,updatedAt}]` — what the image picker offers | | POST | `textures` | **raw image bytes** → `{file,width,height,bytes}` (201) | | DELETE | `textures/{file}` | 204 | +| GET | `fonts` | `[{file,bytes,updatedAt}]` — what the font picker offers | +| POST | `fonts` | **raw font bytes** → `{file,bytes}` (201) | +| DELETE | `fonts/{file}` | 204 | 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 @@ -17,7 +20,9 @@ the rest. Uploads send the bytes as the body rather than `multipart/form-data`: no boundary parsing in PHP, and PHP's `upload_max_filesize` (2 MB) never applies — only `post_max_size`. The API has its -own caps behind nginx's: 8 MB per request, 4 MB per image (413). The stored -files are served statically from `data/textures/`, so reading an image never runs PHP. +own caps behind nginx's: 8 MB per request, 4 MB per image or font (413). The stored +files are served statically from `data/textures/` and `data/fonts/`, so reading one never runs +PHP. An image is downscaled and re-encoded in the browser when it would not fit; a font cannot +be, so an oversized typeface is refused rather than shrunk. There is no authentication yet; `data/` is world-readable. Both are phase-2 topics. diff --git a/Architecture.md b/Architecture.md index 82f3c59..ea08d6c 100644 --- a/Architecture.md +++ b/Architecture.md @@ -47,13 +47,15 @@ src/core/commands/ undoable commands, CommandStack, SetBagValueCommand base, src/core/graph/ graph model, NodeRegistry/NodeContext/values, interpreter, dropTarget, nested (behaviors as nodes), stateMachine (the State node, a machine's states, the ports they become), variablePorts, propertyPorts, lookup (what a node may look up: behaviors, state machines), keyRepeat; nodes/{events,flow,data,action,variable}/.ts -src/core/store/ serialize (untrusted JSON → Project), http (one API call), ProjectStore/HttpProjectStore, TextureStore/HttpTextureStore +src/core/store/ serialize (untrusted JSON → Project), http (one API call), ProjectStore/HttpProjectStore, + TextureStore/HttpTextureStore, FontStore/HttpFontStore src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem, AnimationSystem, BehaviorSystem src/ui/ widget toolkit (generic, knows nothing about assets) src/viewport/ view (transforms), items (what to draw), symbols (how to draw it), - TextureLibrary, activeCamera — no editor concepts, so tests reach it freely + TextureLibrary, FontLibrary, activeCamera — no editor concepts, so tests reach it freely src/editor/ App → Editor (layout + ticker), PlaySession (the run), ProjectSession (load/create), - EditorRun (run in editor), state, actions, shortcuts, debugHook, autosave, textureImport, + EditorRun (run in editor), state, actions, shortcuts, debugHook, autosave, + fileImport (drop/paste) → textureImport / fontImport, framing (fit/marquee/snap maths), AssetTabEditor (the shared shell of the asset tabs) src/editor/viewport/ WorldViewport base + SceneViewport/GameViewport — 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 diff --git a/Concepts.md b/Concepts.md index 18fa3fe..a01f854 100644 --- a/Concepts.md +++ b/Concepts.md @@ -1,10 +1,11 @@ | Term | Meaning | |---|---| -| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`) and typed properties. Assets have **no position**. | -| **Texture** | An uploaded image. Scenes, textures, animations, state machines and shaders are the five types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). | +| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`, `font`) and typed properties. Assets have **no position**. | +| **Texture** | An uploaded image. Scenes, textures, animations, state machines, shaders and fonts are the six types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). | +| **Font** | An uploaded typeface, worn by a text through the ordinary property `font`; no font means the editor's own. Uploaded and picked exactly like a texture, and stored the same way. See [Fonts](Fonts). | | **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state` and branches on it with `State branch`; each behavior instance keeps its own current state. See [State machines](State-machines). | | **Shader** | A node graph that becomes the GLSL a sprite is drawn with, edited in its own tab. A sprite wears it through `shader`; the numbers the graph declares are rows below that dropdown. See [Shaders](Shaders). | -| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. | +| **Text** | An asset that draws a line of text (`text`, `font`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. | | **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run — **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents); a behavior hears the switch as `On enable` / `On disable`. | | **Scene object** | An asset placed into a scene: a link to the asset + a **position** (`x`, `y`, `z`) + sparse **overrides** of the asset's properties + an optional **parent** (another object of the scene). Drawn objects also carry **transform** rows (scale, rotation, mirror) — properties like any other, and they cascade to children the way a position does (see [Scene editing](Scene-editing)). Scenes cannot be placed. | | **Container** | A reusable group ("prefab"): the asset holds a **template** of objects (asset link, local position, overrides, parent links) centred on its origin. Placing a container stores the container object **and nothing else** — its content is resolved from the template on every read, so a template edit reaches every instance at once. **Create container** turns the current selection into such an asset: the selected objects move into the template and the scene keeps one placed container at their centre (world positions unchanged). The template is edited in the **Container** tab (see [Containers](Containers)) or from an instance through **Apply to container**. | diff --git a/Development.md b/Development.md index 7ddafd5..8c1fb94 100644 --- a/Development.md +++ b/Development.md @@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs # code is lost (tail's is reported), so a red run reads as green npm run build # writes dist/ and regenerates index.html ./build.sh # the same under `set -o pipefail`, for callers that pipe the output -node tools/seed-e2e.mjs && node tools/e2e.mjs # 763 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 775 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 @@ -63,7 +63,7 @@ to touch it. The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, assets, drawing, graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect, container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable, -variable nodes, property nodes, animations, state machines, console, shaders, transforms) that build on each other's +variable nodes, property nodes, animations, state machines, console, shaders, transforms, fonts) that build on each other's scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`, `object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it exercises. @@ -151,4 +151,4 @@ widths to find click points. The **render device is not in this repo** — `/dev and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`, -WebGL 2.0, and the same 763 checks as here. +WebGL 2.0, and the same 775 checks as here. diff --git a/Fonts.md b/Fonts.md new file mode 100644 index 0000000..82b3723 --- /dev/null +++ b/Fonts.md @@ -0,0 +1,51 @@ +A text object is drawn in the editor's own typeface until it is given one of its own. That +typeface is a **font asset**, and the text points at it through the ordinary property `font` — +the same design as a sprite and its `texture`: because it is an ordinary property, instance +overrides, **Apply to container**, **Reset to asset**, undo and `Properties` work on it without +a line written for them. One placed title can wear a different face than its siblings. + +**Getting a typeface in.** **Dropping a `.ttf`, `.otf`, `.woff` or `.woff2` file onto the +canvas** uploads it and makes the asset in one go; several at once are imported one after +another, and a file that fails does not stop the rest. The asset is named after the file +("DejaVuSerif.ttf" becomes "DejaVuSerif"), which is worth more than the stored name — see below. +A dropped file is recognised by its **extension**, not by its MIME type: browsers report a +dropped font as `font/ttf`, `application/x-font-ttf` or as nothing at all. + +**+ → Font** asks the other question and opens the picker: **every typeface the server already +holds**, one row each. There is no thumbnail to show for a font, so each row *is* the preview — +the sample line is drawn in that very face. Picking one and pressing **Use font** makes the +asset; **Upload…** is the way in for a face the server does not have yet. A row captioned with +an asset name is one this project already wears, and picking it uses that asset rather than +making a second one. + +**Where it goes.** The bytes are uploaded to `POST ?r=fonts` and stored as +`data/fonts/{sha256}.{ext}` — named after their own content, exactly like an image, and with +the same four consequences (one file per typeface however often it is uploaded, cacheable +forever, a short string in the project document, an undoable delete). Unlike an image, a font +is **never re-encoded on the way**: what the server stores is the file that was picked. The +format is read from the file's own signature, so nothing a request claims can decide what is +written. Bitmap collections (`.ttc`) and SVG fonts are refused — a collection holds several +faces and `@font-face` cannot say which, and an SVG font can carry script. + +**Wearing it.** A text object's inspector has a **Font** row between Text and Font size: a +dropdown of the project's font assets, with **None** for the editor's own typeface. That is +also what a reference falls back to when the asset behind it is gone, so a deleted font leaves +readable text rather than nothing. The row works in the scene view, in the game preview and +while playing — all three go through one place (`WorldViewport.syncText`). + +**It arrives late.** A face is fetched over the network, so a text is drawn in the fallback for +the first frames and then reflows into the real one. That is the same "not yet, redraw when it +lands" contract the image library has, and the reason the width of a text can change shortly +after a project opens. + +**The family name is derived, not read.** A face is registered with the browser as +`gm-font-`, never under the name inside the font: two uploads +of "Regular" cannot then fight over one family, and the same bytes always resolve to the same +face. The name *inside* the file is not read at all — the asset's name comes from the uploaded +file name and is yours to change. + +**Known limits.** The selection box and the click area of a text are still estimated from the +character count (`bounds.ts`, `CHAR_WIDTH`), not measured, so a very wide or very narrow face +sits inside a box that does not quite fit it. Only text objects wear fonts; a sprite has no use +for one. There is no bold/italic pair: a font asset is one file, so a bold face is a second +asset. diff --git a/_Sidebar.md b/_Sidebar.md index 36e85de..ab12454 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -5,6 +5,7 @@ - [Behaviors](Behaviors) - [Colliders](Colliders) - [Textures](Textures) +- [Fonts](Fonts) - [Animations](Animations) - [State machines](State-machines) - [Shaders](Shaders)