Erzeugte Bilder im Bildpfad (Grundlage für Vector Art und Pixel Editor) #185

Closed
opened 2026-09-20 19:46:13 +02:00 by meleki · 2 comments
Owner

Vorgezogen aus #129 (Frage 12, Antwort: „gemeinsame Grundlage vorziehen"). Diese Stelle brauchen #129 (Vector Art) und #130 (Pixel Editor) gleichermaßen — sie soll genau einmal gebaut werden.

Befund

Ein Bild ist heute immer eine Datei auf dem Server:

  • TextureRef.file ist ein Dateiname, isTextureFile() prüft Hash + Endung (src/core/model/texture.ts).
  • TextureLibrary.load() holt diesen Namen per HTTP.
  • resolvePicture() (src/core/model/texture.ts:86) ist der eine Weg, über den Sprite, Tile, Tilemap, Partikelquelle und der Bild-Picker an ein Bild kommen.

Ein erzeugtes Bild — gerastert aus Vector Art, gemalt im Pixel Editor — hat keine Datei. Ohne diese Grundlage müsste jeder Erzeuger seinen eigenen Sonderweg durch den Bildpfad bauen.

Zu bauen (Vorschlag)

  • file bleibt der Schlüssel, darf aber auch ein erzeugter sein, Form gen:<assetId>@<rev>. TextureLibrary.load() fragt für so einen Schlüssel ein Register von Erzeugern statt den Server. Zuschnitt, Filter-Modus, Shader, Tilemap-Mesh und Partikel sehen weiterhin eine Texture und bleiben unverändert.
  • Neu gezeichnet wird über einen Revisionszähler am Asset: neue Revision → neuer Schlüssel → die Library rastert neu und gibt den alten Eintrag frei. Kein „ist das Bild noch gültig"-Rätselraten an zwanzig Stellen.
  • resolvePicture() liefert für ein erzeugendes Asset einen Ref mit der Leinwandgröße als sourceWidth/Height, damit properties/aspect.ts ohne Sonderfall die Sprite-Größe bestimmt.
  • Bild-Picker und Nutzungssuche (usages.ts) nehmen den neuen Asset-Typ mit.
  • Der Vertrag („was ein Erzeuger liefern muss") kommt ins Wiki (Architecture, Textures).

Tests

  • Unit: Schlüssel bauen und zerlegen, Revision erhöht → neuer Schlüssel, alter Eintrag freigegeben, resolvePicture() auf ein erzeugendes Asset, Erzeuger-Register ohne GPU (Fake-Erzeuger).
  • e2e folgt mit dem ersten echten Erzeuger (#129) — allein ist die Grundlage im Browser nicht sichtbar.

Offen

  • Ob eine erzeugte Textur zusätzlich zwischengespeichert wird (IndexedDB o. ä.), damit sie nach einem Neuladen nicht neu gerastert werden muss. Vorschlag: nein, erst wenn es messbar stört.
Vorgezogen aus #129 (Frage 12, Antwort: „gemeinsame Grundlage vorziehen"). Diese Stelle brauchen #129 (Vector Art) und #130 (Pixel Editor) gleichermaßen — sie soll genau einmal gebaut werden. ## Befund Ein Bild ist heute immer eine **Datei auf dem Server**: - `TextureRef.file` ist ein Dateiname, `isTextureFile()` prüft Hash + Endung (`src/core/model/texture.ts`). - `TextureLibrary.load()` holt diesen Namen per HTTP. - `resolvePicture()` (`src/core/model/texture.ts:86`) ist der eine Weg, über den Sprite, Tile, Tilemap, Partikelquelle und der Bild-Picker an ein Bild kommen. Ein **erzeugtes** Bild — gerastert aus Vector Art, gemalt im Pixel Editor — hat keine Datei. Ohne diese Grundlage müsste jeder Erzeuger seinen eigenen Sonderweg durch den Bildpfad bauen. ## Zu bauen (Vorschlag) - **`file` bleibt der Schlüssel**, darf aber auch ein erzeugter sein, Form `gen:<assetId>@<rev>`. `TextureLibrary.load()` fragt für so einen Schlüssel ein **Register von Erzeugern** statt den Server. Zuschnitt, Filter-Modus, Shader, Tilemap-Mesh und Partikel sehen weiterhin eine `Texture` und bleiben **unverändert**. - **Neu gezeichnet wird über einen Revisionszähler** am Asset: neue Revision → neuer Schlüssel → die Library rastert neu und gibt den alten Eintrag frei. Kein „ist das Bild noch gültig"-Rätselraten an zwanzig Stellen. - `resolvePicture()` liefert für ein erzeugendes Asset einen Ref mit der **Leinwandgröße** als `sourceWidth/Height`, damit `properties/aspect.ts` ohne Sonderfall die Sprite-Größe bestimmt. - **Bild-Picker** und **Nutzungssuche** (`usages.ts`) nehmen den neuen Asset-Typ mit. - Der Vertrag („was ein Erzeuger liefern muss") kommt ins Wiki (Architecture, Textures). ## Tests - Unit: Schlüssel bauen und zerlegen, Revision erhöht → neuer Schlüssel, alter Eintrag freigegeben, `resolvePicture()` auf ein erzeugendes Asset, Erzeuger-Register ohne GPU (Fake-Erzeuger). - e2e folgt mit dem ersten echten Erzeuger (#129) — allein ist die Grundlage im Browser nicht sichtbar. ## Offen - Ob eine erzeugte Textur zusätzlich **zwischengespeichert** wird (IndexedDB o. ä.), damit sie nach einem Neuladen nicht neu gerastert werden muss. Vorschlag: nein, erst wenn es messbar stört.
meleki added the new label 2026-09-20 19:46:13 +02:00
admin added this to the Development project 2026-09-20 19:48:04 +02:00
admin moved this to Next in Development on 2026-09-20 19:48:20 +02:00

richtig, zwischenspeichern kann man weglassen

richtig, zwischenspeichern kann man weglassen
admin removed the new label 2026-09-20 19:49:56 +02:00
meleki referenced this issue from a commit 2026-09-20 20:52:02 +02:00
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)
meleki referenced this issue from a commit 2026-09-20 20:52:02 +02:00
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)
meleki referenced this issue from a commit 2026-09-20 20:52:02 +02:00
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)
Author
Owner

