Vector Art mit eigenem editor (tab) - dort soll man Formen mit farbe und kurven erstellen können. Punkte hinzufügen, bezier kurven. das kann man dann bei einem Sprite auswählen können wie Texture oder Texture-Frame
Vector Art mit eigenem editor (tab) - dort soll man Formen mit farbe und kurven erstellen können. Punkte hinzufügen, bezier kurven. das kann man dann bei einem Sprite auswählen können wie Texture oder Texture-Frame
admin
added this to the Development project 2026-09-17 20:56:29 +02:00
admin
moved this to Next in Development on 2026-09-20 19:25:38 +02:00
Ich habe den Code sondiert (Bildpfad, Sprite-Rendering, Asset-Modell, der vorhandene Polygon-Editor) und dabei ein paar Entscheidungen gefunden, die vor dem Bauen fallen müssen. Zu jeder mein Vorschlag — es reicht, wenn du „passt" schreibst oder die Nummern nennst, die du anders willst.
1. Rastern oder als echte Geometrie zeichnen?
Das ist die Grundsatzfrage, alles andere hängt daran. Heute ist jedes Sprite ein Mesh mit Quad-Geometrie, das eine Texture trägt (viewport/symbols.ts), und daran hängen Shader (viewport/shaders.ts), Partikel, Nine-Slice und die Tile-Bilder — alle erwarten eine Textur.
A (mein Vorschlag): Vector Art wird zu einer Textur gerastert. Das Asset wird in eine RenderTexture gezeichnet und wie eine hochgeladene Datei aus der TextureLibrary geholt; neu gerastert wird beim Ändern des Assets. Vorteil: alles Bestehende funktioniert sofort — Shader, Partikel, Tiles, Nine-Slice, Animation, das Seitenverhältnis. Nachteil: beim starken Hineinzoomen ist es ein Bild, keine Kurve mehr (mildern lässt sich das über eine Rasterauflösung, die größer ist als die Sprite-Größe).
B: Eigener Zeichenweg (Mesh aus triangulierten Pfaden). Bei jedem Zoom gestochen scharf, aber Shader, Partikel, Tiles und Nine-Slice fallen erstmal weg bzw. sind je ein eigener Umbau.
2. Wo darf Vector Art überall stehen?
Ein Bild ist heute eine Id — Textur-Asset oder Frame — und alles geht durch resolvePicture(). Wenn Vector Art dort mitläuft, taucht es ohne Mehraufwand überall auf, wo heute ein Bild steht: Sprite, Tile (und damit Tilemaps), Partikelquelle, der Bild-Picker.
Vorschlag: überall, also ein Ausgang statt vier. Willst du es bewusst nur am Sprite haben?
3. Wie groß ist ein Vector-Art-Asset?
Eine Textur hat eine Pixelgröße, daraus nimmt ein neu gesetztes Sprite seine Breite und Höhe (properties/aspect.ts). Kurven haben keine.
Vorschlag: Das Asset hat eine eigene Leinwandgröße (Vorgabe 256 × 256, im Inspector änderbar). Sie ist der Bezug für Größe und Seitenverhältnis, und der Editor zeigt sie als Rahmen. Zusatzfrage: Wird an diesem Rahmen abgeschnitten oder darf eine Form darüber hinausragen? Mein Vorschlag: abschneiden, sonst ist die Größe des Sprites eine Lüge.
4. Was genau ist eine Form?
Vorschlag: ein geschlossener Pfad aus Punkten, wobei jeder Punkt entweder eine Ecke (gerade Kante) oder glatt mit zwei Bezier-Griffen ist (kubisch) — das übliche Pen-Tool-Modell. Dazu je Form: Füllfarbe + Deckkraft, optional Kontur (Farbe, Breite). Offene Pfade (nur Strich, keine Füllung) — willst du die auch?
5. Wie reich darf die Füllung sein?
Vorschlag für Runde 1: einfarbig plus Deckkraft. Verläufe (linear/radial) und eine Füllung mit einer Textur wären je ein eigenes Issue — sagenswert ist mir vor allem, ob du Verläufe als Teil von #129 erwartest.
6. Mehrere Formen, Löcher, Gruppen?
Vorschlag: eine flache Liste von Formen mit Reihenfolge (oben/unten verschieben), keine Gruppen und keine booleschen Operationen (Vereinigen/Abziehen). Löcher bekommt eine Form dadurch, dass sie mehrere Ringe haben darf (Even-Odd) — oder reicht dir fürs Erste, ein Loch durch eine hintergrundfarbene Form darüber zu bauen? Mein Vorschlag: Ringe ja, boolesche Operationen nein.
7. Werkzeuge im Tab
Vorschlag: Zoomen/Schieben wie im Texture-Tab; Stift (neue Form Punkt für Punkt), Auswahl (Punkt ziehen, Griff ziehen, ganze Form ziehen), Punkt auf einer Kante einfügen, Doppelklick entfernt ihn — die Regeln dafür gibt es schon als reine Funktionen in editor/polygonEdit.ts (#141/#151), die würde ich um die Bezier-Griffe erweitern statt etwas Zweites zu bauen. Punkte am Raster einrasten: einschaltbar, standardmäßig aus.
8. Wo werden Farbe und Kontur eingestellt?
Vorschlag: im Inspector rechts, wenn eine Form ausgewählt ist (wie sonst die Eigenschaften eines Objekts), plus eine Formenliste im Tab. Alternative wäre eine Werkzeugleiste im Tab selbst.
9. Kollision
Analog zu #161 („Frame outline" als Collider-Form) könnte ein Collider die Umrisse einer Vector-Art-Form übernehmen.
Vorschlag:nicht in dieser Runde, aber als eigenes Issue notieren — die Formen liegen dann ja schon als Polygone vor.
10. Laufzeit
Vorschlag: keine Nodes zum Ändern von Farben oder Punkten im Lauf, und keine Animation der Punkte auf der Timeline. Beides später, wenn es gebraucht wird. Ein Sprite kann seine Vector Art zur Laufzeit wechseln, weil texture schon eine Eigenschaft ist — das fällt ohne Arbeit ab.
11. SVG-Import?
Vorschlag: nein, eigenes Issue. Ein SVG-Parser ist ein Thema für sich (Transformationen, Gruppen, Verläufe, Text).
Beide Issues bringen ein Bild, das keine hochgeladene Datei ist. Der Bildpfad (resolvePicture, der Picker, die TextureLibrary) muss das genau einmal lernen.
Vorschlag: Ich baue diese Stelle in #129 so, dass #130 nur noch ein zweiter Erzeuger ist, und schreibe den Vertrag ins Wiki. Wenn du #130 zeitnah willst, wäre auch denkbar, die gemeinsame Grundlage vorzuziehen — sag Bescheid, ich richte mich danach.
Offen für mich ist vor allem 1, 3 und 6 — der Rest lässt sich auch später noch drehen.
## Fragen zur Planung
Ich habe den Code sondiert (Bildpfad, Sprite-Rendering, Asset-Modell, der vorhandene Polygon-Editor) und dabei ein paar Entscheidungen gefunden, die vor dem Bauen fallen müssen. Zu jeder mein Vorschlag — es reicht, wenn du „passt" schreibst oder die Nummern nennst, die du anders willst.
### 1. Rastern oder als echte Geometrie zeichnen?
Das ist die Grundsatzfrage, alles andere hängt daran. Heute ist jedes Sprite ein **Mesh mit Quad-Geometrie, das eine `Texture` trägt** (`viewport/symbols.ts`), und daran hängen Shader (`viewport/shaders.ts`), Partikel, Nine-Slice und die Tile-Bilder — alle erwarten eine Textur.
- **A (mein Vorschlag): Vector Art wird zu einer Textur gerastert.** Das Asset wird in eine `RenderTexture` gezeichnet und wie eine hochgeladene Datei aus der `TextureLibrary` geholt; neu gerastert wird beim Ändern des Assets. Vorteil: **alles Bestehende funktioniert sofort** — Shader, Partikel, Tiles, Nine-Slice, Animation, das Seitenverhältnis. Nachteil: beim starken Hineinzoomen ist es ein Bild, keine Kurve mehr (mildern lässt sich das über eine Rasterauflösung, die größer ist als die Sprite-Größe).
- **B: Eigener Zeichenweg (Mesh aus triangulierten Pfaden).** Bei jedem Zoom gestochen scharf, aber Shader, Partikel, Tiles und Nine-Slice fallen erstmal weg bzw. sind je ein eigener Umbau.
### 2. Wo darf Vector Art überall stehen?
Ein Bild ist heute **eine Id** — Textur-Asset oder Frame — und alles geht durch `resolvePicture()`. Wenn Vector Art dort mitläuft, taucht es ohne Mehraufwand überall auf, wo heute ein Bild steht: Sprite, **Tile** (und damit Tilemaps), **Partikelquelle**, der Bild-Picker.
**Vorschlag:** überall, also ein Ausgang statt vier. Willst du es bewusst nur am Sprite haben?
### 3. Wie groß *ist* ein Vector-Art-Asset?
Eine Textur hat eine Pixelgröße, daraus nimmt ein neu gesetztes Sprite seine Breite und Höhe (`properties/aspect.ts`). Kurven haben keine.
**Vorschlag:** Das Asset hat eine eigene **Leinwandgröße** (Vorgabe 256 × 256, im Inspector änderbar). Sie ist der Bezug für Größe und Seitenverhältnis, und der Editor zeigt sie als Rahmen. **Zusatzfrage:** Wird an diesem Rahmen **abgeschnitten** oder darf eine Form darüber hinausragen? Mein Vorschlag: abschneiden, sonst ist die Größe des Sprites eine Lüge.
### 4. Was genau ist eine Form?
**Vorschlag:** ein **geschlossener Pfad** aus Punkten, wobei jeder Punkt entweder eine **Ecke** (gerade Kante) oder **glatt** mit zwei Bezier-Griffen ist (kubisch) — das übliche Pen-Tool-Modell. Dazu je Form: **Füllfarbe + Deckkraft**, optional **Kontur** (Farbe, Breite). Offene Pfade (nur Strich, keine Füllung) — willst du die auch?
### 5. Wie reich darf die Füllung sein?
**Vorschlag für Runde 1:** einfarbig plus Deckkraft. **Verläufe** (linear/radial) und eine Füllung mit einer Textur wären je ein eigenes Issue — sagenswert ist mir vor allem, ob du Verläufe als Teil von #129 erwartest.
### 6. Mehrere Formen, Löcher, Gruppen?
**Vorschlag:** eine flache Liste von Formen mit Reihenfolge (oben/unten verschieben), **keine Gruppen und keine booleschen Operationen** (Vereinigen/Abziehen). **Löcher** bekommt eine Form dadurch, dass sie mehrere Ringe haben darf (Even-Odd) — oder reicht dir fürs Erste, ein Loch durch eine hintergrundfarbene Form darüber zu bauen? Mein Vorschlag: Ringe ja, boolesche Operationen nein.
### 7. Werkzeuge im Tab
**Vorschlag:** Zoomen/Schieben wie im Texture-Tab; **Stift** (neue Form Punkt für Punkt), **Auswahl** (Punkt ziehen, Griff ziehen, ganze Form ziehen), Punkt auf einer Kante einfügen, Doppelklick entfernt ihn — die Regeln dafür gibt es schon als reine Funktionen in `editor/polygonEdit.ts` (#141/#151), die würde ich um die Bezier-Griffe erweitern statt etwas Zweites zu bauen. Punkte am Raster einrasten: einschaltbar, standardmäßig aus.
### 8. Wo werden Farbe und Kontur eingestellt?
**Vorschlag:** im **Inspector** rechts, wenn eine Form ausgewählt ist (wie sonst die Eigenschaften eines Objekts), plus eine Formenliste im Tab. Alternative wäre eine Werkzeugleiste im Tab selbst.
### 9. Kollision
Analog zu #161 („Frame outline" als Collider-Form) könnte ein Collider die Umrisse einer Vector-Art-Form übernehmen.
**Vorschlag:** **nicht** in dieser Runde, aber als eigenes Issue notieren — die Formen liegen dann ja schon als Polygone vor.
### 10. Laufzeit
**Vorschlag:** keine Nodes zum Ändern von Farben oder Punkten im Lauf, und keine Animation der Punkte auf der Timeline. Beides später, wenn es gebraucht wird. Ein Sprite kann seine Vector Art zur Laufzeit wechseln, weil `texture` schon eine Eigenschaft ist — das fällt ohne Arbeit ab.
### 11. SVG-Import?
**Vorschlag:** nein, eigenes Issue. Ein SVG-Parser ist ein Thema für sich (Transformationen, Gruppen, Verläufe, Text).
### 12. Verhältnis zu #130 (Pixel Editor)
Beide Issues bringen ein **Bild, das keine hochgeladene Datei ist**. Der Bildpfad (`resolvePicture`, der Picker, die `TextureLibrary`) muss das genau einmal lernen.
**Vorschlag:** Ich baue diese Stelle in #129 so, dass #130 nur noch ein zweiter Erzeuger ist, und schreibe den Vertrag ins Wiki. Wenn du #130 zeitnah willst, wäre auch denkbar, die gemeinsame Grundlage vorzuziehen — sag Bescheid, ich richte mich danach.
---
**Offen für mich ist vor allem 1, 3 und 6** — der Rest lässt sich auch später noch drehen.
definitiv rastern, muss behandelt werden wie eine texture
passt, überall
passt, auch das mit dem abschneiden
passt, alles wenn geht
vorschlag passt, erstmal nur solide füllung, verlauf ist folge issue
passt, aber auch boolsche operationen als folge issue
passt, poly bearbeitung wiederverwenden und erweitern mit bezier handles
passt, inspector - später vielleicht eigenes panel wie bei tilemap
passt, eigenes issue
dein vorschlag, laufzeit brauchen wir vorerst garnicht
passt, eigens issue
passt, gemeinsame grundlage vorziehen
weiters: reihenfolge von formen muss man auch festlegen können (eins nach vorne bringen, ganz nach vorne bringen, und auch eins nach hinten bzw. ganz nach hinten) - vielleicht wäre da eine hierarchie im inspektor nicht schlecht, mit vorschau vom shape. weiteres folge issue wären dann sogar gruppen bzw. layers
1. definitiv rastern, muss behandelt werden wie eine texture
2. passt, überall
3. passt, auch das mit dem abschneiden
4. passt, alles wenn geht
5. vorschlag passt, erstmal nur solide füllung, verlauf ist folge issue
6. passt, aber auch boolsche operationen als folge issue
7. passt, poly bearbeitung wiederverwenden und erweitern mit bezier handles
8. passt, inspector - später vielleicht eigenes panel wie bei tilemap
9. passt, eigenes issue
10. dein vorschlag, laufzeit brauchen wir vorerst garnicht
11. passt, eigens issue
12. passt, gemeinsame grundlage vorziehen
weiters: reihenfolge von formen muss man auch festlegen können (eins nach vorne bringen, ganz nach vorne bringen, und auch eins nach hinten bzw. ganz nach hinten) - vielleicht wäre da eine hierarchie im inspektor nicht schlecht, mit vorschau vom shape. weiteres folge issue wären dann sogar gruppen bzw. layers
Aus deinen Antworten (Kommentar oben) folgt der Zuschnitt. Gebaut ist noch nichts — das hier ist der Stand zum Durchsehen.
Die tragende Entscheidung
Rastern (1). Damit wird Vector Art behandelt wie eine Textur, läuft durch resolvePicture() und ist ohne Mehrarbeit überall (2) — Sprite, Tile, Tilemap, Partikelquelle, Picker. Shader, Nine-Slice, Animation und das Seitenverhältnis funktionieren unverändert, weil sie weiterhin eine Texture sehen.
Die einzige Stelle, die ich im Plan offen gelassen hatte, ist die Überabtastung: gerastert wird in 2× der Leinwandgröße, damit starkes Hineinzoomen nicht sofort matscht. Das ist so festgelegt, solange du nichts anderes sagst.
Zu 12 („gemeinsame Grundlage vorziehen"). Heute ist TextureRef.file ein Dateiname auf dem Server (isTextureFile() prüft Hash + Endung, src/core/model/texture.ts:98), und TextureLibrary lädt ihn per HTTP. Ein erzeugtes Bild hat keine Datei.
#185 lehrt dem Bildpfad genau einmal, dass ein Bild auch erzeugt sein darf: Schlüssel gen:<assetId>@<rev>, ein Register von Erzeugern statt des Servers, Neuzeichnen über einen Revisionszähler, Leinwandgröße als sourceWidth/Height, Picker und Nutzungssuche mit. #130 (Pixel Editor) ist danach nur noch ein zweiter Erzeuger. Das Issue wird eigens gebaut, committet und geschlossen, bevor #129 anfängt.
Modell — neuer Asset-Typ vectorart in ASSET_TYPES (nicht platzierbar): Leinwand w/h, Liste von Formen. Eine Form = ein oder mehrere Ringe (Even-Odd, also Löcher — 6), je Ring Punkte mit optionalen Bezier-Griffen in/out, closed ja/nein (offener Pfad = nur Kontur — 4), dazu Füllfarbe + Deckkraft und optional Kontur (Farbe, Breite — 5). Serialisierung, Validierung, Undo-Commands.
Rasterung — reine Funktion Pfad → Zeichenbefehle (unit-testbar, ohne GPU), gezeichnet in eine RenderTexture in 2× Leinwandgröße, an der Leinwand abgeschnitten (3).
Tab — Zoom/Pan wie im Texture-Tab; Stift (neue Form Punkt für Punkt), Auswahl (Punkt, Griff, ganze Form ziehen), Punkt auf einer Kante einfügen, Doppelklick entfernt ihn. editor/polygonEdit.ts wird um die Bezier-Griffe erweitert statt etwas Zweites zu bauen (7) — dieselben Regeln gelten dann auch für Collider-Polygone und Frame-Umrisse. Raster-Einrasten schaltbar, Vorgabe aus.
Inspector (8) — Formenliste mit Vorschau und der Reihenfolge aus deinem Nachtrag: eins nach vorne, ganz nach vorne, eins nach hinten, ganz nach hinten, dazu Ziehen in der Liste. Darunter Farbe, Deckkraft, Kontur; am Asset die Leinwandgröße.
Tests und Doku — Unit-Tests für Modell, Rasterbefehle und die erweiterten Punkt-Regeln; ein e2e-Modul (Form zeichnen, Farbe setzen, auf ein Sprite legen, Reihenfolge ändern); Wiki-Seite Vector Art plus Nachträge in Architecture, Textures, Tilemaps.
Laufzeit-Nodes lege ich nicht an (10, „brauchen wir vorerst gar nicht"). Dass ein Sprite seine Vector Art im Lauf wechseln kann, fällt über die Eigenschaft texture ohnehin ab.
Zwei Anmerkungen
Das ist groß.#185 wird eigens gebaut und geschlossen; #129 danach in einem Zug, weil ein halbes Asset (Modell ohne Tab) nichts nützt.
## Plan
Aus deinen Antworten (Kommentar oben) folgt der Zuschnitt. Gebaut ist noch nichts — das hier ist der Stand zum Durchsehen.
### Die tragende Entscheidung
**Rastern** (1). Damit wird Vector Art behandelt wie eine Textur, läuft durch `resolvePicture()` und ist ohne Mehrarbeit **überall** (2) — Sprite, Tile, Tilemap, Partikelquelle, Picker. Shader, Nine-Slice, Animation und das Seitenverhältnis funktionieren unverändert, weil sie weiterhin eine `Texture` sehen.
Die einzige Stelle, die ich im Plan offen gelassen hatte, ist die **Überabtastung**: gerastert wird in **2× der Leinwandgröße**, damit starkes Hineinzoomen nicht sofort matscht. Das ist so festgelegt, solange du nichts anderes sagst.
### Vorgezogen: #185 — Erzeugte Bilder im Bildpfad
Zu 12 („gemeinsame Grundlage vorziehen"). Heute ist `TextureRef.file` ein Datei**name** auf dem Server (`isTextureFile()` prüft Hash + Endung, `src/core/model/texture.ts:98`), und `TextureLibrary` lädt ihn per HTTP. Ein erzeugtes Bild hat keine Datei.
**#185** lehrt dem Bildpfad genau einmal, dass ein Bild auch erzeugt sein darf: Schlüssel `gen:<assetId>@<rev>`, ein Register von Erzeugern statt des Servers, Neuzeichnen über einen Revisionszähler, Leinwandgröße als `sourceWidth/Height`, Picker und Nutzungssuche mit. #130 (Pixel Editor) ist danach nur noch ein zweiter Erzeuger. Das Issue wird eigens gebaut, committet und geschlossen, bevor #129 anfängt.
### #129 selbst, in fünf Etappen
1. **Modell** — neuer Asset-Typ `vectorart` in `ASSET_TYPES` (nicht platzierbar): Leinwand w/h, Liste von Formen. Eine Form = ein oder mehrere **Ringe** (Even-Odd, also Löcher — 6), je Ring Punkte mit optionalen Bezier-Griffen `in`/`out`, `closed` ja/nein (offener Pfad = nur Kontur — 4), dazu Füllfarbe + Deckkraft und optional Kontur (Farbe, Breite — 5). Serialisierung, Validierung, Undo-Commands.
2. **Rasterung** — reine Funktion Pfad → Zeichenbefehle (unit-testbar, ohne GPU), gezeichnet in eine `RenderTexture` in 2× Leinwandgröße, **an der Leinwand abgeschnitten** (3).
3. **Tab** — Zoom/Pan wie im Texture-Tab; **Stift** (neue Form Punkt für Punkt), **Auswahl** (Punkt, Griff, ganze Form ziehen), Punkt auf einer Kante einfügen, Doppelklick entfernt ihn. `editor/polygonEdit.ts` wird um die Bezier-Griffe **erweitert** statt etwas Zweites zu bauen (7) — dieselben Regeln gelten dann auch für Collider-Polygone und Frame-Umrisse. Raster-Einrasten schaltbar, Vorgabe aus.
4. **Inspector** (8) — **Formenliste mit Vorschau** und der Reihenfolge aus deinem Nachtrag: eins nach vorne, ganz nach vorne, eins nach hinten, ganz nach hinten, dazu Ziehen in der Liste. Darunter Farbe, Deckkraft, Kontur; am Asset die Leinwandgröße.
5. **Tests und Doku** — Unit-Tests für Modell, Rasterbefehle und die erweiterten Punkt-Regeln; ein e2e-Modul (Form zeichnen, Farbe setzen, auf ein Sprite legen, Reihenfolge ändern); Wiki-Seite *Vector Art* plus Nachträge in Architecture, Textures, Tilemaps.
### Folge-Issues (angelegt)
| Nr. | Thema | aus Frage |
|---|---|---|
| #186 | Verläufe und Texturfüllung | 5 |
| #187 | boolesche Operationen | 6 |
| #188 | Collider-Form „Vector art outline" | 9 |
| #189 | SVG-Import | 11 |
| #190 | Gruppen und Layers | Nachtrag |
| #191 | eigenes Panel statt Inspector | 8 |
**Laufzeit-Nodes lege ich nicht an** (10, „brauchen wir vorerst gar nicht"). Dass ein Sprite seine Vector Art im Lauf wechseln kann, fällt über die Eigenschaft `texture` ohnehin ab.
### Zwei Anmerkungen
- **Das ist groß.** #185 wird eigens gebaut und geschlossen; #129 danach in einem Zug, weil ein halbes Asset (Modell ohne Tab) nichts nützt.
- **Reihenfolge:** #185 → #129 → der Rest nach Bedarf.
Etappen 1 und 2 sind gebaut und gepusht. Die Grundlage #185 ist vorher geschlossen. Das Issue bleibt offen — es fehlen Etappe 3 (Tab), 4 (Inspector) und 5 (Tests/Doku zum Zeichnen).
Etappe 1 — das Modell (bdb3f06)
Der Asset-Typ vectorart existiert, nicht platzierbar, im +-Menü und im Asset-Panel mit eigenem Symbol (eine Kurve zwischen zwei Punkten). Einen Tab hat er noch nicht — er verhält sich wie eine Font: auswählbar, Inspector zeigt seine Zeilen, Doppelklick führt nirgendwohin. Ein halbes Asset wäre nur im Weg, der Tab ist Etappe 3.
Das Datenmodell in Asset.ts: eine Form = mehrere Ringe (Even-Odd, also Löcher — Antwort 6), je Ring Punkte mit optionalen Bezier-Griffen in/out als Offset vom Punkt und closed ja/nein (offen = nur Kontur — Antwort 4), dazu Füllfarbe, Deckkraft und optional Kontur (Antwort 5). Die beiden Griffe liegen getrennt statt gespiegelt: was „glatt" heißt, ist Sache des Editors, nicht des Modells. Leinwandbreite, -höhe und Filter sind Properties (Inspector-Zeile und Undo wie jede andere), die Formen sind intrinsische Daten wie die Frames einer Textur.
Die Revision, die an #185 geht, ist ein Fingerabdruck des Gezeichneten, kein Zähler — genau deshalb nahm der Schlüssel dort einen String-Token: kein Command kann vergessen, sie hochzuzählen. Der Name einer Form steckt bewusst nicht drin, Umbenennen ändert kein Pixel.
Sechs Undo-Commands in vectorArtCommands.ts (Form hinzufügen, entfernen — sie kommt an ihre alte Stelle zurück, und anders als eine Tilemap-Ebene darf auch die letzte gehen —, umbenennen, verschieben, Ringe schreiben als ein Schritt pro Geste mit kopierter Liste, Stil setzen, wobei stroke: null die Kontur wegnimmt) plus die reine Funktion shapeOrderTarget für die vier Knöpfe der geplanten Inspector-Liste; sie bedient auch das Ziehen in dieser Liste.
Das Laden aus einer Projektdatei ist streng: ein Punkt ohne Zahlen fällt weg, ein kaputter Griff lässt einen Eckpunkt übrig, doppelte Form-Ids fallen weg wie doppelte Layer-Ids, eine Kontur der Breite 0 ist keine Kontur — während ein Ring mit nur einem Punkt, den gerade jemand zeichnet, stehenbleibt.
Drei Funde unterwegs:
JSON.stringify als Fingerabdruck war falsch: dieselbe Zeichnung kam aus der Projektdatei als {points, closed} zurück und wurde als {closed, points} gebaut — ein Reload hätte dasselbe Bild unter neuem Schlüssel neu gerastert. Der Fingerabdruck schreibt seine Felder jetzt in fester Reihenfolge, ein Test misst genau diesen Fall.
toNumber in serialize.ts macht aus jedem Unsinn eine 0, die Number.isFinite-Prüfungen liefen deshalb ins Leere: {x: 'far'} wurde zur linken Kante statt zu verschwinden. Formen lesen Zahlen streng über asFinite, weil 0 hier ein Ort auf der Leinwand ist.
„Vector art" wäre zugleich Menüeintrag und Panel-Überschrift gewesen — ein bestehender Test fängt das ab, weil ein Klick nach Text die Überschrift träfe. Die Gruppe heißt jetzt „Vector drawings", der Menüeintrag bleibt „Vector art".
20 neue Unit-Tests in tests/core/vectorart.test.ts; drei Gegenproben greifen (Fingerabdruck zurück auf JSON.stringify → 2 fallen, Zahlen wieder lose lesen → 4, Ringliste nicht kopieren → 1). Kein e2e-Check, Check-Summe blieb 1157, nur Modul 04 zählt 19 Asset-Typen statt 18.
Etappe 2 — die Rasterung (6371576)
Vector Art ist jetzt ein Bild. Das Asset steht im Bild-Picker, ein Sprite trägt es in seiner Texture-Zeile, und was der Sprite zeigt, hat der Rasterizer gemalt. Registriert wird per Funktion statt per Modul-Seiteneffekt: installVectorArtPictures(app.renderer) hängt Source (core) und Generator (viewport) einmal pro Seite in App.ts ein — ein Test darf sie also weglassen.
Die Regel liegt in core, gemalt wird in viewport.core/model/vectorPath.ts liefert pro Form reine Zeichenbefehle (move/line/curve/close): geschlossene Ringe füllen, alle Ringe konturieren, ein Segment bleibt eine Linie, solange keines seiner Enden einen Griff trägt — ein einzelner Griff genügt für eine Kurve, der fehlende Kontrollpunkt sitzt dann auf seinem eigenen Ende, damit eine Ecke neben einer runden Seite stehen kann. Der Rückweg zum ersten Punkt wird nur gezeichnet, wenn er gebogen ist; sonst macht close ihn.
Löcher gehen nur über einen Weg: Pixi erkennt sie ausschließlich an einem GraphicsPath mit checkForHoles — und prüft dabei Enthaltensein, nicht Even-Odd-Tiefe. Für Ring-in-Ring reicht das; ohne das Flag malt der innere Ring einfach über den äußeren (gemessen, siehe Gegenproben).
2× Überabtastung wie im Plan:RenderTexture.create({resolution: 2}) legt 512 px für eine 256er Leinwand auf die GPU, texture.width bleibt die Leinwandgröße — das ist das Maß, an dem der Sprite hängt. Geclippt wird durch die Rendertextur selbst: was über die Leinwand ragt, ist nicht drin, es braucht keine Maske. Gezeichnete Bilder bekommen denselben Sampler-Zustand wie hochgeladene (setPictureFactory, spiegelbildlich zu keep()).
Ein Punkt zum Mitlesen:renderer.render() läuft innerhalb der Bildanfrage. Das ist hier sicher, weil jeder Aufrufer während seines eigenen Widget-Redraws fragt und der Frame danach auf dem Ticker gezeichnet wird; ein verschachteltes render mitten in einem laufenden Renderpass wäre es nicht. Die Warnung steht im Modulkopf.
Neues Modul 47 mit 8 eigenen Checks in eigener Lane: das Bild liegt in doppelter Auflösung auf der GPU und misst die Leinwand; der Sprite zeigt die Füllung; der innere Ring ist ein Loch (die Szene scheint durch); die später gezeichnete Form liegt oben; die Kontur läuft am Ring; eine geänderte Zeichnung bekommt einen neuen Schlüssel und die alte wird freigegeben; der Sprite zeigt die neue Farbe. Die Formen legt der Test per mutate ins Modell — der Tab, der sie zeichnet, ist Etappe 3.
21 neue Unit-Tests in tests/core/vector-path.test.ts. Drei Gegenproben greifen: Überabtastung auf 1 → der GPU-Check fällt (pixelWidth: 256); checkForHoles aus → der Loch-Check fällt (#2e8b57 statt Hintergrund); den gebogenen Rückweg nicht zeichnen → ein Unit-Test fällt. Die volle Suite lief lokal statt nur in CI, weil die Änderung in der TextureLibrary jedes Bild berührt. Check-Summe 1165, gemessen.
Zwei Mess-Fallen, die Läufe gekostet haben:__gm.objectRect(id) gibt die Mitte des Objekts, nicht die Ecke — der erste Durchgang las vier Pixel neben dem Sprite und meldete Hintergrund, während der Rasterizer korrekt war; entschieden hat das der Screenshot, nicht die Zahlen. Und die Gitterlinien der Szene kreuzen genau im Mittelpunkt, der Loch-Check las #363940 und fiel aus dem richtigen Grund falsch; er misst jetzt daneben.
Offen
Etappe 3 (Tab: Stift, Auswahl, Griffe, polygonEdit.ts erweitern), Etappe 4 (Inspector: Formenliste mit Vorschau und Reihenfolge, Farbe/Deckkraft/Kontur), Etappe 5 (e2e zum Zeichnen, Wiki-Seite Vector Art).
**Etappen 1 und 2 sind gebaut und gepusht.** Die Grundlage #185 ist vorher geschlossen. Das Issue bleibt offen — es fehlen Etappe 3 (Tab), 4 (Inspector) und 5 (Tests/Doku zum Zeichnen).
## Etappe 1 — das Modell (`bdb3f06`)
Der Asset-Typ `vectorart` existiert, nicht platzierbar, im `+`-Menü und im Asset-Panel mit eigenem Symbol (eine Kurve zwischen zwei Punkten). Einen Tab hat er noch nicht — er verhält sich wie eine Font: auswählbar, Inspector zeigt seine Zeilen, Doppelklick führt nirgendwohin. Ein halbes Asset wäre nur im Weg, der Tab ist Etappe 3.
**Das Datenmodell** in `Asset.ts`: eine Form = mehrere **Ringe** (Even-Odd, also Löcher — Antwort 6), je Ring Punkte mit optionalen Bezier-Griffen `in`/`out` als Offset vom Punkt und `closed` ja/nein (offen = nur Kontur — Antwort 4), dazu Füllfarbe, Deckkraft und optional Kontur (Antwort 5). Die beiden Griffe liegen getrennt statt gespiegelt: was „glatt" heißt, ist Sache des Editors, nicht des Modells. Leinwandbreite, -höhe und Filter sind **Properties** (Inspector-Zeile und Undo wie jede andere), die Formen sind intrinsische Daten wie die Frames einer Textur.
**Die Revision, die an #185 geht, ist ein Fingerabdruck des Gezeichneten, kein Zähler** — genau deshalb nahm der Schlüssel dort einen String-Token: kein Command kann vergessen, sie hochzuzählen. Der Name einer Form steckt bewusst nicht drin, Umbenennen ändert kein Pixel.
**Sechs Undo-Commands** in `vectorArtCommands.ts` (Form hinzufügen, entfernen — sie kommt an ihre alte Stelle zurück, und anders als eine Tilemap-Ebene darf auch die letzte gehen —, umbenennen, verschieben, Ringe schreiben als ein Schritt pro Geste mit kopierter Liste, Stil setzen, wobei `stroke: null` die Kontur wegnimmt) plus die reine Funktion `shapeOrderTarget` für die vier Knöpfe der geplanten Inspector-Liste; sie bedient auch das Ziehen in dieser Liste.
**Das Laden aus einer Projektdatei ist streng:** ein Punkt ohne Zahlen fällt weg, ein kaputter Griff lässt einen Eckpunkt übrig, doppelte Form-Ids fallen weg wie doppelte Layer-Ids, eine Kontur der Breite 0 ist keine Kontur — während ein Ring mit nur einem Punkt, den gerade jemand zeichnet, stehenbleibt.
**Drei Funde unterwegs:**
- `JSON.stringify` als Fingerabdruck war falsch: dieselbe Zeichnung kam aus der Projektdatei als `{points, closed}` zurück und wurde als `{closed, points}` gebaut — ein Reload hätte dasselbe Bild unter neuem Schlüssel neu gerastert. Der Fingerabdruck schreibt seine Felder jetzt in fester Reihenfolge, ein Test misst genau diesen Fall.
- `toNumber` in `serialize.ts` macht aus jedem Unsinn eine 0, die `Number.isFinite`-Prüfungen liefen deshalb ins Leere: `{x: 'far'}` wurde zur linken Kante statt zu verschwinden. Formen lesen Zahlen streng über `asFinite`, weil 0 hier ein Ort auf der Leinwand ist.
- „Vector art" wäre zugleich Menüeintrag und Panel-Überschrift gewesen — ein bestehender Test fängt das ab, weil ein Klick nach Text die Überschrift träfe. Die Gruppe heißt jetzt **„Vector drawings"**, der Menüeintrag bleibt „Vector art".
20 neue Unit-Tests in `tests/core/vectorart.test.ts`; drei Gegenproben greifen (Fingerabdruck zurück auf `JSON.stringify` → 2 fallen, Zahlen wieder lose lesen → 4, Ringliste nicht kopieren → 1). Kein e2e-Check, Check-Summe blieb 1157, nur Modul 04 zählt 19 Asset-Typen statt 18.
## Etappe 2 — die Rasterung (`6371576`)
**Vector Art ist jetzt ein Bild.** Das Asset steht im Bild-Picker, ein Sprite trägt es in seiner *Texture*-Zeile, und was der Sprite zeigt, hat der Rasterizer gemalt. Registriert wird per Funktion statt per Modul-Seiteneffekt: `installVectorArtPictures(app.renderer)` hängt Source (core) und Generator (viewport) einmal pro Seite in `App.ts` ein — ein Test darf sie also weglassen.
**Die Regel liegt in `core`, gemalt wird in `viewport`.** `core/model/vectorPath.ts` liefert pro Form reine Zeichenbefehle (`move/line/curve/close`): geschlossene Ringe füllen, alle Ringe konturieren, ein Segment bleibt eine Linie, solange keines seiner Enden einen Griff trägt — ein einzelner Griff genügt für eine Kurve, der fehlende Kontrollpunkt sitzt dann auf seinem eigenen Ende, damit eine Ecke neben einer runden Seite stehen kann. Der Rückweg zum ersten Punkt wird nur gezeichnet, wenn er gebogen ist; sonst macht `close` ihn.
**Löcher gehen nur über einen Weg:** Pixi erkennt sie ausschließlich an einem `GraphicsPath` mit `checkForHoles` — und prüft dabei Enthaltensein, nicht Even-Odd-Tiefe. Für Ring-in-Ring reicht das; ohne das Flag malt der innere Ring einfach über den äußeren (gemessen, siehe Gegenproben).
**2× Überabtastung wie im Plan:** `RenderTexture.create({resolution: 2})` legt 512 px für eine 256er Leinwand auf die GPU, `texture.width` bleibt die Leinwandgröße — das ist das Maß, an dem der Sprite hängt. Geclippt wird durch die Rendertextur selbst: was über die Leinwand ragt, ist nicht drin, es braucht keine Maske. Gezeichnete Bilder bekommen denselben Sampler-Zustand wie hochgeladene (`setPictureFactory`, spiegelbildlich zu `keep()`).
**Ein Punkt zum Mitlesen:** `renderer.render()` läuft innerhalb der Bildanfrage. Das ist hier sicher, weil jeder Aufrufer während seines eigenen Widget-Redraws fragt und der Frame danach auf dem Ticker gezeichnet wird; ein verschachteltes `render` mitten in einem laufenden Renderpass wäre es nicht. Die Warnung steht im Modulkopf.
### Tests
```
./check.sh → 126 Files, 1860 Tests, CHECK EXIT 0 (vorher 124 / 1817)
npm run build → BUILD EXIT 0
node tools/e2e.mjs (alle Module) → ALL PASSED – 1165 checks
GM_STEPS=47 (frisch geseedet) → ALL PASSED – 10 checks
```
Neues **Modul 47** mit 8 eigenen Checks in eigener Lane: das Bild liegt in doppelter Auflösung auf der GPU und misst die Leinwand; der Sprite zeigt die Füllung; der innere Ring ist ein Loch (die Szene scheint durch); die später gezeichnete Form liegt oben; die Kontur läuft am Ring; eine geänderte Zeichnung bekommt einen neuen Schlüssel und die alte wird freigegeben; der Sprite zeigt die neue Farbe. Die Formen legt der Test per `mutate` ins Modell — der Tab, der sie zeichnet, ist Etappe 3.
21 neue Unit-Tests in `tests/core/vector-path.test.ts`. Drei Gegenproben greifen: Überabtastung auf 1 → der GPU-Check fällt (`pixelWidth: 256`); `checkForHoles` aus → der Loch-Check fällt (`#2e8b57` statt Hintergrund); den gebogenen Rückweg nicht zeichnen → ein Unit-Test fällt. Die volle Suite lief lokal statt nur in CI, weil die Änderung in der `TextureLibrary` jedes Bild berührt. **Check-Summe 1165, gemessen.**
**Zwei Mess-Fallen, die Läufe gekostet haben:** `__gm.objectRect(id)` gibt die **Mitte** des Objekts, nicht die Ecke — der erste Durchgang las vier Pixel neben dem Sprite und meldete Hintergrund, während der Rasterizer korrekt war; entschieden hat das der Screenshot, nicht die Zahlen. Und die Gitterlinien der Szene kreuzen genau im Mittelpunkt, der Loch-Check las `#363940` und fiel aus dem richtigen Grund falsch; er misst jetzt daneben.
## Offen
Etappe 3 (Tab: Stift, Auswahl, Griffe, `polygonEdit.ts` erweitern), Etappe 4 (Inspector: Formenliste mit Vorschau und Reihenfolge, Farbe/Deckkraft/Kontur), Etappe 5 (e2e zum Zeichnen, Wiki-Seite *Vector Art*).
Etappe 3 ist gebaut (Commit 3d7ad65), dazu ein Fix, den der Test im Browser nicht gefunden hat, sondern der Benutzer: das Ziehen eines Punktes hinkte der Maus hinterher (Commit 3a68067).
Etappe 3 — der Tab
Kopfzeile mit Select / Pen / Grid / Fit, Schachbrett, die Leinwand als Rahmen, darin die Zeichnung — gemalt mit denselben Befehlen wie der Rasterizer (paintDrawing ist dafür aus viewport/vectorArtPicture.ts exportiert), so dass Tab und Bild nie auseinanderlaufen können.
Pen: Klick setzt Punkte, Ziehen zieht die Griffe heraus (beide, gespiegelt), Klick auf den ersten Punkt schließt den Ring, Esc beendet die Linie.
Select: Punkt, Griff und ganze Form ziehen, Klick auf eine Kante setzt dort einen Punkt, Doppelklick nimmt einen weg, Alt macht eine Ecke rund und wieder eckig, Delete entfernt die ausgewählte Form.
Grid rastet auf 8 px ein, Vorgabe aus.
Die Regeln sind in polygonEdit.ts gewachsen, kein zweites Modul — wie im Plan (Antwort 7). Zwei Punkte daraus sind mehr als Fleißarbeit: ein Punkt auf einer gebogenen Kante spaltet die Kurve (de Casteljau) statt sie abzuschneiden, beide Hälften zeichnen zusammen genau das, was vorher da war; und eine gebogene Kante wird abgelaufen, nicht über ihre Sehne gemessen, sonst greift ein Klick auf die Kurve ins Leere. Eine Geste ist ein Undo-Schritt (Vorschau schreibt direkt, beim Loslassen wird der alte Stand zurückgelegt und das Kommando ausgeführt) — dasselbe Muster wie bei den Frame-Umrissen aus #141. Neu nötig war eine Auswahlart shape in state.ts.
Tests:check.sh 127 Dateien / 1878 Tests, ALL PASSED – 1172 checks, 18 neue Unit-Tests in tests/editor/ring-edit.test.ts, 7 neue e2e-Checks in 47c/47d.
Ein Fund, der festgehalten gehört: eine Gegenprobe, die keine war. Das Schließen des Stifts mit * 0 entschärft ließ den Lauf grün — zu Recht, denn der schließende Klick landet auf exakt der Bildschirmkoordinate von Punkt 0, die Distanz ist 0 und <= 0 ist wahr. Erst eine unerfüllbare Bedingung schaltete wirklich ab (2 Checks fallen). Dazu: ein false && vor einem Guard bricht das TS-Narrowing, der Build steigt aus, und zwei Läufe gingen gegen das alte Bundle.
Der Fix: Ziehen hält jetzt mit der Maus Schritt
Gemeldet als „aktualisiert erst, wenn ich die Maus stehen lasse". Das ist ein echter Rückstau, kein Gefühl — gemessen mit einem Wegwerf-Modul, das 120 Maus-Events im 8-ms-Takt schickt (125 Hz):
Objekte in der Szene
ein preview()
je Move-Event vorher
nachher
0
3,7 ms
10 ms
7 ms
40
5,4 ms
19 ms
4 ms
120
10,3 ms
36 ms
3 ms
Die 120 Moves dauerten vorher 4173 ms statt der gesollten 960 — der Rest war Rückstau in der Eingabeschlange, genau das Symptom.
Ursache: Jede Mausbewegung schrieb das Modell und feuerte state.preview() → emit('project'), und daran hängt die ganze Editor-Kette (Play-Sync, Invalidieren des Editor-Laufs, alle Dokumente und Panels, dazu das Neurastern des gezeichneten Bildes). Das ist Arbeit für einen Frame, nicht für jedes Event.
Fix:onFrame(key, fn) in src/ui/render.ts sammelt solche Arbeit pro Schlüssel und führt sie im Ticker vorapp.render() aus — danach wäre sie einen Frame zu spät im Bild. Der Canvas stellt im pointermove nur noch ein und räumt seinen Platz beim Loslassen (cancelFrameWork), weil dann das Kommando den Endstand schreibt. Während der Geste ist die eigene Ring-Kopie die Wahrheit, das Modell wird nie zurückgelesen, es geht also nichts verloren.
Tests:check.sh 127 Dateien / 1883 Tests, volle Suite lokal (weil die Änderung im Render-Loop sitzt): ALL PASSED – 1172 checks, Summe unverändert — ein Zeitcheck im Browser wäre unter CI-Last wackelig, der Beleg ist die Node-Messung. 5 neue Unit-Tests in tests/ui/render.test.ts. Zwei Gegenproben greifen: die Arbeit nach dem Zeichnen laufen lassen lässt einen Unit-Test fallen, das Coalescing im Canvas ausbauen bringt die Messung zurück auf 36 ms / 4153 ms. Vom Benutzer im eigenen Projekt nachgeprüft.
Nicht angefasst: Szenen-Viewport, Texture-Tab und Graph rufen preview() genauso im Move-Handler und haben dieselbe Kostenstelle. Das liegt außerhalb des gemeldeten Fehlers; onFrame liegt allgemein in ui/ und wäre ein Kandidat für die nächste Refactoring-Runde (#85).
Offen
Etappe 4 (Inspector: Formenliste mit Vorschau, Reihenfolge-Knöpfe, Farbe/Deckkraft/Kontur) und Etappe 5 (Wiki-Seite Vector Art, restliche Doku). Das Issue bleibt offen.
**Etappe 3 ist gebaut** (Commit `3d7ad65`), dazu ein Fix, den der Test im Browser nicht gefunden hat, sondern der Benutzer: **das Ziehen eines Punktes hinkte der Maus hinterher** (Commit `3a68067`).
### Etappe 3 — der Tab
Kopfzeile mit **Select / Pen / Grid / Fit**, Schachbrett, die Leinwand als Rahmen, darin die Zeichnung — gemalt mit denselben Befehlen wie der Rasterizer (`paintDrawing` ist dafür aus `viewport/vectorArtPicture.ts` exportiert), so dass Tab und Bild nie auseinanderlaufen können.
- **Pen:** Klick setzt Punkte, Ziehen zieht die Griffe heraus (beide, gespiegelt), Klick auf den ersten Punkt schließt den Ring, Esc beendet die Linie.
- **Select:** Punkt, Griff und ganze Form ziehen, Klick auf eine Kante setzt dort einen Punkt, Doppelklick nimmt einen weg, Alt macht eine Ecke rund und wieder eckig, Delete entfernt die ausgewählte Form.
- **Grid** rastet auf 8 px ein, Vorgabe aus.
Die Regeln sind in `polygonEdit.ts` gewachsen, kein zweites Modul — wie im Plan (Antwort 7). Zwei Punkte daraus sind mehr als Fleißarbeit: ein Punkt auf einer gebogenen Kante **spaltet die Kurve** (de Casteljau) statt sie abzuschneiden, beide Hälften zeichnen zusammen genau das, was vorher da war; und eine gebogene Kante wird **abgelaufen**, nicht über ihre Sehne gemessen, sonst greift ein Klick auf die Kurve ins Leere. Eine Geste ist ein Undo-Schritt (Vorschau schreibt direkt, beim Loslassen wird der alte Stand zurückgelegt und das Kommando ausgeführt) — dasselbe Muster wie bei den Frame-Umrissen aus #141. Neu nötig war eine Auswahlart `shape` in `state.ts`.
**Tests:** `check.sh` 127 Dateien / 1878 Tests, `ALL PASSED – 1172 checks`, 18 neue Unit-Tests in `tests/editor/ring-edit.test.ts`, 7 neue e2e-Checks in 47c/47d.
**Ein Fund, der festgehalten gehört: eine Gegenprobe, die keine war.** Das Schließen des Stifts mit `* 0` entschärft ließ den Lauf grün — zu Recht, denn der schließende Klick landet auf exakt der Bildschirmkoordinate von Punkt 0, die Distanz ist 0 und `<= 0` ist wahr. Erst eine unerfüllbare Bedingung schaltete wirklich ab (2 Checks fallen). Dazu: ein `false &&` vor einem Guard bricht das TS-Narrowing, der Build steigt aus, und zwei Läufe gingen gegen das alte Bundle.
### Der Fix: Ziehen hält jetzt mit der Maus Schritt
Gemeldet als „aktualisiert erst, wenn ich die Maus stehen lasse". Das ist ein echter Rückstau, kein Gefühl — gemessen mit einem Wegwerf-Modul, das 120 Maus-Events im 8-ms-Takt schickt (125 Hz):
| Objekte in der Szene | ein `preview()` | je Move-Event **vorher** | **nachher** |
|---|---|---|---|
| 0 | 3,7 ms | 10 ms | 7 ms |
| 40 | 5,4 ms | 19 ms | 4 ms |
| 120 | 10,3 ms | 36 ms | 3 ms |
Die 120 Moves dauerten vorher **4173 ms** statt der gesollten 960 — der Rest war Rückstau in der Eingabeschlange, genau das Symptom.
**Ursache:** Jede Mausbewegung schrieb das Modell und feuerte `state.preview()` → `emit('project')`, und daran hängt die ganze Editor-Kette (Play-Sync, Invalidieren des Editor-Laufs, alle Dokumente und Panels, dazu das Neurastern des gezeichneten Bildes). Das ist Arbeit für einen Frame, nicht für jedes Event.
**Fix:** `onFrame(key, fn)` in `src/ui/render.ts` sammelt solche Arbeit pro Schlüssel und führt sie im Ticker **vor** `app.render()` aus — danach wäre sie einen Frame zu spät im Bild. Der Canvas stellt im `pointermove` nur noch ein und räumt seinen Platz beim Loslassen (`cancelFrameWork`), weil dann das Kommando den Endstand schreibt. Während der Geste ist die eigene Ring-Kopie die Wahrheit, das Modell wird nie zurückgelesen, es geht also nichts verloren.
**Tests:** `check.sh` 127 Dateien / 1883 Tests, volle Suite lokal (weil die Änderung im Render-Loop sitzt): `ALL PASSED – 1172 checks`, Summe unverändert — ein Zeitcheck im Browser wäre unter CI-Last wackelig, der Beleg ist die Node-Messung. 5 neue Unit-Tests in `tests/ui/render.test.ts`. Zwei Gegenproben greifen: die Arbeit nach dem Zeichnen laufen lassen lässt einen Unit-Test fallen, das Coalescing im Canvas ausbauen bringt die Messung zurück auf 36 ms / 4153 ms. Vom Benutzer im eigenen Projekt nachgeprüft.
**Nicht angefasst:** Szenen-Viewport, Texture-Tab und Graph rufen `preview()` genauso im Move-Handler und haben dieselbe Kostenstelle. Das liegt außerhalb des gemeldeten Fehlers; `onFrame` liegt allgemein in `ui/` und wäre ein Kandidat für die nächste Refactoring-Runde (#85).
### Offen
Etappe 4 (Inspector: Formenliste mit Vorschau, Reihenfolge-Knöpfe, Farbe/Deckkraft/Kontur) und Etappe 5 (Wiki-Seite *Vector Art*, restliche Doku). Das Issue bleibt offen.
Etappe 4 ist gebaut, committet und gepusht (78595ec) — das Issue bleibt offen, Etappe 5 (Wiki-Seite Vector Art und restliche Doku) folgt direkt.
Was jetzt da ist
Eine ausgewählte Form hatte bisher die Zeilen ihres Assets — Farbe, Kontur und Reihenfolge einer Form waren also nur über die Projektdatei erreichbar. Jetzt zeigt der Inspector SHAPES (die Liste), die vier Reihenfolge-Knöpfe, VECTOR SHAPE (Name, Fill, Opacity, Outline; mit Kontur zusätzlich Line colour und Line width), Delete shape und Add shape. Wählt man das Asset, steht dieselbe Liste unter seinen Zeilen, ohne dass eine Form gewählt ist. Die Leinwandgröße bleibt eine gewöhnliche Property am Asset.
Jede Zeile zeigt ihre Form, gemalt mit denselben Befehlen wie der Rasterizer (shapeDrawing + paintDrawing) in einen 22-px-Swatch, die ganze Leinwand hineinskaliert — die Zeile sagt also auch, wo auf der Leinwand die Form sitzt. Ein Punkt, der im Tab gezogen wird, zieht die Vorschau mit.
Die Reihenfolge ist die Liste: oberste Zeile = zuletzt gezeichnet, wie in jeder Layer-Liste. Ziehen einer Zeile mit Markierungslinie, dazu die vier Knöpfe aus dem Nachtrag (eins vor, ganz nach vorne, eins zurück, ganz nach hinten) — deaktiviert, wenn es nichts zu verschieben gibt.
Ein gezogener Farbregler ist ein Undo-Schritt: die Stilzeilen laufen über previewingHandlers, beim Loslassen ein SetVectorShapeStyleCommand. Die Kontur wird per Häkchen hinzugefügt oder weggenommen, nicht unsichtbar gemacht; Linienbreite 0 ist keine Kontur, das Kommando lehnt sie ab, die Zeile klemmt auf 0,1.
14 neue Unit-Tests in tests/editor/shape-rows.test.ts: die Arithmetik der Liste (Reihenfolge, Ziel einer abgelegten Zeile) und die echten Zeilen (welche Zeilen mit und ohne Kontur, Fill als ein Undo-Schritt, Opacity geklemmt, Häkchen gibt und nimmt die Kontur, Signatur, Refresh).
7 neue e2e-Checks (Block 47e): die Liste zeigt die Modellreihenfolge umgedreht, ein Klick auf eine Zeile wählt die Form, die Fill-Zeile färbt um und der Tab zeigt es, das Häkchen bringt die Konturzeilen, „To front" legt die Form über die, die sie verdeckt hat (am Pixel gemessen), Ziehen einer Zeile ans Ende zeichnet sie zuerst, „Add shape" legt eine leere Form an und wählt sie.
Volle Suite lokal statt nur CI, weil die Änderung im Inspector sitzt und damit jede Auswahl berührt.
Drei Gegenproben, alle greifen: Reihenfolge nicht umdrehen -> 1 Unit-Test und 2 e2e-Checks fallen; die Lücken-Korrektur beim Ablegen weg -> 2 Unit-Tests und der Zieh-Check; Signatur auf eine Konstante -> Modul 47 stirbt, weil die Konturzeilen nach dem Einschalten nie erscheinen. Originale per cmp zurückgeprüft und neu gebaut.
Screenshot angesehen (47-shape-rows): Zeilen mit Vorschau, ausgewählte Zeile markiert, Knöpfe, Fill #20c0e8, Häkchen, Line colour/width.
Drei Funde aus dieser Etappe
locate('Outline') zielt daneben. Der Helfer gibt die Mitte des Label-Textes, und „Outline" ist breiter als „Fill" — ein fester Versatz landet mal im Feld, mal im Nichts. Richtig ist __gm.inspectorField(label), der die Mitte des Bedienelements kennt.
Ein Klick in ein Textfeld setzt den Cursor, er wählt nichts aus. Das getippte 20c0e8 landete als #e8c02020c0e8 im Hex-Feld, parseHex gab null, nichts passierte — und typeTextwirft dabei nicht, weil es nur prüft, ob der Text enthalten ist. Jetzt geht ein Ctrl+A voraus.
Vier Knöpfe nebeneinander kürzen „Backward" auf „Backw…". Gesehen hat das nur der Screenshot, kein Check; jetzt zwei Reihen zu zweit, dasselbe Muster wie im Tilemap-Panel.
Doku
Wiki c473762: Architecture (shapeSection, ShapeList, shapeRows samt der Regel, warum die Liste auf dem Kopf steht), Development (Check-Summe). README-Check-Summe 1179 — gemessen, nicht gerechnet. Die eigene Seite Vector Art gehört laut Plan in Etappe 5.
Offen bleibt Etappe 5: die Wiki-Seite Vector Art und die restliche Doku.
**Etappe 4 ist gebaut, committet und gepusht** (`78595ec`) — das Issue bleibt offen, Etappe 5 (Wiki-Seite *Vector Art* und restliche Doku) folgt direkt.
## Was jetzt da ist
Eine ausgewählte Form hatte bisher die Zeilen ihres **Assets** — Farbe, Kontur und Reihenfolge einer Form waren also nur über die Projektdatei erreichbar. Jetzt zeigt der Inspector **SHAPES** (die Liste), die vier Reihenfolge-Knöpfe, **VECTOR SHAPE** (Name, Fill, Opacity, Outline; mit Kontur zusätzlich Line colour und Line width), *Delete shape* und *Add shape*. Wählt man das Asset, steht dieselbe Liste unter seinen Zeilen, ohne dass eine Form gewählt ist. Die Leinwandgröße bleibt eine gewöhnliche Property am Asset.
- **Jede Zeile zeigt ihre Form**, gemalt mit denselben Befehlen wie der Rasterizer (`shapeDrawing` + `paintDrawing`) in einen 22-px-Swatch, die ganze Leinwand hineinskaliert — die Zeile sagt also auch, *wo* auf der Leinwand die Form sitzt. Ein Punkt, der im Tab gezogen wird, zieht die Vorschau mit.
- **Die Reihenfolge ist die Liste**: oberste Zeile = zuletzt gezeichnet, wie in jeder Layer-Liste. Ziehen einer Zeile mit Markierungslinie, dazu die vier Knöpfe aus dem Nachtrag (eins vor, ganz nach vorne, eins zurück, ganz nach hinten) — deaktiviert, wenn es nichts zu verschieben gibt.
- **Ein gezogener Farbregler ist ein Undo-Schritt**: die Stilzeilen laufen über `previewingHandlers`, beim Loslassen ein `SetVectorShapeStyleCommand`. Die Kontur wird per Häkchen hinzugefügt oder weggenommen, nicht unsichtbar gemacht; Linienbreite 0 ist keine Kontur, das Kommando lehnt sie ab, die Zeile klemmt auf 0,1.
## Tests
```
./check.sh -> 128 Files, 1897 Tests, CHECK EXIT 0 (vorher 127 / 1883)
npm run build -> BUILD EXIT 0
node tools/e2e.mjs (alle Module) -> ALL PASSED - 1179 checks (vorher 1172)
GM_STEPS=47 (frisch geseedet) -> ALL PASSED - 24 checks (vorher 17)
```
- **14 neue Unit-Tests** in `tests/editor/shape-rows.test.ts`: die Arithmetik der Liste (Reihenfolge, Ziel einer abgelegten Zeile) und die echten Zeilen (welche Zeilen mit und ohne Kontur, Fill als ein Undo-Schritt, Opacity geklemmt, Häkchen gibt und nimmt die Kontur, Signatur, Refresh).
- **7 neue e2e-Checks** (Block 47e): die Liste zeigt die Modellreihenfolge umgedreht, ein Klick auf eine Zeile wählt die Form, die Fill-Zeile färbt um *und der Tab zeigt es*, das Häkchen bringt die Konturzeilen, „To front" legt die Form über die, die sie verdeckt hat (am Pixel gemessen), Ziehen einer Zeile ans Ende zeichnet sie zuerst, „Add shape" legt eine leere Form an und wählt sie.
- **Volle Suite lokal statt nur CI**, weil die Änderung im Inspector sitzt und damit jede Auswahl berührt.
- **Drei Gegenproben, alle greifen**: Reihenfolge nicht umdrehen -> 1 Unit-Test **und** 2 e2e-Checks fallen; die Lücken-Korrektur beim Ablegen weg -> 2 Unit-Tests und der Zieh-Check; Signatur auf eine Konstante -> Modul 47 stirbt, weil die Konturzeilen nach dem Einschalten nie erscheinen. Originale per `cmp` zurückgeprüft und neu gebaut.
- **Screenshot angesehen** (`47-shape-rows`): Zeilen mit Vorschau, ausgewählte Zeile markiert, Knöpfe, Fill `#20c0e8`, Häkchen, Line colour/width.
## Drei Funde aus dieser Etappe
- **`locate('Outline')` zielt daneben.** Der Helfer gibt die Mitte des Label-*Textes*, und „Outline" ist breiter als „Fill" — ein fester Versatz landet mal im Feld, mal im Nichts. Richtig ist `__gm.inspectorField(label)`, der die Mitte des Bedienelements kennt.
- **Ein Klick in ein Textfeld setzt den Cursor, er wählt nichts aus.** Das getippte `20c0e8` landete als `#e8c02020c0e8` im Hex-Feld, `parseHex` gab null, nichts passierte — und `typeText` **wirft dabei nicht**, weil es nur prüft, ob der Text enthalten ist. Jetzt geht ein Ctrl+A voraus.
- **Vier Knöpfe nebeneinander kürzen „Backward" auf „Backw…".** Gesehen hat das nur der Screenshot, kein Check; jetzt zwei Reihen zu zweit, dasselbe Muster wie im Tilemap-Panel.
## Doku
Wiki `c473762`: Architecture (`shapeSection`, `ShapeList`, `shapeRows` samt der Regel, warum die Liste auf dem Kopf steht), Development (Check-Summe). README-Check-Summe **1179** — gemessen, nicht gerechnet. Die eigene Seite *Vector Art* gehört laut Plan in Etappe 5.
**Offen bleibt Etappe 5**: die Wiki-Seite *Vector Art* und die restliche Doku.
Die Etappen 1 bis 4 haben die Formen per mutate ins Modell gelegt, weil der Tab, der sie zeichnet, erst Etappe 3 war. Block 47f geht jetzt den Weg, den du gehst:
Das +-Menü bietet Vector art unter den Typen, die nur in der Bibliothek leben; der Eintrag legt ein Asset an, ausgewählt und im Panel stehend.
Ein Doppelklick auf die Zeile öffnet den Vector-Art-Tab auf diesem Asset — das hatte Etappe 3 gebaut, aber kein Check hat es bisher gemessen.
Die Texture-Zeile eines Sprites bietet die Zeichnung nach Namen an, einsortiert zwischen den übrigen Bildern, und die Auswahl dort lässt den Sprite sie tragen — am Pixel in der Szene gemessen, nicht an der Zeile.
Ein Fund: Der Picker zeigte drei Zeilen, wo ich mehr erwartet hatte. Der Grund ist in Ordnung — ein frisches Fixture hat eine Textur und die Zeichnung, „None" ist die dritte. Der Check nennt jetzt alle drei, statt sie zu zählen: geht eine verloren, sagt er welche.
Die Doku
Vector Art war in Stücken dokumentiert — Textures trug das erzeugte Bild, Architecture die Module, Keyboard den Tab. Es gibt jetzt eine eigene Wiki-Seite Vector Art, die vom ersten Klick bis zum fertigen Sprite durchliest: Asset und Leinwand, die zwei Werkzeuge, was ein Griff tut, wie Ringe ein Loch machen, die Formenliste samt Reihenfolge — und die Grenzen, die eigene Issues sind (#186, #187, #188, #189). Sidebar und Home tragen die Seite; Textures und Tiles verweisen darauf, statt es ein zweites Mal in eigenen Worten zu beschreiben.
Die Unit-Tests sind seit Etappe 4 unverändert — Etappe 5 ist Browser-Beleg und Doku, keine neue Regel. Beide Gegenproben greifen: keinen Erzeuger registrieren → der Picker-Check fällt; den Tab ohne das Asset öffnen → der Doppelklick-Check fällt. Screenshot angesehen.
das Modell: Ringe, Griffe, Löcher, sechs Undo-Commands
2
6371576
die Rasterung, 2× überabgetastet, an der Leinwand geclippt
3
3d7ad65
der Tab: Stift, Auswahl, Griffe
—
3a68067
das Ziehen hält mit der Maus mit (36 → 3 ms je Event)
4
78595ec
die Inspector-Zeilen einer Form samt Formenliste
5
ad62a0f
der Nutzerweg als Check, die Wiki-Seite
Die Check-Summe ist von 1157 auf 1184 gewachsen, die Unit-Tests von 1799 auf 1897.
Offen bleibt bewusst: Verläufe (#186), boolesche Operationen (#187), Collider aus einer Form (#188), SVG-Import (#189), Gruppen und Layers (#190), ein eigenes Panel statt Inspector (#191). Laufzeit-Nodes sind nach deiner Antwort 10 nicht angelegt.
Auf den CI-Lauf zu ad62a0f habe ich nicht gewartet — erst der bestätigt die Zahlen oben auf einer fremden Maschine.
## Etappe 5 ist fertig — #129 ist damit durch
`ad62a0f` (Repo) und `21bea26` (Wiki).
### Der Nutzerweg ist jetzt gemessen
Die Etappen 1 bis 4 haben die Formen per `mutate` ins Modell gelegt, weil der Tab, der sie zeichnet, erst Etappe 3 war. Block **47f** geht jetzt den Weg, den du gehst:
- Das `+`-Menü bietet **Vector art** unter den Typen, die nur in der Bibliothek leben; der Eintrag legt ein Asset an, ausgewählt und im Panel stehend.
- Ein Doppelklick auf die Zeile öffnet den **Vector-Art-Tab** auf diesem Asset — das hatte Etappe 3 gebaut, aber kein Check hat es bisher gemessen.
- Die *Texture*-Zeile eines Sprites bietet die Zeichnung **nach Namen** an, einsortiert zwischen den übrigen Bildern, und die Auswahl dort lässt den Sprite sie tragen — **am Pixel in der Szene gemessen**, nicht an der Zeile.
**Ein Fund:** Der Picker zeigte drei Zeilen, wo ich mehr erwartet hatte. Der Grund ist in Ordnung — ein frisches Fixture hat eine Textur und die Zeichnung, „None" ist die dritte. Der Check **nennt jetzt alle drei**, statt sie zu zählen: geht eine verloren, sagt er welche.
### Die Doku
Vector Art war in Stücken dokumentiert — Textures trug das erzeugte Bild, Architecture die Module, Keyboard den Tab. Es gibt jetzt eine **eigene Wiki-Seite *Vector Art***, die vom ersten Klick bis zum fertigen Sprite durchliest: Asset und Leinwand, die zwei Werkzeuge, was ein Griff tut, wie Ringe ein Loch machen, die Formenliste samt Reihenfolge — und die Grenzen, die eigene Issues sind (#186, #187, #188, #189). Sidebar und Home tragen die Seite; Textures und Tiles verweisen darauf, statt es ein zweites Mal in eigenen Worten zu beschreiben.
### Tests
```
./check.sh → 128 Files, 1897 Tests, CHECK EXIT 0
node tools/e2e.mjs (alle Module) → ALL PASSED – 1184 checks (vorher 1179)
GM_STEPS=47 (frisch geseedet) → ALL PASSED – 29 checks (vorher 24)
```
Die Unit-Tests sind seit Etappe 4 unverändert — Etappe 5 ist Browser-Beleg und Doku, keine neue Regel. **Beide Gegenproben greifen:** keinen Erzeuger registrieren → der Picker-Check fällt; den Tab ohne das Asset öffnen → der Doppelklick-Check fällt. Screenshot angesehen.
### Was #129 insgesamt gebracht hat
| Etappe | Commit | was |
|---|---|---|
| Grundlage (#185) | `cb1804d` | erzeugte Bilder im Bildpfad |
| 1 | `bdb3f06` | das Modell: Ringe, Griffe, Löcher, sechs Undo-Commands |
| 2 | `6371576` | die Rasterung, 2× überabgetastet, an der Leinwand geclippt |
| 3 | `3d7ad65` | der Tab: Stift, Auswahl, Griffe |
| — | `3a68067` | das Ziehen hält mit der Maus mit (36 → 3 ms je Event) |
| 4 | `78595ec` | die Inspector-Zeilen einer Form samt Formenliste |
| 5 | `ad62a0f` | der Nutzerweg als Check, die Wiki-Seite |
Die Check-Summe ist von 1157 auf **1184** gewachsen, die Unit-Tests von 1799 auf **1897**.
**Offen bleibt bewusst:** Verläufe (#186), boolesche Operationen (#187), Collider aus einer Form (#188), SVG-Import (#189), Gruppen und Layers (#190), ein eigenes Panel statt Inspector (#191). Laufzeit-Nodes sind nach deiner Antwort 10 nicht angelegt.
Auf den CI-Lauf zu `ad62a0f` habe ich nicht gewartet — erst der bestätigt die Zahlen oben auf einer fremden Maschine.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Vector Art mit eigenem editor (tab) - dort soll man Formen mit farbe und kurven erstellen können. Punkte hinzufügen, bezier kurven. das kann man dann bei einem Sprite auswählen können wie Texture oder Texture-Frame
Fragen zur Planung
Ich habe den Code sondiert (Bildpfad, Sprite-Rendering, Asset-Modell, der vorhandene Polygon-Editor) und dabei ein paar Entscheidungen gefunden, die vor dem Bauen fallen müssen. Zu jeder mein Vorschlag — es reicht, wenn du „passt" schreibst oder die Nummern nennst, die du anders willst.
1. Rastern oder als echte Geometrie zeichnen?
Das ist die Grundsatzfrage, alles andere hängt daran. Heute ist jedes Sprite ein Mesh mit Quad-Geometrie, das eine
Textureträgt (viewport/symbols.ts), und daran hängen Shader (viewport/shaders.ts), Partikel, Nine-Slice und die Tile-Bilder — alle erwarten eine Textur.RenderTexturegezeichnet und wie eine hochgeladene Datei aus derTextureLibrarygeholt; neu gerastert wird beim Ändern des Assets. Vorteil: alles Bestehende funktioniert sofort — Shader, Partikel, Tiles, Nine-Slice, Animation, das Seitenverhältnis. Nachteil: beim starken Hineinzoomen ist es ein Bild, keine Kurve mehr (mildern lässt sich das über eine Rasterauflösung, die größer ist als die Sprite-Größe).2. Wo darf Vector Art überall stehen?
Ein Bild ist heute eine Id — Textur-Asset oder Frame — und alles geht durch
resolvePicture(). Wenn Vector Art dort mitläuft, taucht es ohne Mehraufwand überall auf, wo heute ein Bild steht: Sprite, Tile (und damit Tilemaps), Partikelquelle, der Bild-Picker.Vorschlag: überall, also ein Ausgang statt vier. Willst du es bewusst nur am Sprite haben?
3. Wie groß ist ein Vector-Art-Asset?
Eine Textur hat eine Pixelgröße, daraus nimmt ein neu gesetztes Sprite seine Breite und Höhe (
properties/aspect.ts). Kurven haben keine.Vorschlag: Das Asset hat eine eigene Leinwandgröße (Vorgabe 256 × 256, im Inspector änderbar). Sie ist der Bezug für Größe und Seitenverhältnis, und der Editor zeigt sie als Rahmen. Zusatzfrage: Wird an diesem Rahmen abgeschnitten oder darf eine Form darüber hinausragen? Mein Vorschlag: abschneiden, sonst ist die Größe des Sprites eine Lüge.
4. Was genau ist eine Form?
Vorschlag: ein geschlossener Pfad aus Punkten, wobei jeder Punkt entweder eine Ecke (gerade Kante) oder glatt mit zwei Bezier-Griffen ist (kubisch) — das übliche Pen-Tool-Modell. Dazu je Form: Füllfarbe + Deckkraft, optional Kontur (Farbe, Breite). Offene Pfade (nur Strich, keine Füllung) — willst du die auch?
5. Wie reich darf die Füllung sein?
Vorschlag für Runde 1: einfarbig plus Deckkraft. Verläufe (linear/radial) und eine Füllung mit einer Textur wären je ein eigenes Issue — sagenswert ist mir vor allem, ob du Verläufe als Teil von #129 erwartest.
6. Mehrere Formen, Löcher, Gruppen?
Vorschlag: eine flache Liste von Formen mit Reihenfolge (oben/unten verschieben), keine Gruppen und keine booleschen Operationen (Vereinigen/Abziehen). Löcher bekommt eine Form dadurch, dass sie mehrere Ringe haben darf (Even-Odd) — oder reicht dir fürs Erste, ein Loch durch eine hintergrundfarbene Form darüber zu bauen? Mein Vorschlag: Ringe ja, boolesche Operationen nein.
7. Werkzeuge im Tab
Vorschlag: Zoomen/Schieben wie im Texture-Tab; Stift (neue Form Punkt für Punkt), Auswahl (Punkt ziehen, Griff ziehen, ganze Form ziehen), Punkt auf einer Kante einfügen, Doppelklick entfernt ihn — die Regeln dafür gibt es schon als reine Funktionen in
editor/polygonEdit.ts(#141/#151), die würde ich um die Bezier-Griffe erweitern statt etwas Zweites zu bauen. Punkte am Raster einrasten: einschaltbar, standardmäßig aus.8. Wo werden Farbe und Kontur eingestellt?
Vorschlag: im Inspector rechts, wenn eine Form ausgewählt ist (wie sonst die Eigenschaften eines Objekts), plus eine Formenliste im Tab. Alternative wäre eine Werkzeugleiste im Tab selbst.
9. Kollision
Analog zu #161 („Frame outline" als Collider-Form) könnte ein Collider die Umrisse einer Vector-Art-Form übernehmen.
Vorschlag: nicht in dieser Runde, aber als eigenes Issue notieren — die Formen liegen dann ja schon als Polygone vor.
10. Laufzeit
Vorschlag: keine Nodes zum Ändern von Farben oder Punkten im Lauf, und keine Animation der Punkte auf der Timeline. Beides später, wenn es gebraucht wird. Ein Sprite kann seine Vector Art zur Laufzeit wechseln, weil
textureschon eine Eigenschaft ist — das fällt ohne Arbeit ab.11. SVG-Import?
Vorschlag: nein, eigenes Issue. Ein SVG-Parser ist ein Thema für sich (Transformationen, Gruppen, Verläufe, Text).
12. Verhältnis zu #130 (Pixel Editor)
Beide Issues bringen ein Bild, das keine hochgeladene Datei ist. Der Bildpfad (
resolvePicture, der Picker, dieTextureLibrary) muss das genau einmal lernen.Vorschlag: Ich baue diese Stelle in #129 so, dass #130 nur noch ein zweiter Erzeuger ist, und schreibe den Vertrag ins Wiki. Wenn du #130 zeitnah willst, wäre auch denkbar, die gemeinsame Grundlage vorzuziehen — sag Bescheid, ich richte mich danach.
Offen für mich ist vor allem 1, 3 und 6 — der Rest lässt sich auch später noch drehen.
weiters: reihenfolge von formen muss man auch festlegen können (eins nach vorne bringen, ganz nach vorne bringen, und auch eins nach hinten bzw. ganz nach hinten) - vielleicht wäre da eine hierarchie im inspektor nicht schlecht, mit vorschau vom shape. weiteres folge issue wären dann sogar gruppen bzw. layers
Plan
Aus deinen Antworten (Kommentar oben) folgt der Zuschnitt. Gebaut ist noch nichts — das hier ist der Stand zum Durchsehen.
Die tragende Entscheidung
Rastern (1). Damit wird Vector Art behandelt wie eine Textur, läuft durch
resolvePicture()und ist ohne Mehrarbeit überall (2) — Sprite, Tile, Tilemap, Partikelquelle, Picker. Shader, Nine-Slice, Animation und das Seitenverhältnis funktionieren unverändert, weil sie weiterhin eineTexturesehen.Die einzige Stelle, die ich im Plan offen gelassen hatte, ist die Überabtastung: gerastert wird in 2× der Leinwandgröße, damit starkes Hineinzoomen nicht sofort matscht. Das ist so festgelegt, solange du nichts anderes sagst.
Vorgezogen: #185 — Erzeugte Bilder im Bildpfad
Zu 12 („gemeinsame Grundlage vorziehen"). Heute ist
TextureRef.fileein Dateiname auf dem Server (isTextureFile()prüft Hash + Endung,src/core/model/texture.ts:98), undTextureLibrarylädt ihn per HTTP. Ein erzeugtes Bild hat keine Datei.#185 lehrt dem Bildpfad genau einmal, dass ein Bild auch erzeugt sein darf: Schlüssel
gen:<assetId>@<rev>, ein Register von Erzeugern statt des Servers, Neuzeichnen über einen Revisionszähler, Leinwandgröße alssourceWidth/Height, Picker und Nutzungssuche mit. #130 (Pixel Editor) ist danach nur noch ein zweiter Erzeuger. Das Issue wird eigens gebaut, committet und geschlossen, bevor #129 anfängt.#129 selbst, in fünf Etappen
vectorartinASSET_TYPES(nicht platzierbar): Leinwand w/h, Liste von Formen. Eine Form = ein oder mehrere Ringe (Even-Odd, also Löcher — 6), je Ring Punkte mit optionalen Bezier-Griffenin/out,closedja/nein (offener Pfad = nur Kontur — 4), dazu Füllfarbe + Deckkraft und optional Kontur (Farbe, Breite — 5). Serialisierung, Validierung, Undo-Commands.RenderTexturein 2× Leinwandgröße, an der Leinwand abgeschnitten (3).editor/polygonEdit.tswird um die Bezier-Griffe erweitert statt etwas Zweites zu bauen (7) — dieselben Regeln gelten dann auch für Collider-Polygone und Frame-Umrisse. Raster-Einrasten schaltbar, Vorgabe aus.Folge-Issues (angelegt)
Laufzeit-Nodes lege ich nicht an (10, „brauchen wir vorerst gar nicht"). Dass ein Sprite seine Vector Art im Lauf wechseln kann, fällt über die Eigenschaft
textureohnehin ab.Zwei Anmerkungen
Etappen 1 und 2 sind gebaut und gepusht. Die Grundlage #185 ist vorher geschlossen. Das Issue bleibt offen — es fehlen Etappe 3 (Tab), 4 (Inspector) und 5 (Tests/Doku zum Zeichnen).
Etappe 1 — das Modell (
bdb3f06)Der Asset-Typ
vectorartexistiert, nicht platzierbar, im+-Menü und im Asset-Panel mit eigenem Symbol (eine Kurve zwischen zwei Punkten). Einen Tab hat er noch nicht — er verhält sich wie eine Font: auswählbar, Inspector zeigt seine Zeilen, Doppelklick führt nirgendwohin. Ein halbes Asset wäre nur im Weg, der Tab ist Etappe 3.Das Datenmodell in
Asset.ts: eine Form = mehrere Ringe (Even-Odd, also Löcher — Antwort 6), je Ring Punkte mit optionalen Bezier-Griffenin/outals Offset vom Punkt undclosedja/nein (offen = nur Kontur — Antwort 4), dazu Füllfarbe, Deckkraft und optional Kontur (Antwort 5). Die beiden Griffe liegen getrennt statt gespiegelt: was „glatt" heißt, ist Sache des Editors, nicht des Modells. Leinwandbreite, -höhe und Filter sind Properties (Inspector-Zeile und Undo wie jede andere), die Formen sind intrinsische Daten wie die Frames einer Textur.Die Revision, die an #185 geht, ist ein Fingerabdruck des Gezeichneten, kein Zähler — genau deshalb nahm der Schlüssel dort einen String-Token: kein Command kann vergessen, sie hochzuzählen. Der Name einer Form steckt bewusst nicht drin, Umbenennen ändert kein Pixel.
Sechs Undo-Commands in
vectorArtCommands.ts(Form hinzufügen, entfernen — sie kommt an ihre alte Stelle zurück, und anders als eine Tilemap-Ebene darf auch die letzte gehen —, umbenennen, verschieben, Ringe schreiben als ein Schritt pro Geste mit kopierter Liste, Stil setzen, wobeistroke: nulldie Kontur wegnimmt) plus die reine FunktionshapeOrderTargetfür die vier Knöpfe der geplanten Inspector-Liste; sie bedient auch das Ziehen in dieser Liste.Das Laden aus einer Projektdatei ist streng: ein Punkt ohne Zahlen fällt weg, ein kaputter Griff lässt einen Eckpunkt übrig, doppelte Form-Ids fallen weg wie doppelte Layer-Ids, eine Kontur der Breite 0 ist keine Kontur — während ein Ring mit nur einem Punkt, den gerade jemand zeichnet, stehenbleibt.
Drei Funde unterwegs:
JSON.stringifyals Fingerabdruck war falsch: dieselbe Zeichnung kam aus der Projektdatei als{points, closed}zurück und wurde als{closed, points}gebaut — ein Reload hätte dasselbe Bild unter neuem Schlüssel neu gerastert. Der Fingerabdruck schreibt seine Felder jetzt in fester Reihenfolge, ein Test misst genau diesen Fall.toNumberinserialize.tsmacht aus jedem Unsinn eine 0, dieNumber.isFinite-Prüfungen liefen deshalb ins Leere:{x: 'far'}wurde zur linken Kante statt zu verschwinden. Formen lesen Zahlen streng überasFinite, weil 0 hier ein Ort auf der Leinwand ist.20 neue Unit-Tests in
tests/core/vectorart.test.ts; drei Gegenproben greifen (Fingerabdruck zurück aufJSON.stringify→ 2 fallen, Zahlen wieder lose lesen → 4, Ringliste nicht kopieren → 1). Kein e2e-Check, Check-Summe blieb 1157, nur Modul 04 zählt 19 Asset-Typen statt 18.Etappe 2 — die Rasterung (
6371576)Vector Art ist jetzt ein Bild. Das Asset steht im Bild-Picker, ein Sprite trägt es in seiner Texture-Zeile, und was der Sprite zeigt, hat der Rasterizer gemalt. Registriert wird per Funktion statt per Modul-Seiteneffekt:
installVectorArtPictures(app.renderer)hängt Source (core) und Generator (viewport) einmal pro Seite inApp.tsein — ein Test darf sie also weglassen.Die Regel liegt in
core, gemalt wird inviewport.core/model/vectorPath.tsliefert pro Form reine Zeichenbefehle (move/line/curve/close): geschlossene Ringe füllen, alle Ringe konturieren, ein Segment bleibt eine Linie, solange keines seiner Enden einen Griff trägt — ein einzelner Griff genügt für eine Kurve, der fehlende Kontrollpunkt sitzt dann auf seinem eigenen Ende, damit eine Ecke neben einer runden Seite stehen kann. Der Rückweg zum ersten Punkt wird nur gezeichnet, wenn er gebogen ist; sonst machtcloseihn.Löcher gehen nur über einen Weg: Pixi erkennt sie ausschließlich an einem
GraphicsPathmitcheckForHoles— und prüft dabei Enthaltensein, nicht Even-Odd-Tiefe. Für Ring-in-Ring reicht das; ohne das Flag malt der innere Ring einfach über den äußeren (gemessen, siehe Gegenproben).2× Überabtastung wie im Plan:
RenderTexture.create({resolution: 2})legt 512 px für eine 256er Leinwand auf die GPU,texture.widthbleibt die Leinwandgröße — das ist das Maß, an dem der Sprite hängt. Geclippt wird durch die Rendertextur selbst: was über die Leinwand ragt, ist nicht drin, es braucht keine Maske. Gezeichnete Bilder bekommen denselben Sampler-Zustand wie hochgeladene (setPictureFactory, spiegelbildlich zukeep()).Ein Punkt zum Mitlesen:
renderer.render()läuft innerhalb der Bildanfrage. Das ist hier sicher, weil jeder Aufrufer während seines eigenen Widget-Redraws fragt und der Frame danach auf dem Ticker gezeichnet wird; ein verschachteltesrendermitten in einem laufenden Renderpass wäre es nicht. Die Warnung steht im Modulkopf.Tests
Neues Modul 47 mit 8 eigenen Checks in eigener Lane: das Bild liegt in doppelter Auflösung auf der GPU und misst die Leinwand; der Sprite zeigt die Füllung; der innere Ring ist ein Loch (die Szene scheint durch); die später gezeichnete Form liegt oben; die Kontur läuft am Ring; eine geänderte Zeichnung bekommt einen neuen Schlüssel und die alte wird freigegeben; der Sprite zeigt die neue Farbe. Die Formen legt der Test per
mutateins Modell — der Tab, der sie zeichnet, ist Etappe 3.21 neue Unit-Tests in
tests/core/vector-path.test.ts. Drei Gegenproben greifen: Überabtastung auf 1 → der GPU-Check fällt (pixelWidth: 256);checkForHolesaus → der Loch-Check fällt (#2e8b57statt Hintergrund); den gebogenen Rückweg nicht zeichnen → ein Unit-Test fällt. Die volle Suite lief lokal statt nur in CI, weil die Änderung in derTextureLibraryjedes Bild berührt. Check-Summe 1165, gemessen.Zwei Mess-Fallen, die Läufe gekostet haben:
__gm.objectRect(id)gibt die Mitte des Objekts, nicht die Ecke — der erste Durchgang las vier Pixel neben dem Sprite und meldete Hintergrund, während der Rasterizer korrekt war; entschieden hat das der Screenshot, nicht die Zahlen. Und die Gitterlinien der Szene kreuzen genau im Mittelpunkt, der Loch-Check las#363940und fiel aus dem richtigen Grund falsch; er misst jetzt daneben.Offen
Etappe 3 (Tab: Stift, Auswahl, Griffe,
polygonEdit.tserweitern), Etappe 4 (Inspector: Formenliste mit Vorschau und Reihenfolge, Farbe/Deckkraft/Kontur), Etappe 5 (e2e zum Zeichnen, Wiki-Seite Vector Art).Etappe 3 ist gebaut (Commit
3d7ad65), dazu ein Fix, den der Test im Browser nicht gefunden hat, sondern der Benutzer: das Ziehen eines Punktes hinkte der Maus hinterher (Commit3a68067).Etappe 3 — der Tab
Kopfzeile mit Select / Pen / Grid / Fit, Schachbrett, die Leinwand als Rahmen, darin die Zeichnung — gemalt mit denselben Befehlen wie der Rasterizer (
paintDrawingist dafür ausviewport/vectorArtPicture.tsexportiert), so dass Tab und Bild nie auseinanderlaufen können.Die Regeln sind in
polygonEdit.tsgewachsen, kein zweites Modul — wie im Plan (Antwort 7). Zwei Punkte daraus sind mehr als Fleißarbeit: ein Punkt auf einer gebogenen Kante spaltet die Kurve (de Casteljau) statt sie abzuschneiden, beide Hälften zeichnen zusammen genau das, was vorher da war; und eine gebogene Kante wird abgelaufen, nicht über ihre Sehne gemessen, sonst greift ein Klick auf die Kurve ins Leere. Eine Geste ist ein Undo-Schritt (Vorschau schreibt direkt, beim Loslassen wird der alte Stand zurückgelegt und das Kommando ausgeführt) — dasselbe Muster wie bei den Frame-Umrissen aus #141. Neu nötig war eine Auswahlartshapeinstate.ts.Tests:
check.sh127 Dateien / 1878 Tests,ALL PASSED – 1172 checks, 18 neue Unit-Tests intests/editor/ring-edit.test.ts, 7 neue e2e-Checks in 47c/47d.Ein Fund, der festgehalten gehört: eine Gegenprobe, die keine war. Das Schließen des Stifts mit
* 0entschärft ließ den Lauf grün — zu Recht, denn der schließende Klick landet auf exakt der Bildschirmkoordinate von Punkt 0, die Distanz ist 0 und<= 0ist wahr. Erst eine unerfüllbare Bedingung schaltete wirklich ab (2 Checks fallen). Dazu: einfalse &&vor einem Guard bricht das TS-Narrowing, der Build steigt aus, und zwei Läufe gingen gegen das alte Bundle.Der Fix: Ziehen hält jetzt mit der Maus Schritt
Gemeldet als „aktualisiert erst, wenn ich die Maus stehen lasse". Das ist ein echter Rückstau, kein Gefühl — gemessen mit einem Wegwerf-Modul, das 120 Maus-Events im 8-ms-Takt schickt (125 Hz):
preview()Die 120 Moves dauerten vorher 4173 ms statt der gesollten 960 — der Rest war Rückstau in der Eingabeschlange, genau das Symptom.
Ursache: Jede Mausbewegung schrieb das Modell und feuerte
state.preview()→emit('project'), und daran hängt die ganze Editor-Kette (Play-Sync, Invalidieren des Editor-Laufs, alle Dokumente und Panels, dazu das Neurastern des gezeichneten Bildes). Das ist Arbeit für einen Frame, nicht für jedes Event.Fix:
onFrame(key, fn)insrc/ui/render.tssammelt solche Arbeit pro Schlüssel und führt sie im Ticker vorapp.render()aus — danach wäre sie einen Frame zu spät im Bild. Der Canvas stellt impointermovenur noch ein und räumt seinen Platz beim Loslassen (cancelFrameWork), weil dann das Kommando den Endstand schreibt. Während der Geste ist die eigene Ring-Kopie die Wahrheit, das Modell wird nie zurückgelesen, es geht also nichts verloren.Tests:
check.sh127 Dateien / 1883 Tests, volle Suite lokal (weil die Änderung im Render-Loop sitzt):ALL PASSED – 1172 checks, Summe unverändert — ein Zeitcheck im Browser wäre unter CI-Last wackelig, der Beleg ist die Node-Messung. 5 neue Unit-Tests intests/ui/render.test.ts. Zwei Gegenproben greifen: die Arbeit nach dem Zeichnen laufen lassen lässt einen Unit-Test fallen, das Coalescing im Canvas ausbauen bringt die Messung zurück auf 36 ms / 4153 ms. Vom Benutzer im eigenen Projekt nachgeprüft.Nicht angefasst: Szenen-Viewport, Texture-Tab und Graph rufen
preview()genauso im Move-Handler und haben dieselbe Kostenstelle. Das liegt außerhalb des gemeldeten Fehlers;onFrameliegt allgemein inui/und wäre ein Kandidat für die nächste Refactoring-Runde (#85).Offen
Etappe 4 (Inspector: Formenliste mit Vorschau, Reihenfolge-Knöpfe, Farbe/Deckkraft/Kontur) und Etappe 5 (Wiki-Seite Vector Art, restliche Doku). Das Issue bleibt offen.
Etappe 4 ist gebaut, committet und gepusht (
78595ec) — das Issue bleibt offen, Etappe 5 (Wiki-Seite Vector Art und restliche Doku) folgt direkt.Was jetzt da ist
Eine ausgewählte Form hatte bisher die Zeilen ihres Assets — Farbe, Kontur und Reihenfolge einer Form waren also nur über die Projektdatei erreichbar. Jetzt zeigt der Inspector SHAPES (die Liste), die vier Reihenfolge-Knöpfe, VECTOR SHAPE (Name, Fill, Opacity, Outline; mit Kontur zusätzlich Line colour und Line width), Delete shape und Add shape. Wählt man das Asset, steht dieselbe Liste unter seinen Zeilen, ohne dass eine Form gewählt ist. Die Leinwandgröße bleibt eine gewöhnliche Property am Asset.
shapeDrawing+paintDrawing) in einen 22-px-Swatch, die ganze Leinwand hineinskaliert — die Zeile sagt also auch, wo auf der Leinwand die Form sitzt. Ein Punkt, der im Tab gezogen wird, zieht die Vorschau mit.previewingHandlers, beim Loslassen einSetVectorShapeStyleCommand. Die Kontur wird per Häkchen hinzugefügt oder weggenommen, nicht unsichtbar gemacht; Linienbreite 0 ist keine Kontur, das Kommando lehnt sie ab, die Zeile klemmt auf 0,1.Tests
tests/editor/shape-rows.test.ts: die Arithmetik der Liste (Reihenfolge, Ziel einer abgelegten Zeile) und die echten Zeilen (welche Zeilen mit und ohne Kontur, Fill als ein Undo-Schritt, Opacity geklemmt, Häkchen gibt und nimmt die Kontur, Signatur, Refresh).cmpzurückgeprüft und neu gebaut.47-shape-rows): Zeilen mit Vorschau, ausgewählte Zeile markiert, Knöpfe, Fill#20c0e8, Häkchen, Line colour/width.Drei Funde aus dieser Etappe
locate('Outline')zielt daneben. Der Helfer gibt die Mitte des Label-Textes, und „Outline" ist breiter als „Fill" — ein fester Versatz landet mal im Feld, mal im Nichts. Richtig ist__gm.inspectorField(label), der die Mitte des Bedienelements kennt.20c0e8landete als#e8c02020c0e8im Hex-Feld,parseHexgab null, nichts passierte — undtypeTextwirft dabei nicht, weil es nur prüft, ob der Text enthalten ist. Jetzt geht ein Ctrl+A voraus.Doku
Wiki
c473762: Architecture (shapeSection,ShapeList,shapeRowssamt der Regel, warum die Liste auf dem Kopf steht), Development (Check-Summe). README-Check-Summe 1179 — gemessen, nicht gerechnet. Die eigene Seite Vector Art gehört laut Plan in Etappe 5.Offen bleibt Etappe 5: die Wiki-Seite Vector Art und die restliche Doku.
Etappe 5 ist fertig — #129 ist damit durch
ad62a0f(Repo) und21bea26(Wiki).Der Nutzerweg ist jetzt gemessen
Die Etappen 1 bis 4 haben die Formen per
mutateins Modell gelegt, weil der Tab, der sie zeichnet, erst Etappe 3 war. Block 47f geht jetzt den Weg, den du gehst:+-Menü bietet Vector art unter den Typen, die nur in der Bibliothek leben; der Eintrag legt ein Asset an, ausgewählt und im Panel stehend.Ein Fund: Der Picker zeigte drei Zeilen, wo ich mehr erwartet hatte. Der Grund ist in Ordnung — ein frisches Fixture hat eine Textur und die Zeichnung, „None" ist die dritte. Der Check nennt jetzt alle drei, statt sie zu zählen: geht eine verloren, sagt er welche.
Die Doku
Vector Art war in Stücken dokumentiert — Textures trug das erzeugte Bild, Architecture die Module, Keyboard den Tab. Es gibt jetzt eine eigene Wiki-Seite Vector Art, die vom ersten Klick bis zum fertigen Sprite durchliest: Asset und Leinwand, die zwei Werkzeuge, was ein Griff tut, wie Ringe ein Loch machen, die Formenliste samt Reihenfolge — und die Grenzen, die eigene Issues sind (#186, #187, #188, #189). Sidebar und Home tragen die Seite; Textures und Tiles verweisen darauf, statt es ein zweites Mal in eigenen Worten zu beschreiben.
Tests
Die Unit-Tests sind seit Etappe 4 unverändert — Etappe 5 ist Browser-Beleg und Doku, keine neue Regel. Beide Gegenproben greifen: keinen Erzeuger registrieren → der Picker-Check fällt; den Tab ohne das Asset öffnen → der Doppelklick-Check fällt. Screenshot angesehen.
Was #129 insgesamt gebracht hat
cb1804dbdb3f0663715763d7ad653a6806778595ecad62a0fDie Check-Summe ist von 1157 auf 1184 gewachsen, die Unit-Tests von 1799 auf 1897.
Offen bleibt bewusst: Verläufe (#186), boolesche Operationen (#187), Collider aus einer Form (#188), SVG-Import (#189), Gruppen und Layers (#190), ein eigenes Panel statt Inspector (#191). Laufzeit-Nodes sind nach deiner Antwort 10 nicht angelegt.
Auf den CI-Lauf zu
ad62a0fhabe ich nicht gewartet — erst der bestätigt die Zahlen oben auf einer fremden Maschine.