Move the todo notes into the Gitea issue tracker and drop the folder

All 50 notes are now issues #1-#50 in meleki/game-maker, open ones open
and finished ones closed. Every issue keeps the note verbatim and adds a
footer with the commit it was filed in, the commit that finished it and
who raised it; the 37 notes the user wrote are posted as `admin`, the 13
I raised as `meleki`. Label `todo-note` marks the whole batch.

todo-notes/ is gone - the tracker is the backlog from here on. README
says where they live now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-12 15:08:25 +02:00
1 parent 3ed8b68ecf
commit 2c0c5eb99f
53 files changed
+6 -1867

No files matched your search

+6
View File
@@ -885,6 +885,12 @@ he walks on the spot – a clock that ticks without the picture following would
The workspace folder itself is what nginx serves, so `npm run build` is the deployment.
### Ideas, gaps and bugs
They live in the issue tracker of the Gitea repo: <https://git.nockal.com/meleki/game-maker/issues>.
The folder `todo-notes/` that held them as files until 2026-09-12 is gone; all 50 notes were moved
into issues #1–#50, closed ones included, each carrying the commits it was filed and finished with.
## API
All routes go through `api/index.php?r=…` (the site's nginx does not pass `PATH_INFO`).
-7
View File
@@ -1,7 +0,0 @@
# todo-notes
A drop box for ideas that come up while work is already in progress, so they are not lost and not
squeezed into the running task. One file per topic, free form, any language.
Anything in here is fair game to pick up at any time. When a note is done, move the file to `done/`
rather than deleting it – the note says what was wanted, the commit says what was built.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
-18
View File
@@ -1,18 +0,0 @@
Angeblich kann man Assets per Doppelklick in die Scene/Container platzieren. Das funktioniert aber nicht.
Falls es irgendwie implementiert ist gehört es sowieso raus => Das Doppelklick Feature brauchen wir nicht!
---
**Umgesetzt (2026-09-09).** Raus, wie gewuenscht.
Es *war* implementiert: `ListView` meldete einen Doppelklick (und Enter) als `onActivate`, und
`AssetPanel.activate` platzierte damit in die Mitte der sichtbaren Ansicht — der Weg, den ich in
`asset-drag-and-drop.md` noch als "bleibt bestehen" beschrieben hatte. Warum er bei dir nichts
tat, habe ich nicht weiter verfolgt: das Feature faellt ohnehin weg.
Entfernt sind damit auch die Teile, die nur dafuer existierten: `AssetPanel.place`, das
`PlacementTarget`-Interface samt `Editor.placementTarget`, und die beiden `worldCenter()`-Methoden
in `ContainerEditor` und `SceneViewport`, die sonst tot im Baum stehengeblieben waeren.
**Geblieben:** Doppelklick (oder Enter) auf ein **Szenen**-Asset setzt es weiterhin als aktive
Szene — das ist kein Platzieren, sondern derselbe Befehl wie im Kontextmenue.
-30
View File
@@ -1,30 +0,0 @@
momentan funktioniert das platzieren eines asset in der scene/container per "Place in ..." über das kontextmenü.
zukünftig soll es per drag and drop passieren
---
**Umgesetzt (2026-09-09).** Drag and Drop ersetzt den Menueintrag vollstaendig — auf deine
Ansage hin ist "Place in scene"/"Place in container" aus dem Kontextmenue verschwunden.
Doppelklick auf die Zeile platzierte zunaechst weiterhin in die Mitte der Ansicht — auf deine Notiz
`asset-doppelklick.md` hin ist auch das entfernt.
- **Ziehen:** die Zeile im Assets-Panel wird nach 4 px Weg zum Drag; ein Ghost mit dem
Asset-Namen haengt am Cursor (`src/ui/drag.ts`). Die `ListView` erkennt die Geste nur und
gibt sie per `onDragOut` nach aussen ab — sie kennt kein Ziel ausserhalb ihrer selbst.
- **Vorschau:** ueber einer Ansicht zeigt ein Umriss die **Groesse**, die das Objekt haben wird,
plus Fadenkreuz auf dem Ursprung. Rot = der Drop wird abgelehnt.
- **Ziel:** die Ansicht unter dem Zeiger, nicht der aktive Tab. Scene-Tab → aktive Szene,
Container-Tab → offene Vorlage. `ContainerCanvas` erbt das von `SceneViewport`.
- **Regel:** `placementRejection` (`src/core/model/placement.ts`) — dieselbe Funktion, die
`PlaceAssetCommand` ausfuehrt. Die Vorschau kann also nichts versprechen, was der Drop
danach verweigert. Heute lehnt sie ab: Szenen und Container, die in sich selbst landen.
**Zwei Fallen, die beim Bauen erst sichtbar wurden:**
1. Ein Drag beginnt mit einem **Linksklick**, und der selektiert die Zeile — der Container-Tab
folgte der Selektion und haette sich mitten im Drag auf genau den Container umgeschaltet,
der gerade gezogen wird. Das Ziel waere unter dem Zeiger weggerutscht und jeder Drop haette
sich selbst verschachtelt. `followSelection` haelt jetzt still, solange ein Drag laeuft.
Vorher schuetzte davor der Rechtsklick, der nicht selektiert; mit Drag&Drop trug das nicht mehr.
2. Ein Drag endet oft dort, wo die Zielansicht **nichts davon mitbekommt** (ueber einem anderen
Panel, ausserhalb des Canvas). Ohne `onDragChange` waere die Vorschau stehengeblieben.
-19
View File
@@ -1,19 +0,0 @@
beim build kommt immer folgender output:
```
../dist/assets/index-CPWPKF3g.js 578.97 kB │ gzip: 180.43 kB │ map: 2,777.11 kB
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
```
- was bedeutet das?
- kann man das "fixen"?
---
Antwort (User-Wahl: nichts ändern):
- Die Meldung ist nur ein Hinweis von Vite, kein Fehler. Sie erscheint bei jedem Chunk über 500 kB (minifiziert).
- Das Hauptbundle (590 kB) besteht zur Hälfte aus Pixi (300 kB: Grafik, Text, Container, Renderer, also fast nur Benutztes) und zur Hälfte aus eigenem Code (287 kB, davon Editor 130, Core 97, UI 40). Code-Splitting per `import()` bringt nichts, weil der Editor beim Start alles braucht.
- „Fixen“ ginge über einen eigenen Pixi-Chunk (`manualChunks`, beide Teile unter 500 kB, Pixi bleibt nach einem Deploy im Cache) oder über eine höhere Warngrenze (`chunkSizeWarningLimit`). An der Gesamtgröße ändert beides nichts.
- Größerer Hebel: nginx liefert JS unkomprimiert aus, weil `gzip` in der Konfiguration des Web-Stacks aus ist (590 kB statt 184 kB mit gzip, gemessen). Das Modul `gzip_static` ist im Container vorhanden.
@@ -1,92 +0,0 @@
ein toggle bei einem behavior object in einer scene (passender name fehlt mir noch):
- wenn der toggle aktiviert ist, soll das behavior außerhalb des play modes trotzdem laufen (sprich immer)
- das behavior ist natürlich nicht aktiv wenn die scene nicht geladen ist wo sich das behavior befindet (sprich genauso wie im play mode, nur ohne play mode)
---
**Umgesetzt (2026-09-09).** Der Schalter heisst **`Run in editor`** und steht im Inspector eines
Behavior-Objekts direkt unter `Enabled`. Ist er an, laeuft das Behavior weiter, waehrend das
Projekt *nicht* spielt (`src/editor/EditorRun.ts`, vom Editor neben der Play-Session getickt).
## Die eine Entscheidung, die der Schalter erzwingt
Play arbeitet auf einer **Wegwerf-Kopie** — deshalb ist Spielen fuer das gespeicherte Projekt
gefahrlos. Ausserhalb von Play gibt es diese Kopie nicht, und damit stand die Frage im Raum, was
ein laufendes Behavior ueberhaupt anfassen darf. Entschieden wurde (Rueckfrage an den User) fuer
die **Unity-Semantik** (`ExecuteInEditMode`):
> Was das Behavior bewegt oder schreibt, ist eine **echte Aenderung am gespeicherten Modell**.
> Sie wird autosaved, die Panels zeigen sie — und **Undo holt sie nicht zurueck**.
Das macht den Schalter maechtig (ein Behavior kann Inhalt bauen oder anordnen) und ist genau der
Grund, warum er **standardmaessig aus** ist. Der E2E-Check haelt beide Haelften fest: der Server
liefert die verschobene Position aus, und nach zwei Undos ist der Schalter weg, die Bewegung nicht.
## Was der Editor-Lauf bewusst *nicht* bekommt
| | |
|---|---|
| **Eingaben** | Tastatur und Maus bleiben beim Editor, sonst wuerde Tippen in einem Textfeld Behaviors ausloesen. `On key …`, `On click` und `Is key down` feuern hier nie; `On start`, `On update`, `On timer` und Kollisionen schon. |
| **Container-Inhalt** | `syncFromModel` schreibt den Inhalt platzierter Container als normale Objekte aus (*baking*) und loescht dabei deren `instanceEdits` — im Editor waere das eine stille, irreversible Aenderung. Der Lauf instanziiert deshalb mit `bakeContainers: false`. **Preis:** ein Behavior *innerhalb* eines platzierten Containers laeuft ausserhalb von Play nicht. |
| **ein zweites Leben** | Nur die Behaviors der **geladenen** Szene laufen, und nur im Edit-Modus. Play stoppt den Editor-Lauf — dort laeuft ohnehin jedes Behavior. Eine Exception im Graphen oder ein `Stop game`-Node beendet den Lauf und schreibt den Grund in die Notice-Zeile, statt ihn 60× pro Sekunde zu wiederholen. |
Der zweite Punkt der Notiz faellt damit von selbst heraus: die Welt wird aus der aktiven Szene
gebaut, ein Behavior einer anderen Szene hat gar keine Entity. Beim Szenenwechsel wird der Lauf
neu aufgebaut oder verworfen.
## Kein Save-Sturm
Ein Behavior, das jeden Frame etwas verschiebt, macht das Modell jeden Frame schmutzig — 60×
pro Sekunde speichern waere die naheliegende Katastrophe. Zwei Dinge verhindern das:
- `syncToModel()` meldet jetzt, **ob** ueberhaupt geschrieben wurde (`{structure, values}` statt
eines blossen `boolean`). Ein Behavior, das nur liest oder wartet, macht gar nichts schmutzig.
- `setSaveStatus('dirty')` emittiert nur bei **Wechsel**, der Autosave-Timer wird also nicht jeden
Frame neu gestellt. Gemessen: **2 Speicherungen in 2 Sekunden**, nicht ~120.
## Ein Darstellungsfehler, den erst der Screenshot gezeigt hat
`schemaForAsset` liefert fuer ein Behavior `COMMON_PROPS + Typschema + Variablen`. Der
Object-Inspector zeigte alles ausser `enabled` unter der Ueberschrift **VARIABLES** — der neue
Schalter landete also dort, wo nur steht, was das Behavior *deklariert*, und die Notiz „The
behavior declares no variables." verschwand fuer immer. Beides ist repariert: die Typschema-Zeilen
eines Behaviors stehen jetzt bei `Enabled`, die Sektion darunter fuehrt wieder nur die Variablen.
Aufgefallen ist es nur, weil ich mir den Screenshot angesehen habe — die Checks waren zufrieden.
## Ein Messgeraet statt eines geratenen Offsets
Der erste Anlauf klickte die Checkbox ueber `locate('Enabled').x + 55`. Das trifft nur Zeilen,
deren Label in die 64-px-Spalte passt — `Run in editor` ist breiter und schiebt seine Checkbox
11 px nach rechts, also ging der Klick daneben und fuenf Checks fielen um. Neu ist deshalb
`__gm.inspectorField(label)`: es liefert die Bildschirmmitte des **Controls** einer Inspector-
Zeile, gefunden ueber sein Label. Damit ist jeder kuenftige Inspector-Check unabhaengig von der
Labelbreite.
## Belegt
Acht Unit-Tests (`tests/editor/editor-run.test.ts`, plus einer in `container.test.ts` fuer das
Nicht-Backen). Gegengeprueft: dreht man den Filter auf „alle Behaviors" und die Szenen-Bedingung
weg, fallen **vier von sieben** um; mit `bakeContainers: true` faellt der Container-Test.
Dazu zehn E2E-Checks in Modul 17:
```
PASS a placed behavior does not run outside play mode until it is asked to
– {"running":false,"time":0,"behaviors":[]} x=0
PASS the inspector offers the switch on a behavior object, with Enabled and above the variables
– {"enabledBox":{"x":1070,"y":221},"runBox":{"x":1081,"y":253},"variables":{…,"y":509.5}}
PASS switching it on is an ordinary override on the object – {"runInEditor":true}
PASS the behavior runs although Play was never pressed
– {"running":true,"time":0.46,"behaviors":["er-logic"]} mode=edit
PASS the owner drifts on, and the moved object is the stored one – x 48.33 → 108.32
PASS what the behavior moved is saved, not just shown – stored x=308.31 (started at 0)
PASS saving stays debounced while it runs, instead of once per frame – 2 saves in 2 s
PASS taking the switch back stops the run and the object stands still – x=329.98
PASS undo takes back the switch, never the movement it caused – overrides={} x=351.65, started at 0
PASS the step leaves nothing of its own behind
```
Der Schritt baut sich seine eigene Kiste samt Behavior (`On update → Move(owner)`) statt den
Mover der Fixture zu nehmen — der laeuft ueber die Pfeiltasten, und die sieht der Editor-Lauf
absichtlich nie. Am Ende raeumt er alles wieder weg.
Screenshot `36-editor-run`.
-21
View File
@@ -1,21 +0,0 @@
beim expand und pack node habe ich mir eigentlich auch vorgestellt, dass man sich eine struct variable auswählt und automatisch die felder erzeugt werden.
wir können das so lassen wie es ist (eigene felder definieren), aber eine helfer auswahl von structs das die felder erstellt wäre gut.
---
**Umgesetzt (2026-09-10).** `From variable ▾` steht in der Kopfzeile des Port-Editors neben
`+ Field ▾`; das Menü listet die Struct-Variablen des Behaviors und legt deren Felder als Ports an.
Drei Entscheidungen, die die Notiz offen ließ:
- **Es ergänzt nur, es schreibt nichts um.** Ein Feld, das der Node schon deklariert, bleibt stehen –
es könnte eine Kante tragen. Ist nichts mehr zu ergänzen, ist der Eintrag deaktiviert.
- **Eine Liste aus Structs zählt mit** (`shapes (item)`): sie deklariert die Form *jedes Items*, also
genau das Bündel, das ein `Pack` für sie baut.
- **Ein Color-Feld wird ein Zahlport.** `FIELD_KINDS` ist *keine* Teilmenge von `DECLARED_PORT_KINDS` –
der Graph kennt keine Farbe, im Wertemodell ist sie eine Zahl.
Zwei Fallen unterwegs, beide gemessen: `AddPortCommand` legt genau **einen** Port an, „alle Felder auf
einmal" wäre sonst in fünf Undo-Schritte zerfallen – daher `AddPortsCommand`, das **alle** Schlüssel
prüft, bevor es einen anhängt (sonst bliebe nach einer Kollision die halbe Liste stehen).
Und ein Struct-Feld darf `exec` heißen, ein Port nicht; die Fehlermeldung sagt jetzt auch warum.
-63
View File
@@ -1,63 +0,0 @@
Der E2E-Check `pixel-art filtering keeps the border between two quadrants hard` faellt
**sporadisch** um — am 2026-09-09 zweimal in vier Laeufen, jeweils ohne dass eine Aenderung
Texturen beruehrt haette.
- gruen: `["#e63c3c","#e63c3c","#3cc85a","#3cc85a","#3cc85a"]` — harte Kante, `nearest` greift
- rot: `["#c65641","#a67047","#868a4c","#66a552","#3cc85a"]` — weicher Verlauf, also `linear`
## Erledigt: der Folge-Check deckt nichts mehr zu
Der zweite Check (`switching the filter back to smooth …`) verglich nur `soft.some(c => !pure(c))`
und meldete damit auch dann gruen, wenn **beide** Filter denselben Verlauf zeigten — ein haengender
Wechsel liess genau einen Check fallen, der zweite verdeckte ihn.
Er verlangt jetzt zusaetzlich, dass sich das Bild gegenueber `nearest` messbar geaendert hat
(Summe der Farbabstaende ueber die fuenf Punkte > 150). An den oben aufgezeichneten Reihen
gerechnet: echter Wechsel 425, haengender 0 — und 30 selbst dann, wenn jeder Kanal um zwei
danebenlaege. Beide Checks legen ausserdem `__gm.textureFilter(datei)` in ihren Fehlertext.
## Das Messgeraet
`__gm.textureFilter(datei)` liest **beides**: was das Modell will (`scaleMode`) und was der Treiber
tatsaechlich weiss (`gl.getTexParameter` an der hochgeladenen Textur). Ein FAIL beantwortet damit
von selbst, was bisher Deutungssache war:
| Messung | Bedeutung |
|---|---|
| `scaleMode` linear, Bild weich | das Modell hat nie umgeschaltet |
| `scaleMode` nearest, `gpu` linear | der Wechsel erreichte den Sampler nicht — Produktfehler |
| beide nearest, Bild trotzdem weich | der Test misst falsch |
Es bindet die Source ueber `renderer.texture.bindSource` statt per rohem `bindTexture` — sonst
verfaelscht die Messung Pixis Vorstellung davon, was gebunden ist, und stiftet selbst Flakiness.
## Offen: die Ursache
Vier Hypothesen sind gemessen und **widerlegt** — nicht erneut probieren:
| Hypothese | Messung | Ergebnis |
|---|---|---|
| Der Wechsel erreicht den GPU-Sampler nicht | 30 Wechsel, Modell und `gl.getTexParameter` gelesen | 60/60 einig |
| Der Vorlauf der elf Module hinterlaesst etwas | 15 Runden nach echtem Vorlauf | alle gruen |
| Verlorener GPU-Prozess (Chrome meldete `exit_code=34`) | Kontext gezielt verloren und wiederhergestellt | `nearest` ueberlebt beides, auch mit Redraw |
| Genau **ein** Wechsel auf frischer Seite, wie im Test | 12 Runden, je neuer Seed und Seitenaufbau | alle gruen |
Dass in ueber 60 gezielten Versuchen nichts faellt, waehrend der echte Lauf in ein bis zwei von
vier Laeufen umfaellt, ist der eigentliche Widerspruch.
Naechste Spur bleibt die Umgebung: Chrome meldet beim Start auf der Windows-Maschine
`GPU process exited unexpectedly: exit_code=34`. Wie oft das passiert, ist allerdings **unbekannt** —
`harness.mjs` gibt die Browser-stderr nur bei einer Exception oder bei `RUN DIED` aus, in gruenen
Laeufen also nie. Solange das so ist, laesst sich gar nicht messen, ob der Absturz mit dem
Fehlschlag korreliert. Wer hier weitermacht, protokolliert die stderr zuerst bei jedem Lauf.
Die Sonden dazu waren Wegwerfware und sind geloescht. Wer weitermacht, baut sie neu — beide
waren duenn: die eine fuhr den echten Modul-Vorlauf und danach eine Messschleife aus
Filterwechsel + `__gm.textureFilter`, die andere erzwang ueber `WEBGL_lose_context` einen
Kontextverlust und mass danach dasselbe.
## Warum diese Notiz trotz offener Ursache in `done/` liegt
Abgelegt am 2026-09-09 auf Anweisung. **Erledigt ist der Testmangel**, den die Notiz beschrieb,
plus das Messgeraet — die **Ursache der Sporadik nicht**. Faellt der Check wieder um, ist das hier
der Stand: vier widerlegte Hypothesen und die einzige offene Spur (stderr zuerst protokollieren).
-58
View File
@@ -1,58 +0,0 @@
die möglichkeit das spiel zu pausieren und fortzusetzen
---
**Umgesetzt (2026-09-09).** In der Toolbar steht neben *Play* ein zweiter Knopf, der zwischen
**Pause** und **Resume** wechselt; dasselbe macht **`Ctrl+Enter`** waehrend des Spielens. Gehalten
heisst: die Welt wird **nicht mehr getickt**. Damit stehen Behaviors, Timer und Kollisionen still
— und die Weltuhr steht mit ihnen, was fuer `On timer` der eigentliche Punkt ist.
**Pause ist ein Unterzustand von Play, kein dritter Modus** (`EditorState.paused`). Alles, was
Play von Edit unterscheidet — die Laufzeitkopie, die eigene Undo-Historie, dass nichts gespeichert
wird —, gilt waehrend der Pause unveraendert; ein dritter `EditorMode` haette jede vorhandene
Abfrage `mode === 'play'` zu einer Fallunterscheidung gemacht. Der Zustand wird beim Verlassen von
Play, beim Betreten und beim Laden eines Projekts geloescht: **eine Pause kann ihren Lauf nie
ueberleben**, sonst stuende der Editor still und niemand wuesste warum.
**Eingaben waehrend der Pause werden verworfen, nicht gesammelt.** Ein gehaltener Frame ruft
weiterhin `input.endFrame()` und `world.dropPointerEdges()` auf. Ohne das haetten sich die
Tastatur- und Klick-Flanken der ganzen Pause angestaut und waeren beim Fortsetzen in einem
einzigen Frame losgegangen — aus zehn Sekunden Pause waere ein Sprung geworden, den niemand
ausgeloest hat.
**Warum `Ctrl+Enter` und kein Buchstabe:** waehrend des Spielens gehoert die Tastatur dem Spiel.
Ein `P` oder die Leertaste waere dem Spiel dauerhaft weggenommen. `Ctrl+Enter` startet Play schon
bisher, haelt es jetzt und laesst es weiterlaufen; `Esc` stoppt wie gehabt.
**Belegt** — sieben Unit-Tests (`tests/editor/pause.test.ts`; ohne die Regeln fallen drei davon
um, gegengeprueft) und neun E2E-Checks in Modul 18. Der Test-Hook `__gm.playInfo()` nennt dafuer
neu die **Weltuhr**, sonst waere "steht wirklich still" nicht von "sieht gerade gleich aus" zu
unterscheiden:
```
PASS the box drifts on its own while playing, with nothing held down
– x 46.66 → 98.33, {"playing":true,"paused":false,"time":0.4666}
PASS pausing stops the world where it is: the box and the clock both stand still
– x 99.99 → 99.99, clock 1.00 → 1.00 over 1 s
PASS resuming lets it move on from where it stopped, without catching up the held time
– x 99.99 → 141.65, clock 1.00 → 1.40
PASS a key pressed while paused is swallowed instead of firing on resume
– 0 hits before, 0 while held, 0 after resuming
PASS the same key does fire once the game is running again – 0 → 1
PASS stopping clears the hold, so the next play runs
– after stop {"playing":false,"paused":false,"time":0}, x 96.66 → 138.33
```
Der erste Check ist die Voraussetzung des zweiten: ohne nachgewiesene Bewegung waere "steht still"
trivial gruen. Der letzte Check derselbe Gedanke fuer die Tastatur — ohne ihn waere "keine Taste
hat gefeuert" auch dann gruen, wenn der Graph ueberhaupt nie feuert. Die Uhr im dritten Check ist
der Beweis gegen das Nachholen: sie laeuft bei **1.00** weiter, nicht bei den ~3.5, die seit dem
Anhalten vergangen sind.
**Offen geblieben — bewusst.** Ein **`Pause game`-Node** fuer den Graphen (passend zu `Stop game`
und `Restart game`) waere der naechste Schritt, damit ein Spiel sich selbst anhalten kann
(Pausenmenue, Game Over). Ich habe dazu gefragt, aber keine Antwort bekommen, und einen
game-facing Node nicht im Alleingang eingefuehrt: ein Node ist dauerhafte API, und ohne einen
Weg zurueck **aus dem Spiel heraus** waere ein so pausiertes Spiel nur ueber die Toolbar wieder
zu starten. Beides — Node und ein fester Weiter-Weg im Spiel — laesst sich auf dem jetzigen
Stand billig nachziehen: `state.setPaused(true)` ist alles, was ein Node tun muesste.
-73
View File
@@ -1,73 +0,0 @@
In der Game View soll anstelle von "Press Play to run the scene here":
- das aktuelle frame von der kamera dargestellt werden
- gibt es keine aktive kamera, der text "No active camera"
---
**Umgesetzt (2026-09-09).** Der Game-Tab zeigt jetzt auch ohne Play das Bild der aktiven Kamera,
und *"No active camera"*, wenn keine an ist. Der alte Hinweis *"Press ▶ Play to run the scene
here"* entfaellt ersatzlos — er stand genau da, wo jetzt das Bild ist.
Der Tab hat damit zwei Quellen fuer dieselbe Darstellung:
| | |
|---|---|
| **waehrend Play** | die laufende Welt, wie bisher |
| **sonst** | das Modell der aktiven Szene, als stehendes Bild |
Beide werden durch **dieselbe** Kamera gerahmt (`activeCamera.ts`, siehe unten) und durch
dieselbe Zeichenroutine gezeichnet. Editor-Symbole (Kamera, Collider, Behavior, Container-Icon)
fallen in der Vorschau genauso weg wie beim Spielen — die Vorschau zeigt das Spiel, nicht den
Editor.
## Zwei reine Funktionen statt einer Regel an zwei Stellen
`src/viewport/activeCamera.ts` beantwortet **welche Kamera und wo**, fuer beide Quellen:
- `cameraOfItems(items)` — die Modell-Kamera. `collectHolderItems` liefert bereits
Hierarchie-Reihenfolge mit aufgeloester Schalterkette, also ist "die erste eingeschaltete"
hier schlicht der erste Treffer. Damit ist es **dieselbe Regel** wie `World.activeCamera`,
nicht eine zweite Formulierung davon.
- `cameraOfWorld(world)` — die laufende Kamera, **an ihrer Weltposition**.
## Ein Fehler, den erst die Vorschau sichtbar gemacht hat
Der zweite Punkt ist kein Detail: `x`/`y` einer Entity sind **relativ zum Elternteil**. Der
Game-View hat sie bisher direkt als Kamerastandort benutzt. Eine Kamera, die unter einem Objekt
haengt (der uebliche Weg, sie einem Spieler folgen zu lassen), wurde deshalb an der falschen
Stelle gerahmt — der Elternversatz fehlte. Aufgefallen ist es, weil die Modellseite die
Weltposition liefert und die Vorschau damit ein **anderes** Bild gezeigt haette als Play. Der
Unit-Test haelt genau diesen Unterschied fest: die Entity meldet `{x: 20, y: 10}`, gerahmt wird
bei `{x: 320, y: 210}`.
## Die Vorschau kostet nichts, solange man sie nicht sieht
Sie erfordert ein Einsammeln der ganzen Szene. Ohne Bremse liefe das bei **jeder** Auswahl-
oder Modelaenderung mit, auch waehrend der Scene-Tab offen ist — vorher tat der Game-View in
dem Fall gar nichts. `redraw()` steigt deshalb aus, solange das Widget unsichtbar ist; das
Einschalten des Tabs laeuft ohnehin ueber ein `relayout`, das per `resize` neu zeichnet.
## Belegt
Vier Unit-Tests (`tests/viewport/active-camera.test.ts`) — drei davon fallen um, wenn man die
Schalter-Bedingung oder die Weltposition entfernt (gegengeprueft). Dazu sechs E2E-Checks in
Modul 03. Der staerkste vergleicht Vorschau und Play direkt, damit "es rendert irgendwas" nicht
als Vorschau durchgeht:
```
PASS the game tab previews the scene through the active camera, without playing
– {"rendering":true,"visibleItems":2,"camera":"o3"}
PASS and the crate is really drawn there, not left as letterbox – #e0553f
PASS and it is the picture Play produces: same camera, same items, same pixel on the crate
– {…,"camera":"o3"} vs {…,"camera":"o3"}, #e0553f vs #e0553f
PASS switching the camera off empties the preview and names the reason
– {"rendering":false,"visibleItems":0,"camera":""}
PASS and undo brings the preview back without a reload
```
Der Pixel-Vergleich ist der Kern: dieselbe Farbe an derselben Bildschirmstelle wie im laufenden
Spiel. Der vierte Check liest zusaetzlich ueber `__gm.locate('No active camera')` nach, dass der
Text wirklich auf dem Schirm steht — `rendering: false` allein waere auch bei einem schwarzen
Kasten ohne Text erfuellt.
Screenshots `10c-game-preview` und `10d-no-active-camera`.
@@ -1,8 +0,0 @@
graph während dem playmode lässt sich nicht editieren.
- abklären was zu machen wäre um das zu ermöglichen
- abklären ob es möglich wäre aktuelle werte anzuzeigen (nicht nur den wie der flow läuft sondern auch was)
---
Umgesetzt (User-Wahl): Änderungen im Play wirken ab dem nächsten Frame und werden beim Stop mit verworfen, wie alles im Play. Ein neuer `On start` feuert für ein schon gestartetes Behavior nicht mehr.
Werte: vor jedem Daten-Ausgang der zuletzt gelieferte Wert, 10× pro Sekunde erneuert; bei mehreren Instanzen zählt die mit dem neuesten Wert.
@@ -1,29 +0,0 @@
1. panning per linksklick drag soll raus, dafür beim mittelklick (aber egal ob man ins leere klick oder sich über einem node befindet) = wie bei scene view
2. mehrere nodes gleichzeitig auswählbar um dadurch mehrere zu bewegen oder zu löschen. auch mit rect-auswahl selektierbar = auch wie bei scene view
3. was anderes: wenn ich in der graph view auf "fit" klicke, dann fittet es nicht wirklich (es befinden sich nodes halb außerhalb)
---
**Umgesetzt (2026-09-09), alle drei Punkte.**
1. **Panning nur noch mit der mittleren Taste** — und zwar von ueberall, auch wenn der Zeiger
ueber einem Node steht: der NodeView laesst den Mittelklick bewusst durchblubbern, statt ihn
wie jeden anderen Klick zu schlucken. Shift+Links auf leerer Flaeche pannt zusaetzlich, genau
wie im Scene-View. Die linke Taste pannt gar nicht mehr.
2. **Mehrfachauswahl**: Ziehen auf leerer Flaeche spannt ein Gummiband auf, das jeden **beruehrten**
Node auswaehlt; Ctrl/Shift-Klick nimmt einzelne dazu oder raus; das Ziehen eines Nodes der
Auswahl bewegt alle; `Delete` (und das Kontextmenue) loescht alle — jeweils als **ein**
Undo-Schritt (`CompoundCommand`).
Bewusster Unterschied zum Scene-View: dort entscheidet die *Position* eines Objekts, hier die
*Ueberschneidung* mit dem Rechteck. Nodes sind grosse Kaesten — ein Band, das sichtbar quer
durch einen Node geht, muss ihn fangen.
3. **Fit fittet wirklich.** Die alte Fassung setzte den Zoom hart auf `1` und *zentrierte* nur —
war der Graph groesser als die Ansicht, hingen Nodes danach zwangslaeufig heraus. Ausserdem
rechnete sie mit einer *geschaetzten* Node-Hoehe (80) fuer alle, obwohl echte Nodes je nach
Portzahl deutlich hoeher sind. Jetzt wird jeder Node mit seiner **gezeichneten** Groesse
gemessen und der Zoom so gewaehlt, dass alles hineinpasst (nie ueber 1 hinaus vergroessert).
Die Geometrie dahinter liegt als reine Funktion in `src/editor/graph/framing.ts`
(`boxesInBand`, `frameBoxes`), nicht im Panel — genau diese Rechnung war der Bug aus Punkt 3 und
war vorher von keinem Test erreichbar. Die Mehrfachauswahl selbst sitzt im Selektionsmodell
(`nodeSelection`/`toggleNode`, symmetrisch zu den Szenenobjekten), nicht im Canvas.
@@ -1,5 +0,0 @@
eine simple animation die den flow symbolisiert. vielleicht edge circles die durch fließen, jedes mal wen ein event ausgelöst wird oder so?
mach vorschläge wie das dargestellt werden könnte.
---
Umgesetzt (User-Wahl): Perlen auf Exec-Kanten, 0,3 s pro Durchlauf, höchstens eine alle 0,15 s je Kante. Laufen auf Spielzeit, stehen also in der Pause.
@@ -1,14 +0,0 @@
man kann ja nun graph nodes zusammenklappen.
hier soll auch die breite veringert werden auf die titeltext breite wenn möglich?
---
EDIT:
speziell für "Get variable" node, "Set variable" node und der zukünftige "Variable" node:
Es soll in der titelbar vom node der variablennamen (falls gesetzt!) angezeigt werden (egal ob zusammengeklappt oder nicht, ist unabhängig davon)
hier die beispiele:
- "Get dummyX" (beim node "Get variable")
- "Set dummyX" (beim node "Set variable")
- "dummyX" (beim node "Variable")
-19
View File
@@ -1,19 +0,0 @@
beim graph bräuchte ich einen toggle der grid snapping aktiviert.
alle vorhandenen nodes soll da bleiben wo sie sind, erst wenn man ein node bewegt soll es greifen
die grid-größe kannst du selbst entscheiden, am besten ein sinnvoller wert
---
Gebaut. Zwei Dinge, die weder oben stehen noch aus dem Commit hervorgehen:
**Die Rastergröße ist 24 px – dieselbe Zahl, die den Hintergrund zeichnet** (`GRID_STEP`). Es gibt
also keinen zweiten, unsichtbaren Raster: was einrastet, rastet auf einer Linie ein, die man sieht.
Bei starkem Herauszoomen blendet das Raster aus (`MIN_GRID_STEP`), der Snap-Schritt bleibt aber
derselbe – er hängt an Weltkoordinaten, nicht am Zoom.
**Bei Mehrfachauswahl richtet sich jeder Node für sich aus** (Nachtrag des Users während der
Arbeit). Die erste Fassung verschob die Auswahl um *einen* gemeinsamen Versatz, was ihre alten,
krummen Abstände mitschleppte: nur der Ankerknoten landete auf einer Linie. Jetzt ist `dragOffset`
pro Node definiert – jeder bekommt seinen eigenen Versatz, die Auswahl fluchtet danach wirklich.
Der Preis ist bewusst: die relativen Abstände innerhalb der Auswahl können sich um bis zu einen
halben Schritt ändern.
@@ -1,28 +0,0 @@
1. wenn ich gerade einen edge dragge und er würde nicht ins leere gehen (sprich ein passender input/ouput gehovered) wird, dann soll der input/ouput gehightlighted werden
2. wenn ich auf das node selbst droppe, soll er das erste freie passende bzw. wenn keines mehr frei ist einfach das letzt passende input/out verbinunden - auch hier gilt die prediction vom highlight (sodass man wärend dem draggen schon sieht welches input/output angeschlossen wird falls man droppt)
EDIT:
3. Wenn ich ins leere droppe, soll sich das create node feld aufgehen und nach passenden nodes filtern die kompatibell sind (wenn von output weg, dann passende nodes vorschlagen die einen passenden input haben und umgekehrt)
---
**Umgesetzt (2026-09-09), alle drei Punkte.**
1. Der Port, auf dem die Kante landen wuerde, bekommt waehrend des Drags einen weissen Ring
(Theme-Token `portTarget`), und die gezogene Linie schnappt darauf — die Vorhersage ist damit
nicht nur farbig, sondern auch geometrisch eindeutig.
2. Drop auf den Node-Koerper nimmt den ersten **freien** passenden Port; ist keiner mehr frei, den
letzten passenden, dessen Kante dann ersetzt wird (genau wie ein Drop auf den Port selbst).
Der Ring zeigt waehrend des Drags schon genau diesen Port.
3. Drop ins Leere oeffnet die Palette, gefiltert auf die Node-Typen, die diese Kante ueberhaupt
erreichen kann (vom Output aus: passender Input, vom Input aus: passender Output; `exec` passt
nur zu `exec`, `any` zu jedem Datentyp). Der gewaehlte Node kommt fertig verdrahtet an.
Die Entscheidung "was passt hier zusammen" sitzt komplett in `src/core/graph/dropTarget.ts` (rein,
getestet); der Canvas zeichnet sie nur. Zwei Dinge, die beim Bauen dazukamen:
- **Node und Kante gehen als ein `CompoundCommand` rein.** Zwei getrennte Kommandos haetten fuer
eine Geste zwei Undos gebraucht.
- **`firstPortFor` statt `dropTargetPort` beim Palette-Weg**: der neue Node ist noch gar nicht im
Graph, `dropTargetPort` koennte ihn also nicht finden. Er hat noch keine Kanten, also ist der
erste passende Port genau der, den `dropTargetPort` spaeter waehlen wuerde.
@@ -1,7 +0,0 @@
- Scene: Add collider button aus insepctor entfernen
- Graph: Delete node button aus inspector entfernen
Umgesetzt: beide Aktionen bleiben erreichbar, nur nicht mehr im Inspector.
- Collider: Rechtsklick auf das Objekt in **Scene objects** → *Add collider*. Das ist jetzt der
einzige Weg; seine Sperre (Template-Inhalt, platzierter Container) prüft e2e 09 über das Menü.
- Node: `Delete` oder Rechtsklick auf den Node → *Delete node*.
-46
View File
@@ -1,46 +0,0 @@
1. kamera enabled auf false setzen bewirkt nichts, wird trotzdem gerendert
2. wenn mehrere kammeras in der scene sind, dann soll die erste gefundene die enabled ist die aktive kamera sein
---
**Umgesetzt (2026-09-09).** Beide Punkte waren dieselbe Stelle: der Game-View nahm die Kamera
mit `world.firstOfType('camera')` — die **erste ueberhaupt**, ohne den Schalter anzusehen. Ein
ausgeschaltetes `enabled` konnte deshalb nichts bewirken (Punkt 1), und eine zweite Kamera kam
nie zum Zug, egal wie die Schalter standen (Punkt 2).
An ihre Stelle tritt **`World.activeCamera()`** — die einzige Stelle, die entscheidet, durch
welche Kamera gespielt wird:
- **geschaltet** zaehlt wie bei jedem Objekt die ganze Kette nach oben (`isHandleEnabled`): eine
Kamera unter einem ausgeschalteten Container ist ebenfalls aus.
- **"erste"** heisst in **Hierarchie-Reihenfolge**, also von oben nach unten im Hierarchie-Panel,
nicht in Speicherreihenfolge. Die beiden koennen auseinanderlaufen, sobald eine Kamera unter
einem Elternteil haengt, das spaeter gespeichert ist — dann gewinnt das, was der User sieht.
- **keine an → gar nichts wird gerendert**, wie bei einer Szene ohne Kamera. Der Hinweis beim
Start sagt jetzt in beiden Faellen dasselbe: *"No camera is switched on – nothing is rendered"*.
Ausgewertet wird pro Frame, also wirkt ein Umschalten waehrend des Spielens sofort — auch das
per `Set enabled`-Node aus einem Graphen heraus. Das ist der Weg, wie eine Szene die Ansicht
von einer Kamera an die naechste uebergibt.
`firstOfType` ist damit unbenutzt und geloescht.
**Belegt** — vier Unit-Tests (`tests/core/camera.test.ts`, jeder faellt ohne den Fix um) und
sieben E2E-Checks in Modul 03. Der Test-Hook `__gm.gameInfo()` nennt dafuer neu die **objectId
der aktiven Kamera**, sonst waere "durch welche Kamera" nur zu erraten:
```
PASS a switched-off camera renders nothing although it is still in the scene
– {"rendering":false,"visibleItems":0,"camera":""}
PASS the switched-on camera takes over, and the game is seen through it
– {"rendering":true,"visibleItems":0,"camera":"m8ngyi2lkc"}
PASS with both switched on the first camera in hierarchy order is the active one again
– {"rendering":true,"visibleItems":2,"camera":"o3"}
```
Der mittlere Check ist der eigentliche Beweis fuer Punkt 2: die zweite Kamera steht bei
x = 5000, die beiden Kisten liegen am Ursprung. `visibleItems: 0` bei `rendering: true` heisst,
dass wirklich durch **ihren** Ausschnitt gesehen wird — der blosse Name der Kamera haette auch
dann gestimmt, wenn das Bild von der alten Kamera gekommen waere.
Screenshot `10b-second-camera`.
@@ -1,9 +0,0 @@
Kann man einen quasi 2 in 1 node machen "Variable" - dieser soll Get/Set gleichzeichtig können
- kommt ein exec flow rein, dann wird gesetzt (vom input port - wert oder edge)
- get gibt es quasi immer, natürlich der neue wert falls er gesetzt wird
---
Umgesetzt: der Ausgang liest die Variable im Moment des Abrufs, nicht beim Schreiben.
`Get variable` liest dagegen weiter einmal pro Puls — auf dieselbe Technik umstellen hiesse,
bestehende Spiele im Verhalten zu aendern (Tetris liest das Brett deshalb zweimal).
-104
View File
@@ -1,104 +0,0 @@
## Nested Behavior / Behavior als Node in Graph
Was damit gemeint ist?
=> Ein Behavior Asset als Node innerhalb eines Graph
Grundsätzlich soll so ein Behavior Node im Graf mit einem Execution Flow ausgestattet sein, sprich wir brauchen einen Start/End Node:
- Start Node ist ähnlich wie ein Event Node mit exec output port und dort soll man auch ouput "variablen" festlegen können (quasi wie variablen)
- End Node ähnlich, hat als Input ein exec port + inputs die als output gelten, wenn man es als node in einen anderen graphen eines behavior assets einbindet.
wenn alles verstanden, direkt loslegen, ansonsten warte ich auf rückfragen :)
---
## Umgesetzt
Verstanden war alles — gebaut ist genau die beschriebene Form. Drei neue Node-Typen:
| | |
|---|---|
| **`Start`** | exec-Ausgang plus die Eingaenge, die das Behavior von aussen bekommt. Was hier deklariert wird, ist ein **Eingang des aufrufenden Nodes** |
| **`End`** | exec-Eingang plus die Werte, die zurueckgehen. Was hier deklariert wird, ist ein **Ausgang des aufrufenden Nodes** |
| **`Run behavior`** | der Aufruf selbst. Seine Ports gibt es nicht in seiner Typdefinition — sie werden aus Start- und End-Node des Ziels **abgeleitet**, bei jedem Zeichnen neu |
Deklariert wird im Inspector des Start-/End-Nodes: `+ Input` bzw. `+ Output`, Art waehlbar
(Number, Boolean, Text, Object), Name direkt editierbar, `×` zum Entfernen — dieselbe Bauform
wie die Variablen eines Behaviors.
**Der Node traegt den Namen des Behaviors**, nicht "Run behavior", und die Palette listet
Behaviors ebenfalls namentlich. Ein Aufruf ist damit ein Griff statt Node einfuegen + Dropdown.
## Die eine Entscheidung, die du mir ueberlassen hast
Ein eingebettetes Behavior hat kein Objekt in der Szene — also auch keinen Ort fuer seine
Variablen. Ich habe **(A) reine Funktion** gewaehlt: jeder Aufruf startet die Variablen auf den
Asset-Defaults, rein geht es ueber den Start-Node, raus ueber den End-Node.
Der Grund ist dein eigener naechster Schritt: du willst den Tetris-Graphen zerlegen. Dabei
sollen die Teile *rechnen*, waehrend der Zustand (`grid`, `pieceX`, …) im Haupt-Behavior
bleibt — genau das, was du mit "kann man ueber input/output loesen" gemeint hast. (A) ist
ausserdem die konservative Wahl: daraus laesst sich spaeter Zustand machen, umgekehrt nicht,
ohne bestehende Graphen zu brechen.
**Die Tuer ist nicht zu:** Zustand gibt es weiterhin ueber ein Behavior-Objekt in der Szene
(`Object by name` → `Get/Set property`) — der Weg, den dein Jump and Run fuer den Punktestand
schon geht. Lokale Variablen im eingebetteten Graphen funktionieren als Zwischenspeicher
innerhalb eines Aufrufs unveraendert; sie werden nur am Ende verworfen.
## Was ich selbst entschieden habe
| | |
|---|---|
| **Ein Start, ein End** | ein zweiter machte "welche Ports hat dieses Behavior" mehrdeutig. Der Command lehnt ihn ab, die Palette bietet ihn nicht mehr an |
| **Selbstaufruf = Fehler** | mit Namen im Log (`Behavior "X" runs itself`), statt still in die Tiefenbegrenzung zu laufen. Die Palette bietet das eigene Behavior gar nicht erst an |
| **`self`/`owner` bleiben** | der eingebettete Graph sieht dasselbe Objekt wie der Aufrufer — sonst waeren `Self`/`Owner` darin wertlos |
| **Toter Verweis ist still** | ein Node ohne gewaehltes Behavior (oder eines ohne Start-Node) tut nichts und **laesst den Fluss weiterlaufen**, statt den Lauf abzubrechen |
## Zwei Dinge, die der Bau erzwungen hat
**Eigener Pulse-Cache pro Aufruf.** Node-Ids sind nur *pro Graph* eindeutig. Liefe der
eingebettete Graph im Cache des Aufrufers, koennte er dessen Werte ueberschreiben. Der
Interpreter arbeitet deshalb auf **Frames** — der Aufruf schiebt einen neuen auf und nimmt ihn
danach wieder weg. Dafuer gibt es einen eigenen Test, der eine Id-Kollision erzwingt; ohne
Frames liefert er 99 statt 7.
**Ports aufraeumen ueber Graphgrenzen.** Einen Port umzubenennen oder zu entfernen betrifft
nicht nur den eigenen Graphen, sondern **jeden anderen, der das Behavior aufruft**. Beides
raeumt dort mit auf — und ein Rename nimmt den eingetippten Inline-Wert mit, sonst faellt er
still auf den Default zurueck. Das ist mir erst beim Gegenpruefen aufgefallen: der Test stand
auf 0 statt auf 6.
**Live-Highlight:** der Trace eines laufenden Behaviors deckt jetzt auch die Graphen ab, die es
als Node aufruft — sonst bliebe ein eingebettetes Behavior beim Zusehen dunkel.
## Belegt
**Zehn Unit-Tests** (`tests/core/nested-behavior.test.ts`) und **elf E2E-Checks** in Modul 19.
Der staerkste ist der erste: das aufrufende Behavior rechnet **nichts** selbst — die
Geschwindigkeit, mit der die Kiste laeuft, *ist* der zurueckgegebene Wert.
```
PASS a behavior used as a node runs, and what it returns is what drives the caller
– moved 200.0 px in ~1 s – 2 × 100 means the returned value arrived, 0 would mean nothing came back
PASS the calling node grows the ports the other behavior declares
PASS and it is drawn under that behavior's name, not as "Run behavior" – title="Double"
PASS adding an input declares it on the start node – [{"key":"in1",…},{"key":"in2",…}]
PASS and the node in the other graph grew that input without being touched
PASS undo removes it in both graphs – [{"key":"in1","kind":"number"}]
PASS picking a behavior in the palette drops in a node already set to it
– {"type":"RunBehavior","params":{"behavior":"nb-double"}}
```
`200.0` statt `100` beweist, dass der **verdoppelte** Wert ankam, nicht bloss irgendeiner;
`0` waere "nichts kam zurueck". Die drei Port-Checks in Folge sind der Nachweis, dass die
Portliste wirklich *abgeleitet* und nicht kopiert ist: deklariert wird im einen Graphen,
gewachsen ist der Node im anderen, ohne dass er angefasst wurde.
Gegenprobe gefahren: ohne die frische Variablenkopie und ohne die Selbstaufruf-Sperre fallen
zwei Unit-Tests; ohne getrennte Frames faellt der Kollisionstest (`expected 99 to be 7`) — der
war beim ersten Versuch faelschlich gruen und musste geschaerft werden, bevor er etwas taugte.
| | |
|---|---|
| volle E2E-Suite, remote | **ALL PASSED – 379 Checks**, 0 FAIL |
| `npm run check` lokal | gruen, **524 Tests** |
@@ -1,109 +0,0 @@
Ein paar Fragen zu behavior als node in einem graph:
- wenn es keinen Start oder End node gibt, kann man trotztdem das behaviour in einem graph hinzufügen, aber wie läuft dann der flow? (vorallem wenn es keinen start node gibt)
- ohne end node: woher weiß der parent graph (behavior) dass das child (sub behavior) "fertig" ist?
- drag and drop vom asset inspector wäre auch gut als spwan im graph, ist das möglich?
- doppelklick auf node (type behavior) um von dem asset behavior den graph zu öffnen wäre noch gut
---
## Beantwortet und umgesetzt
Deine ersten zwei Punkte waren Fragen, die letzten zwei Wuensche. Die Antworten standen schon
im Code — der Mangel war, dass **du sie nicht sehen konntest**. Genau das ist jetzt behoben.
### 1. Ohne Start-Node: wie laeuft der Flow?
**Gar nicht — der Aufruf wird uebersprungen, und der Fluss laeuft am `exec`-Ausgang weiter.**
Ohne Start-Node gibt es keinen Eingang in den Sub-Graphen; der Interpreter bricht den Aufruf ab
(`if (!start) return {}`) und macht beim Aufrufer weiter. Dasselbe gilt, wenn gar kein Behavior
gewaehlt ist oder das gewaehlte geloescht wurde. Das ist Absicht: ein toter Verweis soll kein
Spiel abbrechen.
**Das war aber still, und still ist hier falsch.** Es ist derselbe Fehlgriff wie damals beim
`Run in editor`-Schalter: der Node sah gesund aus und tat nichts. Ein Aufruf, der nichts tun
*kann*, sagt es jetzt selbst — rote Zeile auf dem Node, roter Rand, dieselbe Zeile im Inspector:
| Lage | was der Node sagt |
|---|---|
| kein Behavior gewaehlt | `No behavior picked` |
| Behavior geloescht | `That behavior is gone` |
| Behavior ohne Start-Node | `No start node – does nothing` |
| Behavior zeigt auf sich selbst | `A behavior cannot run itself` |
Einfuegen kann man ein Behavior ohne Start-Node weiterhin — das ist der normale Weg beim Bauen
(erst den Aufruf setzen, dann den Sub-Graphen fuellen). Es sieht nur nicht mehr fertig aus.
### 2. Ohne End-Node: woher weiss der Parent, dass das Child fertig ist?
**Die Frage stellt sich nicht: ein Aufruf ist immer synchron.** Der aufrufende Node kehrt erst
zurueck, wenn der Fluss im Sub-Graphen ausgelaufen ist — "fertig" ist kein Signal, sondern das
Ende der Ausfuehrung. Der End-Node hat damit nichts zu tun; er entscheidet nur, **welche Werte**
zurueckkommen. Ohne ihn kommt schlicht nichts zurueck, alles andere ist unveraendert.
Deshalb wird ein fehlender End-Node auch **nicht** als Mangel angezeigt — das waere eine
Warnung vor etwas Erlaubtem. Ein eigener Test haelt das fest: der Sub-Graph loggt `sub`, der
Aufrufer danach `after`, und das Ergebnis ist `['sub', 'after']` — nicht umgekehrt und nicht
verschraenkt.
### 3. Drag & Drop aus dem Asset-Panel in den Graphen
**Gebaut.** Ein Behavior aus der Asset-Liste in den Graphen gezogen wird zu einem Aufruf-Node,
und zwar **dort, wo du losgelassen hast** (unter dem Zeiger zentriert, nicht rechts-unter ihm).
Waehrend des Ziehens zeigt ein Umriss die Landestelle. Ins **eigene** Behavior gezogen wird
abgelehnt — mit Meldung, statt einen Node zu erzeugen, der beim Laufen einen Fehler wirft.
Das war billig, weil der Mechanismus schon existierte: `startDrag` feuert im Asset-Panel
laengst fuer jedes platzierbare Asset, und die Szenen-Ansicht ist bereits ein Drop-Ziel. Neu
ist nur, dass der Graph-Canvas jetzt auch eines ist.
### 4. Doppelklick auf einen Behavior-Node oeffnet dessen Graph
**Gebaut.** Doppelklick auf einen Aufruf-Node zeigt den Graphen des Behaviors, das er ausfuehrt.
Auch das nutzt vorhandene Mechanik (`pressCount`, dieselbe Erkennung wie in den Listen); der
Node waehlt einfach das Asset aus, und der Graph-Tab folgt der Selektion.
Zusammen mit Punkt 1 schliesst sich damit ein Kreis: der Node sagt *"No start node"*, ein
Doppelklick bringt dich in den Graphen, und dort legst du den Start-Node an.
## Belegt
```
PASS a behavior dragged from the asset panel becomes a call node
PASS and it lands under the pointer, not at some default spot
PASS a behavior without a start node can be added just the same
PASS and the node says why it does nothing, instead of failing silently – "No start node – does nothing"
PASS a healthy call carries no such warning
PASS dropping a behavior into its own graph is refused, out loud
PASS double-clicking a call node opens the behavior it runs
```
Gegenprobe gefahren: ohne die Diagnose fallen beide neuen Unit-Tests. Der zweite war beim
ersten Versuch wertlos — er wollte zeigen, dass der Node **neu gezeichnet** wird, wenn der
Mangel entsteht, prueft das aber an einem Behavior mit Ports, wo sich die Signatur ohnehin
aendert. Jetzt haengt er an einem Behavior ganz ohne Ports, wo sich wirklich nur der Mangel
aendert.
| | |
|---|---|
| volle Suite, remote | **ALL PASSED – 387 Checks** (379 + 8 neue), 0 FAIL |
| `npm run check` lokal | gruen, **527 Tests** (+3) |
## Ein Fehlschlag, der zweimal dasselbe war
Der erste Drag klappte, die beiden folgenden nicht — und zwar **nur im vollen Lauf**, allein
war das Modul gruen. Ich habe zuerst die Drop-Punkte verdaechtigt (falsch: sie lagen in der
Flaeche) und dann die Reihenfolge der Module (falsch: die letzten drei reichten nicht zum
Reproduzieren). Erst eine Messung in der Check-Nachricht hat es gezeigt: die Asset-Zeile lag
bei **y = 449** statt 293 — nach 18 Modulen ist die Liste so lang, dass die zuletzt
angehaengten Behaviors **aus dem sichtbaren Bereich gescrollt** waren. Der Druck landete auf
dem Panel dahinter, es startete gar kein Drag, und das sah exakt aus wie eine Ablehnung.
Die Testdaten liegen jetzt am **Anfang** der Liste. `__gm.dragPayload()` als neuer Hook
unterscheidet die beiden Faelle kuenftig direkt: *abgelehnt* hat einen Drag, *nie angekommen*
keinen.
Ein Reparaturversuch am Test-Hook selbst (`rowGlobal` sollte `null` liefern, wenn der
Scroll-Container die Zeile abschneidet) habe ich **wieder zurueckgenommen** — meine Pruefung
ueber die Masken-Bounds lieferte `null` auch fuer klar sichtbare Zeilen und hat den Lauf
sofort im ersten Modul zerlegt. Lieber ein Hook, der schweigt, als einer, der falsch redet.
-15
View File
@@ -1,15 +0,0 @@
Möglichkeit Nodes zuklappen können, wichtig: soll auch gespeichert werden.
---
Gebaut. Zwei Dinge, die weder oben stehen noch aus dem Commit hervorgehen:
**Zugeklappt ist reine Darstellung, wie ein versteckter Link.** Der Graph bleibt derselbe,
`collapsed` am Node wird nur gespeichert, und ein aufgeklappter Node hinterlässt keine Spur in der
Datei. Weil ein zugeklappter Node keine Ports zum Greifen hat, verbindet ein darauf fallengelassener
Link nichts. Neue Kanten gibt es erst nach dem Aufklappen, statt einen Port zu raten.
**Versteckte Links teilen sich am zugeklappten Node eine Marke pro Seite statt pro Port.** Alle
Ports einer Seite liegen dort auf einem Punkt. Mit einer Marke pro Port stapelten sich Stummel,
Punkt und Name auf denselben Pixeln (gemessen: zwei Marken mit je `links: 1`, identische
Koordinaten).
@@ -1,360 +0,0 @@
## Dynamische Portzahl bei Nodes ermöglichen
Wenn das möglich ist, umsetzen. Folgende Dinge wären dafür praktisch:
### 1. sequence node
diese hat momentan fixe 3 outputs, diese könnten somit ebenfalls dynamisch sein
### 2. eine variable vom typ struct
sprich eine variable die mehreren properties (key mit allen bisher möglichen variablen typen + value).
dazu passende nodes um von einer variable alle properties zu expanden und umgekehrt (alle werte eines structs setzen)
## variablen vom typ struct und list im inspector
bei typ list: momentan steht es einfach nur als string geparsed in einem textfeld.
für die liste bräuchten wir eine view, sprich liste mit variablen.
für typ struct: hier wäre natürlich auch eine passende view gut, wo man bei der variable type + key + default value angeben kann.
---
lass uns das mal ausarbeiten. wie weit das sinnvoll bzw. möglich ist.
---
**Umgesetzt (2026-09-10), Teil 1 und der Werttyp. Was offen bleibt, steht unten.**
**Dynamische Ports gab es schon** — nur nicht aus Node-Einstellungen. Verschachtelte Behaviors
leben seit jeher davon: `GraphNode.ports` speichert eine Deklaration **pro Node**, `resolveDef`
loest den Def pro Node auf, `portSignature` ist der Ausloeser, an dem View und Inspector merken,
dass sich eine abgeleitete Portliste geaendert hat. Die Regel dafuer stand als Dreier-Switch auf
Node-Typen in `nested.ts` — die einzige Stelle im Projekt, die Node-Typen namentlich kannte.
Der Switch ist jetzt **aufgeloest** in zwei Vertraege am `NodeDef`:
- `declaresPorts: 'inputs' | 'outputs'` — der Node traegt seine eigene Portliste, editierbar im
Inspector. Das haben `Start`, `End` und neu `Pack`/`Expand`.
- `derivePorts(source)` — der Node *rechnet* seine Ports aus sich und dem Graphen aus, in dem er
sitzt. Das haben `Sequence` (aus den eigenen Kanten) und `Run behavior` (Ports des benannten
Behaviors).
**Sequence** hatte zuerst eine Einstellung *Outputs* (2…26). Die ist **wieder weg** — siehe den
Nachtrag unten: die Ports wachsen jetzt mit den Kanten. Die Keys bleiben `a`, `b`, `c`, … statt
`out1..N`, weil im Jump-and-Run zwei Nodes an einer Sequence haengen und deren Kanten heil bleiben
sollen.
**Die einzige echte Falle war das Schrumpfen.** `SetNodeParamCommand` war ein reiner Bag-Schreiber:
verschwand ein Port, blieb seine Kante liegen — unsichtbar (`drawEdge` steigt bei unbekanntem Port
still aus), aber sie ueberlebte jedes Speichern und waere beim Hochzaehlen wieder angesprungen.
Der Befehl ist jetzt port-bewusst und nimmt solche Kanten mit, Undo gibt sie zurueck. Das schliesst
**einen bestehenden Fehler mit**: derselbe Verwaisungs-Effekt trat schon immer auf, wenn man an
einem `Run behavior`-Node das Behavior wechselte.
**Struct als echter Werttyp (Weg C).** `PropertyValue` ist jetzt
`number | string | boolean | ListValue | StructValue`. Felder sind Primitive oder Listen, **nie**
wieder ein Struct — aus demselben Grund, aus dem eine Liste flach ist.
Zwei Dinge waren dafuer noetig, und beide sind der eigentliche Preis von C:
1. **Ein Objekt auf einer Datenkante hiess bisher zwangslaeufig Entity.** An drei Stellen in
`values.ts` bedeutete „Objekt, kein Array" ein Entity-Handle. `EntityHandle` traegt jetzt eine
**Symbol-Marke** (`ENTITY_HANDLE`) — ein Symbol, damit kein Struct-Feldname sie imitieren kann,
und es serialisiert nie, was fuer etwas richtig ist, das nur waehrend des Spiels existiert.
Der Typechecker hat jede betroffene Stelle selbst benannt.
2. **`String(value)` haette einen Struct als `[object Object]` geschrieben** — an **39** Stellen.
Die gehen jetzt alle ueber `textOf()`. Das war kein Stilfehler, sondern eine echte Luecke, die
erst der neue Typ sichtbar gemacht hat.
Dazu ein **gemessener, lebender Fehler**, der dabei aufflog und mitrepariert ist: der `List`-Node
verlor seinen Wert beim Speichern. `normalizeGraph` liess bei Node-Params nur Primitive durch,
Arrays fielen weg (`[3,1,4]` → `undefined` nach Round-Trip). Listen-*Variablen* waren nicht
betroffen — die haben ein Schema. Params haben keins, weil sie Einstellungen und Inline-Werte
mischen.
**Belegt** (`tests/core/dynamic-ports.test.ts`): ein Bündel faehrt ueber **eine** Kante durch ein
verschachteltes Behavior — `Pack{x:30,y:12}` → `Run behavior`-Port vom Typ `struct` → `Expand` →
`Add` → 42. Genau die Form, die Tetris heute von Hand parallel fuehrt (`Next move`: 8 rein,
7 raus; 49 deklarierte Ports ueber 9 Behaviors).
**Offen, bewusst nicht gebaut:**
- **Variablen vom Typ struct** (Teil 2 der Notiz). Dafuer braucht eine Variable eine editierbare
Feldliste im Inspector — die Deklaration liegt bisher nur am Node (`Pack`/`Expand`) und am
Behavior-Port. `writeTyped`/`mergeStruct` sind schon darauf vorbereitet: ein Struct-Slot behaelt
seine Felder, was ankommt fuellt die benannten, der Rest bleibt stehen.
- **Die Views im Inspector** (Teil 3). *[Die Listen-Haelfte ist inzwischen gebaut — siehe den
Nachtrag „der List-Tab steht". Der Struct-Teil ist weiter offen.]* Das Textfeld war nicht nur
Kosmetik: `['red, green', 'blue']` wurde als `"red, green, blue"` geschrieben und las sich als
**drei** Items zurueck — ein Komma in einem Text-Item war nicht darstellbar. Fuer den Struct ist
Zeile-pro-Feld richtig, das sind wenige.
- Ein unverbundener `list`-Eingang zeigt weiterhin ein **Zahlenfeld** (`inlineInputDef` hat keinen
`list`-Zweig), und eine Listen-Variable kann nur Zahlen-Items: `AddVariableCommand` nimmt `items`
gar nicht entgegen.
---
EDIT:
> Eine Liste braucht eher einen **eigenen Editor als Overlay**
ok, eigener tab "List"
dann machen wir es so: es gibt dann einfach statt dem textfeld ein button wo drinnen steht "42 items".
klickt man darauf switch zu Tab "List" und dort wird die liste aller items dargestellt die man bearbeiten kann.
dort soll auch der selbe "renderer" verwendet werden wie beim inspector.
wenn wir dann struct (teil 3) umsetzen, kann es auch eine liste aus struct variablen geben (bitte das bedenken) und
im tab "List" sollen variablen (egal welcher type) im prinzip genauso dargestellt werden wie im inspector
---
## Antwort auf den Edit (2026-09-10): geht, mit drei Auflagen
**Der Tab ist hier besser als das Overlay**, aus einem Grund, der erst beim Nachsehen auffiel: der
Inspector haengt **nicht** im Tab-Bereich, sondern als eigene Spalte daneben (`Editor.ts:156`). Er
bleibt in jedem Tab sichtbar, der Sprung verliert also keinen Kontext.
**„Derselbe Renderer" ist fast geschenkt.** `createField` (`fieldFactory.ts:171`) braucht als
Kontext nur `{ project(): Project }` — keine Inspector-Instanz, keinen Command-Stack. Die Zeile
baut selbst kein Kommando, sie ruft `onInput`/`onChange`. `previewingHandlers` benutzt vom
`InspectorHost` ausschliesslich `host.state`; den Parametertyp zu verengen macht auch die
Undo-Mechanik wiederverwendbar. Ein Struct kommt spaeter an *einer* Stelle dazu: dem
`switch (def.kind)` in `fieldFactory.ts:177`.
**Der Weg in den Tab muss ueber den Zustand laufen, nicht ueber `showTab`.** Kein Tab kennt den
`Editor`; Tab-Wechsel von innen gibt es nur selektionsgetrieben, gebuendelt in `Editor.ts:169-179`.
### Woran der Tab haengt
Die anderen Tabs haengen an einer Asset-Id. Eine Liste ist kein Asset, sondern ein *Slot*:
Asset-Property (`assetId+key`), Objekt-Override (`holderId+objectId+key`) oder Node-Param
(`assetId+nodeId+key`). Also eine explizite Adresse **mit Pfad**, die der Tab behaelt wie die
anderen ihre Asset-Id und die auf „nichts" zurueckfaellt, wenn das Ziel verschwindet
(`AssetTabEditor.refresh()` macht genau das). Der Drill-down spaeter ist derselbe Mechanismus
eine Ebene tiefer.
### Zeile-pro-Item traegt — gemessen
Zeilen echt ueber `createFieldRow` gebaut, mit GPU im Browser, dabei **jeden Frame gescrollt**:
| Zeilen | Aufbau | Display-Objekte | fps |
|---|---|---|---|
| 0 (Grundlinie) | – | 11 | 58 |
| 200 | 15 ms | 2 011 | 51 |
| 300 | 17 ms | 3 011 | 46 |
| 3 000 | 122 ms | 30 011 | 18 |
Dagegen die laengsten Listen im echten Bestand: **tetris 224** (`shapes`), **224** (eine
Variablen-Vorgabe), **200** (`grid`); jumpRun und fixture haben keine. Der reale Hoechstfall liegt
also bei 224 — **kein Windowing noetig**. Bei 3 000 Eintraegen faellt es auseinander, aber nichts
kommt dem nahe.
Der eigentliche Fund steckt in der Aufbau-Spalte: 15 ms pro Rebuild bei 200 Zeilen. Deshalb kam
der Fokus-Schutz zuerst (siehe Nachtrag oben) — ein Rebuild pro Tastendruck waere dort die Regel.
### Was „Liste aus Structs" kostet
Gibt es heute **nicht**: `ListValue = readonly (number | string | boolean)[]` (`Properties.ts:5`),
`FieldValue` schliesst Struct aus (`:8`), `sameValue` verlaesst sich auf „recurses exactly one level
deep" (`:50`), `LIST_ITEM_KINDS` kennt kein `struct`, `coerceListItem` gibt ein Primitiv zurueck.
Das macht `ListValue` und `StructValue` **gegenseitig rekursiv** (beliebige Tiefe, echter
Deep-Compare) und die Items brauchen eine deklarierte Feldform — wieder „eine geteilte Struct-Form
hat keine Heimat" (~29 Dateien fuer einen neuen Asset-Typ). Deshalb: Tab jetzt fuer flache Listen,
aber um einen Pfad herum; die Typerweiterung kommt mit den Struct-Variablen.
Beibehalten: eine **Liste ist homogen** (ein `ListItemKind`), der **Struct** ist das heterogene
Ding. Das Textfeld sollte als Massenbearbeitung im Tab bleiben, sonst geht das Einfuegen einer
Komma-Liste verloren. Der unverbundene `list`-Eingang (heute faelschlich ein Zahlenfeld) loest
sich mit dem Knopf mit.
---
## Nachtrag (2026-09-10): Sequence waechst mit den Kanten, statt gezaehlt zu werden
Das Zahlenfeld *Outputs* war **unbedienbar**: tippen ging nicht, Pfeil rauf/runter ging nicht, nur
das Ziehen mit der Maus. Die Ursache ist allgemein und nicht `NumberField` zuzuschreiben:
`onInput` (jeder Tastendruck, auch jede Pfeiltaste) → `previewingHandlers` schreibt in
`node.params` → `state.preview()` → Event `project` → `Inspector.refreshValues()` → die
`nodeSignature` enthaelt die Portliste, hat sich also geaendert → **`rebuild()` wirft die Zeile
weg, in der gerade getippt wird.** Ziehen ueberlebt das, weil ein laufender Drag seine
`globalpointermove`-Events weiter am alten (zerstoerten) Widget bekommt; Tippen braucht den Fokus,
und den gibt es nach dem Rebuild nicht mehr.
**Diese Falle ist inzwischen zu** (siehe „List-Tab" unten): `refreshValues()` verschiebt einen
faelligen Rebuild, solange der Fokus im Inspector sitzt, und holt ihn nach, sobald das Feld
verlassen wird (`FocusManager.changeHandlers` + `isWithin`). Werte sind waehrenddessen ohnehin
sicher — `TextField.setValue` ignoriert eine laufende Eingabe (`TextField.ts:92`). Damit war der
Fehler auch nie `NumberField` zuzuschreiben.
Statt das Feld zu reparieren, ist es nach Wunsch des Users **ersetzt**: die Sequence haelt immer
**einen freien Ausgang unter dem letzten verbundenen**. Nichts verbunden = 1 Ausgang; den
Ersatz-Ausgang verbinden = ein neuer erscheint; die unterste Kante loesen = schrumpft wieder.
Gezaehlt wird der letzte *verbundene* Port, nicht die Anzahl der Kanten — so kann keine Kante
verschwinden, wenn in der Mitte eine geloest wird (dann bleibt eine Luecke stehen).
Preis dafuer: `derivePorts` bekommt jetzt den **Graphen** statt nur der Params
(`resolveDef(node, graph, lookup)`, ~15 Aufrufstellen, alle hatten den Graphen schon zur Hand),
und `run` bekommt als viertes Argument den **aufgeloesten Def** — anders kann ein Node mit
abgeleiteten Ports nicht wissen, wie viele er hat. `SetNodeParamCommand` bleibt port-bewusst, das
braucht `Run behavior` weiterhin.
Nebeneffekt: die Sequence braucht keinen Param mehr, also auch kein Aufraeumen von Kanten beim
Verkleinern — eine Kante zu loesen *ist* das Verkleinern.
---
## Nachtrag (2026-09-10): der List-Tab steht
**Gebaut wie im Edit gewuenscht:** eine Listen-Property ist im Inspector ein Knopf `N items`;
er schaltet auf den neuen sechsten Tab **List**, wo jedes Item eine Zeile mit *demselben*
Widget bekommt, das der Inspector fuer diese Item-Art baut (`createField` — Zahl, Text, Ja/Nein
oder Farbe). Der Inspector bleibt daneben stehen, der Kontext geht also nicht verloren.
**Der Weg dorthin laeuft ueber den Zustand**, nicht ueber `showTab`: `EditorState.listSlot` +
`openList(slot)` + Event `list`; der Editor hoert darauf und schaltet um. Ein Slot ist eine
**Asset-Property, ein Objekt-Override oder eine Node-Einstellung** — `ListSlot` steht in
`state.ts` neben `Selection`, weil `state.ts` sonst in einen UI-Ordner importieren muesste.
`resolveList(state, slot)` in `src/editor/list/slot.ts` liefert Wert, Item-Art, Titel und die
zwei Schreibwege; die drei Faelle benutzen **exakt die Spec der passenden Inspector-Sektion**,
also dasselbe Kommando, dieselbe Beschichtung, denselben Undo-Schritt.
**Abweichung vom eigenen Vorschlag:** kein `path` in der Adresse. Ein immer leeres Feld, das
niemand liest, ist Spekulation; die Naht, an der ein Pfad spaeter ansetzt, ist die **eine**
Funktion `resolveList` — das ist dann eine Signaturaenderung, kein Umbau. Ehrlicher so.
**Zwei Dinge, die erst die Gegenprobe gezeigt hat:**
- Die Zeilen-Handler duerfen **nicht** auf einer Momentaufnahme der Liste sitzen. Sie ueberleben
einen Commit (der Tab baut nur bei geaenderter *Laenge* neu), also wuerde die zweite bearbeitete
Zeile die erste zuruecksetzen. Gemessen: mit Momentaufnahme faellt genau dieser Test.
- Die Beschichtung in `replace` war **redundant** — alle drei Kommandos laufen selbst durch
`coerceValue`. Raus. In `toModel` ist sie noetig: dort entsteht der Wert, der als *Vorschau*
am Kommando vorbei direkt ins Modell geschrieben wird. Auch das ist jetzt bewacht.
**Nebenbei mitgenommen:** `showTab` schaltete fuenf `visible`-Zeilen von Hand — nicht
typgeschuetzt, ein sechster Tab waere dort still vergessen worden. Jetzt ein
`Record<CentreTab, Widget>`, das jede Auslassung zum Typfehler macht.
Der unverbundene `list`-Eingang zeigt weiterhin ein **Zahlenfeld**: `inlineInputDef` hat keinen
`list`-Zweig, und der Inline-Pfad kann gar keine Liste erzeugen. Der Knopf loest das nicht mit.
**Zahlen stehen mit drei Nachkommastellen** (`30.000`). Das ist Hausentscheidung, kein Versehen:
`formatNumber` zeigt immer alle Stellen, „so a value can never look whole when it is not".
**Weiterhin offen:** Variablen vom Typ struct, die Struct-View im Inspector, und „Liste aus
Structs" (macht `ListValue`/`StructValue` gegenseitig rekursiv — siehe oben).
---
## Nachtrag (2026-09-10): drei Korrekturen am List-Tab
**Kurze Liste baute sich nicht auf** (User: „muss die Fenstergroesse kurz aendern"). Die Ursache
lag **nicht** im Tab, sondern im `ScrollContainer` — und traf jeden seiner Benutzer, auch `Menu`:
`rebuild()` uebergibt die Zeilen-`VStack` an `setContent()`, solange sie noch **leer** ist. Der
leere Inhalt wird ueber `Math.max(size.h, this.h)` auf die **Viewporthoehe** gesetzt. Danach kommen
die Zeilen hinein, und `relayoutContent()` rechnet fuer eine kurze Liste wieder dieselbe Hoehe aus
— `Widget.resize()` sieht keine Aenderung und **ueberspringt `layout()`** (`Widget.ts:41`). Alle
Zeilen bleiben auf dem Ursprung gestapelt. Eine *lange* Liste aendert die Hoehe und faellt deshalb
nie auf; die erste Oeffnung ueberhaupt auch nicht, weil der Tab dann noch `w=h=0` hat.
Gemessen an Tetris, bevor etwas geaendert wurde: `colors` (7) nach `shapes` (224) → alle vier
Zeilen auf `{245,116}`; `grid` (200) → sauber gestapelt; `test` (16) → wieder kaputt.
Fix in `ScrollContainer.relayoutContent()`: wenn die Groesse gleich bleibt, `layout()` selbst
rufen. Die Methode wird genau dann gerufen, wenn der Inhalt sich geaendert hat — sich auf die
Aenderungserkennung von `resize` zu verlassen ist dort grundsaetzlich falsch.
**Die Kommazeile im Fuss ist raus** (User-Wunsch). Damit ist auch der Komma-Fallback in
`fieldFactory` unerreichbar geworden — jede Sektion, die eine Liste tragen kann, liefert
`openList` (`assetSection:135`, `nodeSection:134`, `objectSection:325`). Fallback, `listToText`
und `listFromText` sind geloescht; der Test dafuer ist auf `coerceValue` umgeschrieben statt
gestrichen, sonst haette `coerceListItem` seine Abdeckung verloren (Boolean-Schreibweise, Farbe).
**JSON-Speicherung war bereits da.** Gemessen an der Projektdatei: `props.shapes` ist ein
JSON-Array mit 224 Zahlen, kein Komma-String; die PHP-API ist reines JSON. Das Komma war nur die
*Anzeige* im geloeschten Textfeld. Es bleibt genau eine Stelle, an der ein Komma-Format wirklich
persistiert wird: eine Liste, die in ein **Text**-Property rutscht, wird ueber `textOf()` zu
`"1, 2, 3"` (`Properties.ts:33`, via `coerceValue` kind `text`) — betrifft auch die Ausgabe des
`Log`-Nodes. Der User hat entschieden, das **so zu lassen**; falls eine Liste je verlustfrei durch
ein Text-Feld muss, ist das die Stelle.
---
## Nachtrag (2026-09-10): Variablen vom Typ struct — Teil 2 und 3 sind zu
**Der Werttyp war fertig, die Deklaration fehlte.** `StructValue`, `textOf`, `sameValue`,
`writeTyped`/`mergeStruct` und der Port-Typ `struct` gab es seit dem ersten Teil; `Get`/`Set
variable` haben `any`-Ports, ein Bündel fliesst also ohne Änderung durch den Graphen. Neu ist
allein, dass eine Variable eine **Feldliste** deklarieren kann:
`PropertyDef` bekommt `kind: 'struct'` mit `fields: StructFieldDef[]`, `VARIABLE_KINDS` das
fünfte Kind, `variableDef` einen fünften Parameter.
**Ein deklariertes Feld ist ein Primitiv** (`ListItemKind` — Zahl, Text, Ja/Nein, Farbe), obwohl
`FieldValue` auch eine Liste zulässt. Grund: ein Listen-Feld bräuchte einen verschachtelten
Editor und damit den Pfad in der Slot-Adresse — genau der Schritt, auf den auch „Liste aus
Structs" wartet. Der Werttyp bleibt unverändert, die Erweiterung ist additiv.
**`coerceValue` gibt jetzt `PropertyValue` statt `FieldValue` zurück** — der erste Werttyp, der
kein Feld ist. Gemessen: das bricht **keine** der fünf Aufrufstellen.
### Die UI: Block statt Zeile — und *nicht* der Tab
Der List-Tab entstand, weil hunderte Zeilen in keine Inspector-Zeile passen. Ein Struct ist der
Gegenfall: wenige Felder, die untereinander passen. Also kein siebter Tab, keine zweite
Navigation. Eine Struct-Variable ist ein **Block**:
```
⊞ [piece ] [+ Field ▾] [×]
# [x ] [ 30.000 ] [×]
Aa [tag ] [ tee ] [×]
```
Das ist der Port-Editor, den `Pack` schon hat (`portSection.ts`), eine Ebene tiefer und **mit
Wertspalte**: Typ, Key und Default-Wert an einer Stelle — genau der Wortlaut aus Teil 3 der
Notiz. Ein **platziertes Objekt** sieht dieselben Felder als reine Wertzeilen (`createField`
liefert für `struct` einen Stapel beschrifteter Zeilen): überschreiben ja, deklarieren nein.
### Drei Dinge, die den Ausschlag gaben
**Eine Feldänderung muss jeden Wert erreichen.** `AddStructFieldCommand` &co. fassen die
Deklaration, den Default, den Wert des Behaviors **und jeden Instanz-Override** in *einem*
Kommando an (`structSites`); sonst behält ein gespeicherter Wert eine Form, die seine
Deklaration nicht mehr verspricht. Gegenprobe: ohne die Override-Sites fallen 3 Unit-Tests.
**Kein Retype-Knopf** — der Port-Editor hat auch keinen. Feld löschen und neu anlegen.
**Die Handler dürfen nicht auf einer Momentaufnahme sitzen**, dieselbe Falle wie beim List-Tab:
ein Feld schreibt das *ganze* Bündel, muss es also im Moment der Änderung lesen. Gegenprobe im
Browser: mit Momentaufnahme wird `{"x":42,"tag":"ell"}` zu `{"x":30,"tag":"ell"}` — die zweite
bearbeitete Zeile belebt den alten Wert der ersten wieder.
**Die Signatur trägt jetzt die Felder** (`defSignature`): Felder sind eigene Zeilen, ein neues
Feld muss die Sektion also neu bauen. Ohne das erscheint die Zeile erst beim nächsten Rebuild —
gemessen, genau ein Browser-Check fällt.
**Bewusst nicht gebaut:** „Liste aus Structs", ein Struct-Feld vom Typ Liste und der unverbundene
`list`-Eingang. Die drei stehen als eigene Notiz in `todo-notes/struct-und-listen-restposten.md`.
### Der teuerste Fund lag nicht im Struct
Der Schritt lief **solo grün und in der vollen Suite mit drei Fehlschlägen** — identische Klick-
koordinaten, das Menü ging auf, `Boolean` lag genau unter dem Zeiger, und trotzdem passierte
nichts. Kein Notice, also war es kein `CommandError`, sondern eine **echte Ausnahme**, die das
Menü (`closeThen`: erst schliessen, dann ausfuehren) als „Klick auf den Hintergrund" aussehen
liess.
Halbiert (`GM_STEPS=1-9,22` gruen, `1-10,22` rot) und dann im Fenster abgefangen:
`Cannot convert undefined or null to object`. Die Ursache ist ein **Fixture-Fehler**: Schritt 10
schiebt sein Text-Objekt per `mutate` von Hand in die Szene — **ohne `instanceEdits`**, obwohl
`SceneObject` das Feld verlangt. `overrideSites` laeuft mit `Object.entries` darueber. Mein
`AddStructFieldCommand` ist die erste Stelle, die `overrideSites` aus diesem Zustand heraus ruft.
Das Produkt bleibt unangetastet: geladen wird das Feld von `normalizeObjects` gefuellt, angelegt
von den Kommandos — ein Objekt ohne `instanceEdits` kann echt nicht entstehen. Repariert ist das
Objektliteral im Schritt.
**Lehre fuer den naechsten Schritt:** ein Schritt, der solo gruen ist, ist nicht belegt. Nur der
volle Lauf sieht, was die Vorgaenger hinterlassen.
-83
View File
@@ -1,83 +0,0 @@
das on key down event node bräuchte ein "repeat"
sprich wie wenn man auf der tastatur eine taste länger drückt:
- man drückt die taste, das event wir ganz normal gefeuert
- man hält länger gedrückt, nach einem bestimmten delay, feuert das event nochmal
- hält man weiterhin gedrückt, feuert das event öfter hintereinandert
das ist ein beispiel szenario für tetris links/rechts shape bewegung
ich denk wir brauchen beim "on key down" event zwei properties:
- repeat after (seconds)
- repeat interval (seconds, nachdem das erstmal repeat after aufgerufen wurde)
ist repeat after auf 0 - kein effekt (alles wie bisher)
ist repeat interval auf 0 - jedes weitere frame aufrufen
---
## Umgesetzt
Genau die beschriebene Form: zwei Zahlen-Properties am `On key down`-Node.
| | |
|---|---|
| **Repeat after (ms)** | Haltedauer bis zur ersten Wiederholung. **0 = kein Effekt**, alles wie bisher |
| **Repeat interval (ms)** | Abstand der weiteren Wiederholungen. **0 = jedes Frame** |
Beide Felder sind **Millisekunden** — damit bleiben die ueblichen Werte ganze Zahlen (300, 50)
statt 0,3 und 0,05. Die Uhr dahinter rechnet weiter in Sekunden; umgerechnet wird beim Auslesen
der Node-Properties.
Beide tragen `decimals: 0`: das Inspector-Zahlenfeld zeigt sonst immer drei Nachkommastellen
(`NUMBER_DECIMALS`), also stand dort `300.000`. Der Node-Koerper war davon nie betroffen —
`formatValue` gibt ganze Zahlen bar aus.
Der erste Druck feuert immer sofort und unveraendert — die Wiederholung haengt nur hinten dran.
### Warum das nicht der Auto-Repeat des Browsers ist
Der Browser feuert bei gehaltener Taste selbst wiederholt `keydown`. Diese Ereignisse werden
seit jeher verworfen (`Input.press`: `if (!this.down.has(key)) ... // ignore auto-repeat`), und
das bleibt so: Verzoegerung und Rate sind eine **Betriebssystem-Einstellung des Spielers**,
kein Spielparameter. Die Wiederholung wird stattdessen aus der gemessenen Haltedauer gebaut —
damit ist sie auf jeder Maschine gleich und pro Node einstellbar.
Die Regel selbst steht als reine Funktion in `src/core/graph/keyRepeat.ts`, nicht als Bedingung
im Runtime-Code — deshalb ist sie ohne Browser pruefbar. Die Haltedauer pro Node liegt in
`nodeState`, derselben Ablage, aus der `On timer` seine Uhr bezieht.
### Eine Entscheidung, die ich getroffen habe
Ein sehr kleines, positives Intervall wird auf **10 ms** angehoben — dieselbe Untergrenze, die
`On timer` schon hat. Sonst wuerde ein vertipptes `0,0001` bei einem langen Frame tausende
Aufrufe in einem Bild ausloesen. **0 bleibt davon unberuehrt** und behaelt seine eigene
Bedeutung ("jedes Frame").
### Der Node bleibt schmal
Zwei Zeilen `Repeat after (ms): 0` an *jedem* Tastatur-Node waeren reiner Laerm gewesen — ich habe
mir das Bild angesehen, es war deutlich sichtbar. Beide Zeilen bleiben vom Node-Koerper weg,
solange die Verzoegerung 0 ist; der Inspector zeigt sie immer. Neu dafuer ist `NodeDef.hiddenParams`.
Sobald eine Verzoegerung gesetzt ist, erscheinen **beide** — auch ein Intervall von 0, denn das
ist dort eine Einstellung und keine Abwesenheit.
### Der Beweis
Zwei `On key down` auf **derselben** Taste, einer schlicht, einer wiederholend — ein einziges
Halten misst beide, damit "es hat wiederholt" nicht mit "die Taste kam oefter an" verwechselt
werden kann.
```
PASS a hold shorter than the delay fires once, repeat or not
– plain 0 → 1, repeating 0 → 1 over 120 ms with a 300 ms delay
PASS holding past the delay keeps the repeating node firing, while the plain one counts one press
– plain +1, repeating +12 over 800 ms (press + ~10 repeats expected)
PASS a node with no repeat set carries no repeat rows, the repeating one carries both
– plain ["Key: Right arrow"], repeating [… "Repeat after (ms): 300", "Repeat interval (ms): 50"]
PASS typing a new delay lands on the node – {"key":"ArrowRight","repeatAfter":750,"repeatInterval":50}
PASS the delay typed into the inspector is the one the run uses
– repeating 0 → 1 over 500 ms with the delay raised to 750 ms
```
Der letzte ist der wichtigste: derselbe Graph, nur die getippte Zahl geaendert — und der Node
schweigt. Damit haengt die Laufzeit nachweislich am Inspector-Wert und nicht an etwas anderem.
-19
View File
@@ -1,19 +0,0 @@
override darstellung bei scene objects die werte von assets überschreiben:
- momentan wird es fett dargestellt => stattdessen das label in einer gelb/goldener schriftfarbe darstellen
- auch in der hierarchie der scene/container bitte den namen gelb/gold darstellen, falls es irgendein child/property gibt dass dirty ist
---
**Umgesetzt (2026-09-09), beide Punkte.**
- Inspector: das Label einer abweichenden Property ist jetzt gold (`#f2c84b`) statt fett.
Die Farbe steht als Theme-Token `override` an einer Stelle.
- Hierarchie: der Name wird gold, wenn das Objekt selbst abweicht **oder etwas darunter** —
eine zugeklappte Zeile soll nicht verbergen, dass im Teilbaum etwas geaendert wurde.
Die Logik dazu sitzt in `src/core/properties/deviation.ts` (rein, getestet), nicht im Panel:
`objectDeviates` vergleicht gegen das, dem ein Objekt folgt — bei Container-Inhalt gegen das
**Template**, sonst gegen das **Asset** —, `deviationMarks` traegt die Marke auf alle Vorfahren
hoch. Wichtig dabei: `deviationMarks` bekommt den **gespeicherten** Holder, nicht den
aufgeloesten; in der aufgeloesten Sicht sieht Container-Inhalt wie ein normales Objekt aus und
jede Instanz waere faelschlich gold.
-30
View File
@@ -1,30 +0,0 @@
folgende fehlverhalten, beispiel jump and run projekt:
- wenn ich IM container asset "level" von einem einzigen coin sprite die breite ändere von 24 => 8
- wenn ich dann in der level scene bin und auf das geänderte coin sprite gehe und von 8 => 10 ändere funktioniert es noch
- sobald ich aber auf den original standard wert 24 ändere (oder auf den reset button klicke) dann springt es zurück auf 8 (ist korrekt bei reset, aber wenn ich explizit die ursprünglichen 24 haben will, geht das nicht, weil dieser wert nicht mehr als overwrite gilt)
---
**Umgesetzt (2026-09-09).** Reproduziert im Browser am echten Jump-and-Run-Stand: nach dem Tippen
von 24 stand `{"ov":8}` da, genau wie gemeldet. Deine Deutung war richtig — der Wert galt nicht
mehr als Override und wurde weggeworfen.
**Ursache.** Ein Instanz-Edit wurde geloescht, sobald der Wert dem Template-Objekt gleicht
(`instanceRef.setOverride`). Bei **verschachtelten** Containern ist das Template aber nicht die
Basis: `level` haelt auf seinem `c1`-Objekt einen Edit fuer das darunterliegende `coin-body`
(die 8), und der gewinnt gegen das Template im `coin`-Asset. Traegt dieses Template zufaellig
schon die 24 — was jede Ruecknahme auf den Asset-Wert in einem Container-Editor hinterlaesst,
weil ein gespeicherter Override nie geloescht wird — dann galten 24 als "gleich dem Template",
der Edit fiel weg und der Wert landete auf der Zwischenebene: **8**.
**Fix.** `InstanceTarget` hat jetzt `base`: das Template mit den Edits **jedes Containers
dazwischen** eingewoben. Nur das ist ein Wert, auf den etwas zurueckfallen kann. Alles, was eine
Abweichung beurteilt, benutzt es: `setOverride`/`setName`/`setPosition`, die beiden
`Revert…ToTemplate`-Kommandos und die Gold-Markierung im Inspector. Das **Schreiben** von
"Apply to container" geht weiterhin ins echte Template — dort gehoert der Wert hin.
Bei einer einzigen Ebene sind `base` und `template` identisch, das Verhalten aendert sich dort
nicht. Reset landet weiterhin auf 8 (deine Erwartung), 24 bleibt jetzt als Override stehen.
Drei Unit-Tests in `tests/core/apply.test.ts` halten es fest; zwei davon schlagen ohne den Fix
fehl (gegengeprueft).
@@ -1,37 +0,0 @@
die textfarbe beim asset soll auch in diesem gold dargestellt werden, falls ein property des assets einen override hat.
ist somit relativ simpel: im prinzip genau das checken was im inspector erscheint wenn man das asset auswählt => gibt es wo einen override (gold) dann auch den assetname links im panel gold darstellen.
---
**Umgesetzt (2026-09-09).** Der Name einer Asset-Zeile im Assets-Panel ist gold, solange das
Asset irgendeine Property traegt, die der Inspector gold malen wuerde — dieselbe Farbe und
dasselbe Kriterium wie in der Hierarchie.
Die Regel steht als reine Funktion in `src/core/properties/deviation.ts`, neben dem schon
vorhandenen `objectDeviates`:
- `markedAssetSchema(asset)` — die Zeilen, die der Asset-Inspector als "modified" markiert.
- `assetDeviates(asset)` — ob davon eine von ihrem Schema-Default abweicht (`isModified`).
**Panel und Inspector benutzen dieselbe Liste**, statt die Bedingung zweimal zu formulieren:
`buildAssetSection` baut seine Property-Zeilen jetzt aus `markedAssetSchema(asset)` (das ersetzt
die Fallunterscheidung Behavior/Rest, die dort vorher stand). Damit kann die Liste keine
Abweichung behaupten, die der Inspector danach nicht zeigt.
**Ein Sonderfall, der eine Entscheidung brauchte: die Variablen eines Behaviors.** Sie sind Teil
von `schemaForAsset`, aber der Inspector markiert sie bewusst nie (`setModified` ist dort ein
No-op) — ihr Default ist auf der Variablen selbst deklariert, der Asset-Wert *ist* dieser
Default. Ein Behavior mit `speed = 200` ist deshalb nicht gold. Sonst waere im E2E-Fixture der
"Mover" gold gewesen, ohne dass der Inspector irgendwo etwas markiert haette — genau der
Widerspruch, den die Notiz ausschliesst.
**Belegt** (E2E-Modul 04, sechs neue Checks ueber den Test-Hook `__gm.assetRowColor`, der die
tatsaechliche Label-Farbe der Zeile liest):
- Crate (Hoehe 60 statt 100, eigene Farbe) → `#f2c84b`, Main camera (alles auf Default) → `#e6e8ee`
- Mover (Variable `speed` = 200 gegen deklarierten Default 0) → `#e6e8ee`, wie der Inspector
- ein ↺ zurueckgesetzt → bleibt gold, weil die zweite Abweichung noch steht
- die zweite auch zurueckgesetzt → wird schlicht, **ohne Reload**; Undo bringt die Marke zurueck
Screenshot `15-asset-reset`: im Panel sind "Level 1" (Hintergrundfarbe), "Crate" und
"Crate collider" gold, "Main camera", "Mover", "Spawner" und das frisch angelegte "Sprite" weiss.
-23
View File
@@ -1,23 +0,0 @@
## Eine Refactoring-Runde speziell für Typen:
Typen (ob jetzt "String" oder "Color" oder "Texture") werden vertreut eingesetzt:
Geh alles durch, und refactore so, dass
- wenn ein neuer Typ in Zukunft irgendwann hinzugefügt wird
- oder ein Typ geändert wird
- oder wieder wegkommt
dass es nicht ewig viele Stellen beachtet werden müssen.
Reduziere dass auf ein Minimum, wir brauchen das so "modular" wie möglich.
Falls es schon so passt wie es ist, dann braucht man natürlich auch nichts machen, aber ich will nur auf nummer sicher gehen.
Falls dir bei der Arbeit noch andere Dinge auffallen die man besser/konstruktiver/schöner bauen kann, dann kannst du das natürlich gleich mitnehmen.
---
Umgesetzt (Umfang "alles", inkl. Literal-Nodes). Jede Wert-Art ist jetzt eine Zeile in
`src/core/properties/kinds.ts`; die Art-Listen, Menues, Tags, Port-Felder und Literal-Nodes folgen
daraus. Ein Tag ist `'$' + kind`, also generisch. Eine neue Art ist die Zeile plus – wenn sie etwas
im Projekt benennt – ihr Picker (`fieldFactory.REFERENCES`, vom Typecheck erzwungen).
Nicht angefasst, weil kein Typ-Thema: die `type !== 'exec'`-Guards und das Zwillingspaar
`Variables.ts`/`Properties.ts`.
-18
View File
@@ -1,18 +0,0 @@
1. es gibt das Node "String", der typ heißt aber "Text", das ist inkonsistent
2. "Item at" oder "Set item" node: index hat ".000" nachkommastellen im inspector, ist aber ein integer
3. neuer typ "Texture" momentan nur in einer liste hinzufügbar => es gibt kein "Texture" node, man kann den typ auch nicht bei variablen aussuchen
4. alle project seeds überarbeiten, sodass keine migration mehr notwendig ist und reseeden.
5. checken ob es projects gibt (ohne seed) die auch noch eine migration erfordern.
6. migration code entfernen
7. unnötige comments auch wieder entfernen
---
Umgesetzt. Entscheidungen dabei:
- Nur das Label des `String`-Nodes heisst "Text", Typ-Id und Porttyp bleiben.
- `Texture` (und `Color`) sind Variablen-Arten, Struct-Feld-Arten und Port-Arten; deklariert ist eine
Textur die blanke Id, der `$texture`-Tag gilt nur in Listen. Neuer Node `Texture` mit Picker.
- Jump-Run und Tetris wurden mit den aktuellen Seedern ueberschrieben; nur die fuenf Projekte auf
dem Server zaehlen. Danach sind `legacyNodes.ts`, `migrateDeclaredLists` und der
`ownerId`-Fallback samt Tests geloescht.
@@ -1,23 +0,0 @@
objects die nicht enabled sind, sind komplett unsichtbar, das passt
aber: wenn ich sie im panel linksunten selektiere, sollte zumindest das "gizmo" (also der selektions rahmen) im scene viewport sichtbar sein
---
Gebaut. Drei Dinge, die weder oben stehen noch aus dem Commit hervorgehen:
**Der Rahmen ist gedimmt (45 %).** Ein ausgeschaltetes Objekt zeichnet nichts, ein Rahmen um leeren
Raum in voller Stärke liest sich sonst wie eine Zeichnung, die fehlgeschlagen ist. Die Eckgriffe des
primären Objekts kommen mit – sie sind reine Kennzeichnung, es gibt keinen Hit-Test daran, sie
versprechen also keine Geste, die ein ausgeschaltetes Objekt nicht kann.
**Ziehen bleibt aus.** Der Rahmen sagt nur, wo das Objekt liegt; sein Node ist weiterhin unsichtbar
und `eventMode: 'none'`, weil `onScreen` in `WorldViewport` "aus" und "außerhalb des Sichtfelds" in
einem Ausdruck bündelt. Bewegen geht also nur über den Inspector – oder nach dem Einschalten.
**Es greift auch bei geerbtem Aus.** `DrawItem.enabled` ist `enabledAlongChain`, also das Objekt
*und* alle seine Eltern. Ein Kind, das nur deshalb unsichtbar ist, weil sein Elternteil aus ist,
bekommt denselben gedimmten Rahmen.
Nebenbei entstanden: `__gm.gizmos()` – vorher waren Gizmos überhaupt nicht prüfbar. Es meldet das
`alpha`, das wirklich in den Strich geht, nicht ein daneben abgeleitetes Flag; sonst hätte der Test
grün bleiben können, während gar nichts gedimmt wird (als Gegenprobe nachgestellt).
-15
View File
@@ -1,15 +0,0 @@
Sprite-Animation: Frames einer Textur nacheinander abspielen.
Stand: Texturen lassen sich schon in Frames schneiden (Slice-Dialog), aber ein Sprite zeigt immer genau einen Frame. Nichts spielt sie ab.
Abzuklären:
- wo die Animation wohnt: eigenes Asset (Frame-Liste + fps + Loop) oder Properties direkt am Sprite
- wie man sie im Graph steuert (starten/stoppen, anderen Clip wählen, "fertig"-Event)
- Vorschau im Editor oder erst im Play
---
Entschieden (User-Wahl):
- Eigenes Animation-Asset (Bildliste + fps + Loop/Once/Ping-pong) mit eigenem Tab; das Sprite trägt es über `texture`.
- Kein eigener Node: gewechselt wird per `Properties` (Sprite, `texture` auf Set). Eine andere Animation startet von vorn, dieselbe läuft weiter. Dazu `On animation finished`.
- Scene/Container zeigen den ersten Frame, der Animation-Tab spielt ab, im Play läuft alles.
- State-Machine und ein Neustart-Node erst, wenn ein Spiel sie braucht.
-24
View File
@@ -1,24 +0,0 @@
sprites haben das property width und height:
ist es hier auch sinnvoll auf integer umzuändern oder sollen wir .000 decimal dran lassen?
---
**Antwort (2026-09-09): nein, bleibt fraktional — ganzzahlig waere hier schaedlich.**
Sprite-Maße sind keine Zaehlwerte, sondern Geometrie. Zwei Stellen brauchen die Brueche:
- Die Aspect-Ratio-Knoepfe rechnen `fitAxisToAspect` (`height * w/h`). Das liefert echte
Brueche — der Code nennt selbst `9.090909090909092` als Beispiel. Auf ganze Zahlen
gerundet stimmt die Proportion nicht mehr, und der Knopf meldet danach, dass das
Seitenverhaeltnis nicht passt (Vergleich laeuft mit Toleranz `1e-9`).
- Am Objekt sind width/height ziehbar (Scrub), genau wie Position und Collider-Maße.
Ganzzahlig sind nur Felder, die ihrer Bedeutung nach ganzzahlig sind: Slice-Spalten/Zeilen,
Frame-Rect, Kamera-View, Font size. Die runden seit `1b16a55` auch wirklich (vorher war
`decimals` reine Kosmetik — getippte `640.5` wurden als `640.5` gespeichert und als `641`
angezeigt).
Offen bleibt die **Anzeige**-Frage, die dahinter steckt: sollen ueberfluessige Nullen weg
(`100` statt `100.000`, `9.091` bleibt)? Das waere eine Zeile in `formatNumber` und wuerde
fuer alle Zahlenfelder gelten; der Graph macht es bereits so. Dafuer bei Bedarf eine eigene
Notiz anlegen.
@@ -1,77 +0,0 @@
# Restposten aus dynamischen Ports / Structs
Drei Punkte, die beim Bau von List-Tab und Struct-Variablen bewusst liegen geblieben sind. Sie
standen am Ende von `done/nodes-mit-dynamischer-port-zahl.md` und sind hierher gezogen, damit sie
nicht mit der erledigten Notiz verschwinden.
**Alle drei sind gebaut** — dazu der Nebenpunkt, der beim Bauen aufgefallen war.
## 1. Der unverbundene Eingang zeigt das falsche Feld — ERLEDIGT
`inlineInputDef` (`NodeRegistry.ts:131`) kannte nur `boolean`, `string`/`any` und *alles andere* —
und "alles andere" wurde ein **Zahlenfeld**.
Beim Nachmessen war der Restposten **kleiner als die Notiz behauptete**: `object` und `struct`
kommen gar nicht mehr an `inlineInputDef` vorbei, `inlineInputs` siebt sie schon aus (mit der
Begründung, die die Notiz für `object` verlangt hat: ein Objekt ist ein lebendes Handle, ein
Struct wird aus Teilen gebaut). Offen war nur `list` — und dort ist es genau die Antwort geworden,
die die Notiz vorgezeichnet hat: der Knopf `n items` plus List-Tab.
Zwei Dinge, die dabei zu entscheiden waren:
- **Die Item-Art ist `number`.** Kein Port deklariert eine — und das ist derselbe Vorgabewert, den
`+ Variable ▸ List` setzt. Der Graph typisiert eine Liste ohnehin nur als Liste.
- **Ein verbundener Port löst nicht mehr auf.** Sobald eine Kante den Eingang speist, ist der
Inline-Wert tot; der Slot liefert `null`, statt etwas zu bearbeiten, das der Node nicht liest.
## 2. Ein Struct-Feld vom Typ Liste — ERLEDIGT
Es blieb bei einer Signaturänderung, kein Umbau — aber die Naht war nicht `resolveList` allein,
sondern der **Besitzer** dahinter. Die drei Fälle (Asset, Objekt, Node) liefern jetzt nur noch,
*was an `key` steht und wie man es liest und schreibt*; ob daraus die Liste selbst oder die Liste
eines Struct-Feldes wird, entscheidet ein Schritt danach. `ListSlot` trägt dafür ein optionales
`field`.
**Die Tiefenregel steht wieder in der Deklaration, nicht in einem Kommentar:** `StructFieldKind`
(= Primitive + `list`) sagt, was ein Struct-**Feld** hält, und ein solches Listenfeld hält
ausschließlich **Primitive** (`ListFieldDef`). Genau diese eine Asymmetrie hält die speicherbaren
Formen endlich:
```
list → struct → primitiv
struct → list → primitiv
```
Tiefer geht nichts — und das ist buchstäblich das, was `asStoredValue` seit jeher durch einen
untypisierten Slot lässt. Ein Listenfeld in den Items einer Liste weist `AddStructFieldCommand`
deshalb ab.
Geschrieben wird immer das **ganze Bündel**, neu gebaut aus dem, was in dem Moment im Besitzer
steht: nur die Liste zu schreiben würde ihre Nachbarfelder verlieren, ein Schnappschuss würde eine
zwischenzeitliche Änderung zurücknehmen.
## 3. Liste aus Structs — ERLEDIGT
Gebaut. Zwei Annahmen dieser Notiz waren dabei **falsch**, und beide haben es billiger gemacht:
- Es wird **nichts gegenseitig rekursiv**. Die Tiefe steckt in der *Deklaration*, nicht in einer
Konvention (siehe Punkt 2).
- Es setzt **Punkt 2 nicht voraus**. Ein Struct-Item braucht keinen Pfad in der Adresse, weil es
**inline** gerendert wird — dieselbe Begründung, aus der ein Struct im Inspector ein Block ist
und keine eigene Navigation bekommt. `structFieldHandlers` trägt die Feld-Adresse.
## Nebenpunkt: die Item-Art war im Editor nicht wählbar — ERLEDIGT
`+ Variable ▸ List` legte immer `items: 'number'` an, und für `items: 'struct'` gab es keinen
Feld-Editor; eine Liste aus Structs konnte deshalb nur aus einer Projektdatei entstehen.
- **Die Kennmarke ist der Wähler.** `≡#`, `≡Aa`, `≡✓`, `≡◧`, `≡⊞` — eine eigene Spalte gibt die
300 px breite Leiste nicht her, und die Marke ist die eine Stelle, die es sowieso schon sagt.
- **Ein Wechsel zieht jeden Wert mit** (`SetListItemsCommand`): deklarierter Vorgabewert, eigener
Wert des Behaviors und **jede Instanz-Abweichung**, jeweils durch `coerceValue` gegen die neue
Deklaration — dieselbe Umwandlung, die ein im List-Tab bearbeiteter Posten durchläuft.
- **Eine Liste aus Structs deklariert ihre Item-Form mit demselben `+ Field`**, das eine
Struct-Variable hat — nur Namen und Arten, weil die Werte Zeile für Zeile im Tab bearbeitet
werden. Die Feld-Kommandos gelten dafür jetzt auch für den Listen-Besitzer und fassen **jedes
Item jedes Wertes** an, damit kein Wert eine Form behält, die seine Deklaration nicht mehr
verspricht.
@@ -1,10 +0,0 @@
falls man im graph ein sub-behavior klick, wäre eine breadcrumb navigation gut, sodass man auch wieder zurückspringen kann.
sobald man in der hierachie (links im panel, assets/scene objects) wieder ein neues behavior auswählt bzw. über das dropdown im graph, soll das breadcrumb wieder verschwinden.
kurz gefasst: breadcrumb anzeigen sobald behaviour in graph geöffnet wird (doppelklick).
im breadcrump selbst, beispiel: "Game > Move > Other" - hier soll man im prinzip auf "Move" oder auch "Game" klicken können. in dem beispiel wenn man auf "Game" klickt, ist man wieder "Root" d.h. breadcrumb verschwindet wieder. klickt man auf "Move" wird aus dem Breadcrumb "Game > Move"
---
Umgesetzt (Regel in `src/editor/graph/trail.ts`):
- Oeffnet ein Doppelklick ein Behavior, das schon im Pfad steht, ist das ein Schritt zurueck dorthin statt einer Wiederholung.
- Wird ein Behavior im Pfad geloescht, verschwindet der Breadcrumb. Beim Umbenennen aendert sich nur der Eintrag.
- Der Breadcrumb erscheint vor dem Umschalten des Graphen, sonst schneidet er den Graph ab, der gerade auf die Flaeche eingepasst wurde.
-95
View File
@@ -1,95 +0,0 @@
## Nächstes spiel: Tetris
Ganz simpel halten, also wirklich nur das nötigste:
- die möglichen shapes die es halt so gibt
- ein zufallspawn eines dieser shapes das langsam nach unten geht
- links/rechts pfeilstasten um zu bewegen, pfeil rauf um im uhrzeigsinn zu drehen, pfeil runter gedrückt halten um nach unten zu beschleunigen
- eine line gefüllte line solved
- obere grenze = game over, ansonsten ist es einfach ein endlosspiel
- counter ui der einfach nur die gesolved lines zählt
was wir nicht brauchen:
- "levels", sprich dass shapes schneller werden beim runterfallen
- titlescreen oder gameover screen, wenn gameover einfach spiel stoppen
- animationen, spezielle texturen, oder sonst irgendwelchen schnick schnack
- e2e erweiterung zu dem projekt => dieses projekt braucht keine speziellen e2e tests die man immer wieder durchlaufen muss
habe ich irgendwas vergessen? wenn ja, rückfragen, ansonsten einfach project seed anlegen und projekt generieren
falls nodes fehlen, oder praktische nodes sinnvoll wären die noch fehlen, dürfen diese natürlich erstellt werden
---
**Umgesetzt (2026-09-09).** Projekt *Tetris* (`2072b56583af`), Seed `tools/seed-tetris.mjs`,
gespielt von `tools/smoke-tetris.mjs`. Alles aus deiner Liste ist drin, nichts davon, was du
ausgeschlossen hast: keine Level, kein Titel- oder Game-Over-Screen (Game Over stoppt das Spiel
per `Stop game`), keine Animationen, keine Texturen, und **keine Erweiterung der E2E-Suite** —
der Smoke-Lauf ist wie beim Jump-and-Run ein eigenes Skript, das nur laeuft, wenn dieses Projekt
oder die Engine darunter sich aendert.
**Das Brett ist ein Text.** 200 Zeichen, eins pro Zelle: `.` fuer leer, sonst die Ziffer der Form,
die dort liegengeblieben ist. Damit sind die beiden Operationen, auf denen Tetris steht, je ein
einziger Ausdruck statt einer Schleife: eine Zelle ist `Substring(grid, zeile·10 + spalte, 1)`,
und eine volle Reihe zu loesen ist eine leere Reihe, vor alles darueber und darunter gejoint.
Gezeichnet wird aus einem **Vorrat benannter Objekte** — `c0…c199` fuer das Brett, `p0…p3` fuer
den fallenden Stein. Waehrend des Spielens wird nichts gespawnt und nichts zerstoert.
**Vier neue Nodes, alle allgemein und nicht Tetris-eigen** (danach gefragt, du hast (a) gewaehlt):
| | |
|---|---|
| `Repeat` (flow) | `times` → `body` / `index` / `done`. Der Interpreter kann seitdem Schleifen: er verwirft den Datencache **pro Runde**, sonst rechnete jede Runde mit den Werten der ersten. |
| `Substring` | `count` Zeichen ab `start`; negatives `count` reicht bis zum Ende |
| `Find text` | wo ein Text zuerst vorkommt, `−1` wenn nicht — damit ist "Reihe voll" = *kein Punkt darin* |
| `To number` | die fehlende Gegenrichtung zu `To text` |
Dazu eine Aenderung an einem vorhandenen Node: **`Object by name` nimmt den Namen jetzt als
Eingang** statt nur als Einstellung. Ohne das waeren 200 Zellen nicht adressierbar — die Zeichen-
schleife baut `"c" + index`. Alte Projekte sind unberuehrt: ein gespeicherter Name liegt in
`params.name`, und genau von dort liest ein unverbundener Eingang (eigener Test dafuer).
**Der Kunstgriff, der den Graphen klein haelt:** jeder Frame versucht **genau einen** Zug —
Drehen schlaegt Seitwaerts schlaegt Fallen. Die Tasten bewegen nichts selbst, sie hinterlassen
einen Wunsch (`wantDx`, `wantRot`), den der naechste Frame einloest. Dadurch gibt es die
Pruefung "passen die vier Zellen dort hin?" **einmal** im ganzen Graphen statt einmal pro Taste.
Ein um einen Frame verzoegerter Seitwaertsschritt ist bei 60 fps unsichtbar; drei Kopien
derselben 30 Nodes waeren es nicht.
**Wie das Landen ins Brett kommt:** der gelandete Stein wird nicht noch einmal aus der Formtabelle
ausgerechnet, sondern **von seinen vier Zellobjekten abgelesen** (`Get position` → Spalte/Zeile).
Sie stehen genau dort, wo der Stein ist — das Zeichnen ist die einzige Stelle, die sie anfasst.
**Belegt** — `tools/smoke-tetris.mjs`, remote im echten Browser: **ALL PASSED, 17 Checks.**
```
PASS the piece falls on its own – row 1 → 4
PASS the left arrow moves the piece one column – 4,5,4,5 → 3,4,3,4
PASS the up arrow turns the piece – rot 0 → 1
PASS the board draws exactly the cells the board text names – {"lit":8,"inText":8}
PASS a full line disappears and is counted – lines 1, bottom row "....44...."
PASS a piece that cannot fall at the very top stops the game – stopped
```
Der Brett-Check ist der wichtigste: er haelt den **Text** gegen die **Bildschirmzellen** — ohne ihn
koennte die Zeichenschleife irgendetwas anzeigen und alles andere trotzdem gruen sein. Fuer die
volle Reihe wird die unterste Reihe vorher gefuellt, damit jeder Stein sie vollmacht, egal welcher
kommt. Der Game-Over-Check wartet auf die Bedingung statt auf die Uhr — mit fester Wartezeit war
er in einem von zwei Laeufen rot, und ein Check, der wuerfelt, ist keiner.
**Was ich selbst entschieden habe:**
- **Kein Wall-Kick.** Eine Drehung, die nicht passt, findet nicht statt — sie wird nicht an der
Wand vorbeigeschoben. Das ist die simple Regel, und "ganz simpel" war die Vorgabe.
- **Farbe pro Form**, wie im Original. Kostet drei Nodes: die Ziffer im Bretttext sucht ihre
Farbe aus einem zweiten Text (`colors`), acht Ziffern je Farbe.
- **Drehachse** ist die Box der Form (I in 4×4, O in 2×2, Rest 3×3), nicht der Schwerpunkt. Ohne
Wall-Kick ist das das Verhalten, das am wenigsten ueberrascht.
- **Game Over = ein Stein kann ganz oben (`py ≤ 0`) nicht einmal fallen.** Kein zweiter Test beim
Erscheinen, damit die Pruefung an der einen Stelle bleibt.
- **Tempo:** eine Reihe pro halber Sekunde, gehaltene Pfeil-runter-Taste zehnmal so schnell. Beides
steht als Variable `stepTime` im Inspector.
**Was du wissen solltest:** der Board-Container traegt **205 Objekte** (Wanne + 200 Zellen + 4
Steinzellen). Im Editor sieht man davon nichts — die Szene zeigt acht Objekte, der Rest liegt
im Container. Wer ihn aber oeffnet, bekommt eine lange Liste. Das ist der Preis dafuer, dass
nichts gespawnt wird.
@@ -1,46 +0,0 @@
Tetris Game mit Structs neu bauen
...und mit neuen `Get variables` / `Set variables` node
---
**Gebaut (2026-09-10).** Beides, in zwei Durchgängen: erst die Structs, dann die Plural-Knoten.
| | vorher | jetzt |
|---|---|---|
| `shapes` | 224 lose Zahlen | 112 `{x, y}`-Bündel |
| Zellindex | `((shape·4+rot)·8 + corner·2)`, zweimal gelesen | `((shape·4+rot)·4 + corner)`, einmal |
| deklarierte Behavior-Ports | 49 | 35 |
| Variablen im Master | 18 | 13 |
| Master-Graph | 84 Knoten / 94 Kanten | 44 / 64 |
| `next-move` | 44 Knoten | 30 |
| `clear-lines` | 29 Knoten | 23 |
`piece` und der Kandidat `next` sind Structs aus shape/rot/x/y. `Next move` gibt einen ganzen
Kandidaten zurück, `Fits` beurteilt ihn, der Master übernimmt ihn mit *einem* `Set variable`.
Jeder Bewegungsstrang packt nur das Feld, das er ändert (`Pack{rot}`) — „dasselbe Teil, eine Reihe
tiefer" ist dadurch wörtlich ein Feld.
## Zwei Verträge, an denen die Zusammenlegung hängt
Beide gemessen, beide als Unit-Test gepinnt (`tests/core/variable-ports.test.ts`), beide mit
Gegenprobe abgesichert:
- **`Set variables` schreibt je Port mit `writeTyped`**, genau wie der Einzelknoten. Ein
`Pack{rot}` wird deshalb weiterhin per `mergeStruct` eingemischt statt das Bündel zu
überschreiben. Ohne das hätte Drehen die anderen drei Felder gelöscht.
- **Ein Datenknoten wird einmal pro Puls ausgewertet und behält die Antwort** (`dataCache`,
geleert je Schleifenrunde). Ein `Get variables` darf deshalb keine Schreibung einer seiner
Variablen überspannen — sonst liest der spätere Verbraucher den Wert von davor. Deshalb sind
`grid`/`outgrid` in `clear-lines` weiterhin zwei Knoten, und `take`/`keep` bleiben getrennt.
Die Cache-Regel ist im **Spiel** nicht messbar: eine absichtliche Verletzung ließ den Smoke grün,
weil `OnUpdate` jeden Frame neu liest und der Fehler sich einen Frame später selbst heilt. Der
Unit-Test fällt dagegen sauber (`py: 7` statt `1`).
## Was bewusst nicht passiert ist
Das **Brett bleibt eine flache Zahlenliste**. `-1`/Formnummer ist bereits die kompakte Kodierung;
ein `{shape}`-Struct pro Zelle wären 200 Bündel für ein einziges Feld.
Eine **Listen-Variable kann ihre Item-Art im Editor nicht wählen** — eine Liste aus Structs
entsteht heute nur aus einer Projektdatei. Steht in `struct-und-listen-restposten.md`.
-94
View File
@@ -1,94 +0,0 @@
## Tetris Game verbessern
das ist erst relevant wenn behavior als node funktionieren
hier ein paar fragen vorweg:
- habe ich gesehen dass es die variablen `grid`, `shapes`, `colors` und `spawnX` gibt. diese sind sehr kryptisch. welche nodes fehlen, bzw. wie kann man das schöner gestalten.
- man könnte jetzt das ganze auch in mehrere behavior strukturell aufsplitten und diese in das `Tetris`-Behavior als Node einbetten
---
## Beantwortet und umgesetzt
Beide Punkte hatten dieselbe Ursache, und die lag nicht im Tetris-Projekt: **der Engine fehlte
ein Datentyp.** Eine Variable konnte nur Zahl, Text oder Ja/Nein sein (`PropertyValue`), also
musste jede Menge von Werten in einen String kodiert werden. Genau daher kamen deine vier
kryptischen Variablen — nicht aus einer schlechten Wahl beim Bauen.
### 1. Die kryptischen Variablen: es fehlte die Liste
`list` ist jetzt eine Variablen- und eine Port-Art. Sie hält Zahlen, Texte, Ja/Neins oder
**Farben** und wird im Inspector als eine kommagetrennte Zeile getippt.
| Variable | vorher | jetzt |
|---|---|---|
| `grid` | 200-Zeichen-Text, `.` = leer | Liste aus 200 Zahlen, `-1` = leer |
| `shapes` | 224 Ziffern am Stück | Liste aus 224 Zahlen |
| `colors` | `"03066326048763714..."` – Farben als 8-stellig gepaddete Dezimalzahlen | Farbliste: `#2ec9d6, #f5c542, …` |
| `spawnX` | `"3433333"` | `3, 4, 3, 3, 3, 3, 3` |
Dazu acht neue Nodes, alle allgemein statt Tetris-eigen: **`List`** (Konstante), **`Fill`**
(ein Wert wiederholt – so entsteht ein leeres Brett), **`Count`**, **`Item at`**, **`Set item`**,
**`Sub list`**, **`Join lists`**, **`Index of`**.
Was das im Graphen ausmacht: eine Zelle lesen war `Substring(grid, i, 1)` + `Equals '.'`, jetzt
`Item at(grid, i) >= 0`. Eine Landung schreiben war **sieben** Nodes (zweimal `Substring`,
zweimal `Join`, `To text`, `Add`), jetzt **einer**: `Set item`.
**Bewusst nicht gebaut:** eine Liste *von Objekten*. Eine Liste wird in die Projektdatei
geschrieben, ein Objekt ist ein Handle, das keinen Reload überlebt — eine Objektliste wäre die
erste Wertsorte, die beim nächsten Laden still verschwindet. Objekte gehen weiter über Namen
(`Repeat` + `Item at` + `Object by name`), und eine spätere Item-Art `reference` (stabile Id)
wäre reines Hinzufügen, kein Bruch.
### 2. Aufgesplittet: aus einem Graphen sind zehn geworden
Erst waren es fünf; auf deinen Einwurf hin habe ich auch die einzelnen **Schritte** eines Frames
zu Behaviors gemacht, die das Master-Behavior nur noch setzt.
| Behavior | Aufgabe | Nodes |
|---|---|---|
| `Spawn piece` | welcher Stein als nächstes kommt und in welcher Spalte | 5 |
| `Next move` | welchen **einen** Zug dieser Frame versucht | 44 |
| `Fits` | passen die vier Zellen dorthin? | 18 |
| `Draw piece` | den fallenden Stein an seinen Platz stellen | 14 |
| `Lock piece` | die gelandeten vier Zellen ins Brett schreiben | 20 |
| `Clear lines` | volle Reihen entfernen und zählen | 29 |
| `Show lines` | den Zähler auf den Schirm schreiben | 5 |
| `Draw board` | das Brett zeichnen | 11 |
| `Piece cell` | wo eine der vier Zellen eines Steins sitzt | 12 |
| **`Tetris`** | **nur noch: in welcher Reihenfolge, und wohin die Antwort** | **84** (vorher 219) |
`Piece cell` war der erste Gewinn: dieselben zwanzig Nodes standen **zweimal** im Graphen. Jetzt
einmal — und drei der anderen rufen es auf, Sub-Behaviors im Sub-Behavior.
**Keins der neun hält Zustand**, genau dein „kann man über input/output lösen". `Next move` ist
das Beispiel dafür: es *bewegt* nichts, es schlägt einen Zug vor und gibt ihn zurück
(`nx, ny, nrot, kind, fall, wantDx, wantRot`). Ein eingelöster Tastenwunsch kommt als `0`
zurück — so wird ein Tastendruck genau einmal ausgegeben, ohne dass das Sub-Behavior die
Variablen des Aufrufers kennt.
**Woran man misst, dass der Master wirklich nur noch dirigiert:** in seinen 84 Nodes steckt
**keine Schleife und kein Listenzugriff mehr** — kein `Repeat`, `Item at`, `Set item`,
`Get position`, `Set position`, `Set text`. Das prüft der Smoke-Lauf, damit die Arbeit nicht
unbemerkt zurückwandert:
```
PASS the master calls every step as a node, and does none of their work itself
calls: spawn-piece, next-move, fits, draw-piece, lock-piece, clear-lines, show-lines, draw-board
work: []
```
`Draw piece`, `Draw board` und `Show lines` haben **keinen End-Node**: sie beantworten nichts,
sie zeichnen nur.
### Ein Fehlschlag unterwegs
Der Game-Over-Check fiel um — und die Messung nannte den Grund selbst: `lines: 20`. Meine
Vorlage für „Brunnen voll" bestand aus 19 **vollen** Reihen, die `Clear lines` wegräumte, bevor
der neue Stein überhaupt anstand. Der Check maß also das Aufräumen statt das Spielende. Jetzt
lässt jede Reihe ihre letzte Spalte frei: blockiert, aber nirgends vollständig.
Und beim Gegenprüfen fiel auf, dass mein erster Persistenz-Test nichts bewies — er lief über den
schema-geführten Weg, während der Filter, den ich repariert hatte, für Container-Instanzen gilt.
Beide Wege sind jetzt einzeln abgedeckt.
@@ -1,9 +0,0 @@
rechtsklick auf texture asset => menüeintrag: create sprite from texture
selbsterklärend oder? (es soll im prinzip ein sprite asset erstellt werden wo automatisch die ausgewählte textur zugewiesen ist)
---
Umgesetzt: das Sprite bekommt die **Pixelgröße des Bildes** (Wahl des Users) und heißt wie die
Textur, nach der üblichen Regel durchnummeriert (`pasted` → `pasted 2`). Ist die Bildgröße noch
nicht bekannt, bleibt es bei 100 × 100. Die Start-Werte gehen beim Ausführen durch das Schema wie
jede andere Änderung; ein Undo nimmt das Asset in einem Schritt heraus.
@@ -1,40 +0,0 @@
Zitat aus der readme: "Unreferenced image files are never collected."
Ich würde sagen dass soll vorerst auch so bleiben.
Was passt:
drag and drop file in den editor => textur wird hochgeladen und passende asset wird erstellt => das soll so bleiben, das passt
Was wir auch haben aber ändern:
klickt man auf das `+` => `Texture`, dann öffnet sich der File System Browser.
Anstelle des file system browser => ein dialog mit allen images die am server existieren.
- auswählen => ok, dann wird es wiederverwendet => dialog schließt sich => texture asset mit ausgewählten image wird erstellt.
- oder hochladen button => file system browser => OK => dialog schließt sich => texture asset mit hochgeladenen image wird erstellt (im prinzip das was wir jetzt auch haben)
---
**Umgesetzt (2026-09-09).** Der Weg ueber `+` → `Texture` oeffnet jetzt einen Bild-Dialog statt
des File-Browsers; Drag&Drop und Ctrl+V bleiben unveraendert, und gesammelt wird weiterhin nichts.
**Der Dialog** (`src/editor/texture/ImagePicker.ts`) zeigt jede Datei, die der Server haelt, als
Kachel mit Vorschaubild. Auswaehlen und **Use image** legt ein Texture-Asset auf diese Datei an —
ohne Upload, die Bytes liegen ja schon da. **Upload…** oeffnet den File-Browser und muendet in
genau denselben Import wie ein Drop. Doppelklick auf eine Kachel ist die Kurzform von *Use image*.
Kacheln, die das Projekt schon traegt, sind mit dem **Asset-Namen** beschriftet (statt mit der
Pixelgroesse); eine davon zu waehlen legt kein zweites Asset an, sondern selektiert das
vorhandene — zwei Assets auf einer Datei wuerden sich sonst ueber `filter` streiten.
**Zwei Aenderungen, die dazu noetig waren:**
- `GET ?r=textures` liefert jetzt auch `width`/`height` (aus dem Datei-Header). Ein Texture-Asset
haelt die Pixelgroesse, und der Dialog haette sie sonst nur ueber das geladene Vorschaubild
gehabt — dann haenge die Richtigkeit des Assets am Ladezustand der GPU-Textur.
- `TextureLibrary.load(file)` neben `get(ref)`: laedt, **ohne** den `scaleMode` anzufassen. Ein
Vorschaubild darf den Filter eines Bildes nicht umstellen, das die Szene gerade zeichnet.
**Belegt** (`tools/e2e/12-textures.mjs`, Block 19e): ein zweites Bild wird eingefuegt, das Asset
per Undo wieder entfernt — die Datei bleibt auf dem Server, das Projekt kennt sie nicht mehr.
Genau die taucht dann im Dialog auf, ist *nicht* als "im Projekt" markiert, und ueber *Use image*
entsteht daraus wieder ein Asset mit 24 × 24 aus dem Server-Datensatz. Zusaetzlich geprueft: der
Dialog zeigt exakt die Dateiliste von `GET ?r=textures`.
-12
View File
@@ -1,12 +0,0 @@
ok, title bei variables node NUR wenn zusammengeklappt ändern:
- mehr als 1 set => "Set {n} variables"
- ansonsten bei mehr als 1 get => "Get {n} variables"
- ansonsten bei 1 set => "Set {name}"
- ansonsten bei 1 get => "Get {name}"
- ansonsten => "No variables"
dasselbe bei properties node
---
Umgesetzt: aufgeklappt heissen die Nodes immer `Variables` / `Properties`. Der Typ des `Properties`-Nodes steht deshalb als Zeile unter dem Titel (`Type: Sprite`, bei Behavior zusaetzlich `Behavior: <Name>`).
„Get {n}“ zaehlt nur Ports ohne Set.
-4
View File
@@ -1,4 +0,0 @@
e2e-Check `pixel-art filtering` in `12-textures` fällt gelegentlich, im Nachlauf grün.
Stand: fällt mit `gpu:"none", uploaded:false`. Ausgeschlossen: der Filterpfad selbst, 30 Wechsel in einer Sitzung, Kontextverlust. Verdacht: Timing/Umgebung.
Messgerät: `__gm.textureFilter(datei)` (Modellwert und Treiberzustand).
-20
View File
@@ -1,20 +0,0 @@
Ein Sprite, das ein Frame aus einem Sheet zeigt, zeichnet am Rand eine Pixelzeile, die zum
Nachbarframe gehört. Sichtbar im Projekt *Actor demo* als heller Streifen über dem Kopf.
Gemessen (`__gm.pixelAt`, Spielansicht): Beim Stehen nach unten (Sheet-Zeile 0, deren oberste
Pixelzeile in der Datei komplett leer ist) liegt dort `#394349` – bläulich. Das passt zur
*untersten* Sheet-Zeile (Schuhe von Character 6, `(87,87,154)`) über den grünen Hintergrund
gemischt, die Kante bricht also auf die Gegenseite um. Beim Laufen nach rechts (Zeile 2) ist der
Streifen stattdessen hellgrau – die Füße aus Zeile 1 direkt darüber.
Ursache: `frameFillMatrix` (`src/viewport/textureFill.ts`) bildet die **ganze** Textur auf die Shape
ab, weil Pixi-v8-Graphics-Fills `Texture.frame` ignorieren. Eine Fill-Matrix kann keine Grenze
ausdrücken, also sampelt die Randzeile in den Nachbarn, sobald die Skalierung nicht ganzzahlig ist
(die Spielansicht streckt 640×360 auf etwa 735×409, Faktor ~1,13).
Nicht die Ursache: die Frame-Rechtecke. Die sind exakt ganzzahlig; den fraktionalen Fall behandelt
`normalizeFrameRect` bereits.
Abzuklären: halber Pixel Einzug in der Matrix (kostet einen halben Pixel Bildinhalt, trifft jedes
Frame) oder ein echtes `Sprite` mit `new Texture({source, frame})` statt des Graphics-Fills (Pixi
klemmt dann selbst, ändert aber den Zeichenweg für alle Sprites).
-6
View File
@@ -1,6 +0,0 @@
Kommentare und Gruppen im Graph: eine beschriftete Box um mehrere Nodes, damit grosse Graphen lesbar bleiben.
Abzuklären:
- nur Box mit Titel, oder auch freier Kommentar-Text
- verschiebt die Box ihre Nodes mit
- zuklappbar wie ein Node?
-7
View File
@@ -1,7 +0,0 @@
Mehrere Szenen in einem Lauf: ein `Load scene`-Node.
Stand: ein Lauf kennt genau eine Szene, Bildschirme sind heute Container, die man ein- und ausschaltet (Jump-Run: Title/Level/Win/Lose).
Abzuklären:
- was beim Wechsel erhalten bleibt (globale Variablen? ein "persistentes" Objekt?)
- Startszene des Projekts festlegen
-5
View File
@@ -1,5 +0,0 @@
gzip im nginx einschalten.
Stand: das Bundle geht mit 589 827 Bytes unkomprimiert raus, obwohl der Browser gzip anbietet; komprimiert wären es 184 kB. `gzip` ist in `/_stacks/web/nginx.conf` aus.
Achtung: die Datei gilt für alle Seiten im Web-Stack, nicht nur für den game-maker. Nur auf ausdrücklichen Auftrag anfassen.
-5
View File
@@ -1,5 +0,0 @@
Nine-Slice für Sprites: Rahmen und Knöpfe behalten beim Skalieren ihre Ecken, nur die Mitte und die Kanten werden gestreckt.
Stand: ein Frame wird immer auf die Grösse des Sprites gestreckt (steht auch unter "Not yet" in der README).
Abzuklären: die vier Ränder am Frame/an der Textur festlegen oder am Sprite.
-10
View File
@@ -1,10 +0,0 @@
Ein im Play neu angelegter `On start`-Node feuert nicht für Behaviors, die schon laufen.
Stand: seit der Graph im Play bearbeitbar ist, wirken Änderungen ab dem nächsten Frame — nur `On start` nicht, weil der Start schon vorbei ist.
Abzuklären: nachträglich feuern (einmal pro laufender Instanz) oder bewusst so lassen und im Editor darauf hinweisen.
EDIT auch noch abzuklären: Wann feuert `On start`:
- nur beim ersten Frame?
- muss das behavior enabled sein?
- wenn ein behavior später enabled wird, feuert `On start` dann noch einmalig?
-3
View File
@@ -1,3 +0,0 @@
Node-Palette: Treffer im Namen vor Treffern in der Beschreibung ziehen.
Stand: sortiert wird nach Kategorie, dann alphabetisch; gesucht wird auch in Typ und Beschreibung. Suche "variable" + Enter landet deshalb auf `Run behavior` (Kategorie flow, Beschreibung enthält "variables"), nicht auf `Variables`.
-5
View File
@@ -1,5 +0,0 @@
Editor nur bei Änderungen neu zeichnen, im Play weiter durchgehend.
Stand: ein stehendes Bild kostet im Editor gemessen 69 % eines Kerns (Lüfter, Akku). Seit dem Low-Latency-Modus (`desynchronized`) verlängert ein Frame auf Abruf die Reaktionszeit nicht mehr.
Abzuklären: was alles ein Neuzeichnen auslösen muss (Eingabe, Hover, Laden einer Textur, Perlen/Live-Werte im Graph während Play).
-8
View File
@@ -1,8 +0,0 @@
Sound: Audio-Assets und ein Node zum Abspielen.
Stand: es gibt keinerlei Audio, weder Asset-Typ noch Node.
Abzuklären:
- Import wie bei Texturen (Datei hochladen) und welche Formate
- Node: abspielen, stoppen, Lautstärke, Loop (Musik vs. Effekt)
- Browser spielen erst nach der ersten Nutzergeste Ton ab
@@ -1,5 +0,0 @@
Struktur-Änderungen an einem Container-Template erreichen bestehende Instanzen nicht.
Stand: Objekt im Template hinzufügen, löschen oder umhängen wirkt nur bei der nächsten Platzierung. Schon platzierte Instanzen behalten die alte Struktur.
Abzuklären: was mit Overrides passiert, die an einem gelöschten Template-Objekt hängen.