Gebaut und gepusht: cb1804d (10 Dateien, 6 geändert, 4 neu), Wiki dfecee8. Der Erzeuger, der die Grundlage sichtbar macht, kam mit #129 Etappe 2 in 6371576 — damit ist sie auch im Browser belegt.

Was umgesetzt ist

Der Schlüssel steht, wo der Dateiname steht. TextureRef.file darf jetzt auch gen:<assetId>@<revision> sein — eine hochgeladene Datei ist <sha256>.<ext> und kann nie so aussehen. Alles, was einen Ref nur weiterreicht (Sprite, Tile, Tilemap-Mesh, Partikelquelle, Shader, Seitenverhältnis), ist unverändert, weil es weiter eine Texture sieht.

Eine Abweichung vom Plan im Issue: die Revision ist ein String, kein Zähler. gen:a1@7 und gen:a1@c0ffee gehen durch dieselbe Tür. Das kostet nichts und erlaubt einem Erzeuger, seinen eigenen Inhalt zu fingerprinten, statt sich darauf zu verlassen, dass jedes Command das Hochzählen nicht vergisst. #129 nutzt genau das.

Zwei Register, beide nach Asset-Typ als blankem String, damit ein Plugin einen eigenen Typ mitbringen darf:

  • registerPictureSource(type, read) in core/model/generated.ts sagt dem Modell Leinwandgröße und Revision; resolvePicture() baut daraus den Ref — ohne Rechteck, ohne Slice, smooth aus props.filter wie bei einer Textur.
  • registerPictureGenerator(type, draw) in viewport/generatedPictures.ts liefert die fertige Textur; projectPictureFactory(() => state.project) verbindet beides pro Sitzung in App.ts.

Die Invalidierung ist eine Regel statt Rätselraten an zwanzig Aufrufstellen. GeneratedPictures<T> hält ein Bild pro Asset, gekeyt auf die Revision, und gibt das alte frei, sobald das neue gezeichnet ist; die TextureLibrary zerstört es und wirft die davon geschnittenen Crops mit weg. Ein Erzeuger, der noch nichts liefern kann, wird nicht gemerkt, sondern nächstes Mal wieder gefragt. Die Klasse importiert Pixi nur als Typ — die ganze Buchführung ist ohne GPU messbar.

Mitgezogen: der Bild-Picker (ein gezeichnetes Asset steht nach Namen zwischen den Texturen, ohne Frames), die Lösch-Warnung (zählt Tiles und Animationen auch dafür) und das Asset-Kontextmenü, wo Create sprite from picture es jetzt abdeckt, während Create animation from texture texture-only bleibt, weil es Frames braucht.

Ein Fund beim Sondieren: crop() hätte eine erzeugte Textur gar nicht schneiden können — eine RenderTexture hat kein source.resource zum Zurückzeichnen, drawImage wäre auf null gelaufen. Sie wird jetzt ganz herausgegeben; ein gezeichnetes Bild hat ohnehin kein Sheet, dessen Nachbarzeile durchscheinen könnte.

Tests

./check.sh                      → 124 Files, 1817 Tests, CHECK EXIT 0   (vorher 122 / 1799)
npm run build                   → BUILD EXIT 0
GM_STEPS=1-15,25,28,31,37,39,40 → ALL PASSED – 654 checks
GM_STEPS=1-13,39 (nach der Menü-Änderung, neu gebaut) → ALL PASSED – 429 checks

18 neue Unit-Tests: 10 in tests/core/generated-picture.test.ts (Schlüssel bauen und zerlegen, eine hochgeladene Datei ist keiner, kein Register → kein Bild, Auflösung auf Leinwandgröße, neue Revision = neuer Schlüssel, Leinwand ohne Größe, Filter, Sprite-Maß, Picker-Reihenfolge) und 8 in tests/viewport/generated-pictures.test.ts (einmal zeichnen, alte Revision freigeben und nur die des eigenen Assets, null wird nicht gemerkt, kein Schlüssel = nichts gezeichnet, Sitzungswechsel gibt alles zurück, peek).

Drei Gegenproben, alle greifen: erzeugte Assets aus resolvePicture nehmen → 5 der 10 Kern-Tests fallen; die alte Revision nicht freigeben → 1 Viewport-Test fällt; den Picker wieder auf Texturen einengen → 3 Tests fallen. Originale per cmp zurückgeprüft.

Keine neuen e2e-Checks in diesem Commit, Check-Summe blieb bei 1157 — ohne Erzeuger ist die Grundlage im Browser nicht sichtbar. Der e2e-Beleg kam wie angekündigt mit #129 (Modul 47).

Bewusst nicht gebaut

Zwischenspeichern (IndexedDB) — so stand es als Vorschlag im Issue: erst, wenn es messbar stört. Nach einem Reload wird neu gerastert.

Doku: Wiki Textures (neuer Abschnitt A picture can be drawn instead of uploaded — der Vertrag und die zwei Unterschiede: keine Frames, nichts gespeichert) und Architecture (core/model/generated, viewport/generatedPictures).

Gebaut und gepusht: `cb1804d` (10 Dateien, 6 geändert, 4 neu), Wiki `dfecee8`. Der Erzeuger, der die Grundlage sichtbar macht, kam mit #129 Etappe 2 in `6371576` — damit ist sie auch im Browser belegt. ## Was umgesetzt ist **Der Schlüssel steht, wo der Dateiname steht.** `TextureRef.file` darf jetzt auch `gen:<assetId>@<revision>` sein — eine hochgeladene Datei ist `<sha256>.<ext>` und kann nie so aussehen. Alles, was einen Ref nur weiterreicht (Sprite, Tile, Tilemap-Mesh, Partikelquelle, Shader, Seitenverhältnis), ist unverändert, weil es weiter eine `Texture` sieht. **Eine Abweichung vom Plan im Issue: die Revision ist ein String, kein Zähler.** `gen:a1@7` und `gen:a1@c0ffee` gehen durch dieselbe Tür. Das kostet nichts und erlaubt einem Erzeuger, seinen eigenen Inhalt zu fingerprinten, statt sich darauf zu verlassen, dass jedes Command das Hochzählen nicht vergisst. #129 nutzt genau das. **Zwei Register, beide nach Asset-Typ als blankem String**, damit ein Plugin einen eigenen Typ mitbringen darf: - `registerPictureSource(type, read)` in `core/model/generated.ts` sagt dem Modell Leinwandgröße und Revision; `resolvePicture()` baut daraus den Ref — ohne Rechteck, ohne Slice, `smooth` aus `props.filter` wie bei einer Textur. - `registerPictureGenerator(type, draw)` in `viewport/generatedPictures.ts` liefert die fertige Textur; `projectPictureFactory(() => state.project)` verbindet beides pro Sitzung in `App.ts`. **Die Invalidierung ist eine Regel statt Rätselraten an zwanzig Aufrufstellen.** `GeneratedPictures<T>` hält ein Bild pro Asset, gekeyt auf die Revision, und gibt das alte frei, sobald das neue gezeichnet ist; die `TextureLibrary` zerstört es und wirft die davon geschnittenen Crops mit weg. Ein Erzeuger, der noch nichts liefern kann, wird nicht gemerkt, sondern nächstes Mal wieder gefragt. Die Klasse importiert Pixi nur als Typ — die ganze Buchführung ist ohne GPU messbar. **Mitgezogen:** der Bild-Picker (ein gezeichnetes Asset steht nach Namen zwischen den Texturen, ohne Frames), die Lösch-Warnung (zählt Tiles und Animationen auch dafür) und das Asset-Kontextmenü, wo *Create sprite from picture* es jetzt abdeckt, während *Create animation from texture* texture-only bleibt, weil es Frames braucht. **Ein Fund beim Sondieren:** `crop()` hätte eine erzeugte Textur gar nicht schneiden können — eine RenderTexture hat kein `source.resource` zum Zurückzeichnen, `drawImage` wäre auf `null` gelaufen. Sie wird jetzt ganz herausgegeben; ein gezeichnetes Bild hat ohnehin kein Sheet, dessen Nachbarzeile durchscheinen könnte. ## Tests ``` ./check.sh → 124 Files, 1817 Tests, CHECK EXIT 0 (vorher 122 / 1799) npm run build → BUILD EXIT 0 GM_STEPS=1-15,25,28,31,37,39,40 → ALL PASSED – 654 checks GM_STEPS=1-13,39 (nach der Menü-Änderung, neu gebaut) → ALL PASSED – 429 checks ``` 18 neue Unit-Tests: 10 in `tests/core/generated-picture.test.ts` (Schlüssel bauen und zerlegen, eine hochgeladene Datei ist keiner, kein Register → kein Bild, Auflösung auf Leinwandgröße, neue Revision = neuer Schlüssel, Leinwand ohne Größe, Filter, Sprite-Maß, Picker-Reihenfolge) und 8 in `tests/viewport/generated-pictures.test.ts` (einmal zeichnen, alte Revision freigeben und nur die des eigenen Assets, `null` wird nicht gemerkt, kein Schlüssel = nichts gezeichnet, Sitzungswechsel gibt alles zurück, peek). Drei Gegenproben, alle greifen: erzeugte Assets aus `resolvePicture` nehmen → 5 der 10 Kern-Tests fallen; die alte Revision nicht freigeben → 1 Viewport-Test fällt; den Picker wieder auf Texturen einengen → 3 Tests fallen. Originale per `cmp` zurückgeprüft. Keine neuen e2e-Checks in diesem Commit, Check-Summe blieb bei 1157 — ohne Erzeuger ist die Grundlage im Browser nicht sichtbar. Der e2e-Beleg kam wie angekündigt mit #129 (Modul 47). ## Bewusst nicht gebaut **Zwischenspeichern (IndexedDB)** — so stand es als Vorschlag im Issue: erst, wenn es messbar stört. Nach einem Reload wird neu gerastert. Doku: Wiki *Textures* (neuer Abschnitt *A picture can be drawn instead of uploaded* — der Vertrag und die zwei Unterschiede: keine Frames, nichts gespeichert) und *Architecture* (`core/model/generated`, `viewport/generatedPictures`).
admin moved this to Done in Development on 2026-09-20 21:00:10 +02:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#185