Stand: 2026-05-10. Basis: vollständige Quell-Analyse von
packages/twopoint5d,packages/twopoint5d-testing,apps/lookbook, sowie der Monorepo-Tooling-Schicht.Methodik: Sechs unabhängige Explore-Agenten haben parallel die Domänen Architektur (vertex-objects), Rendering-Pipeline (sprites/display/stage), Support-Module (texture/map2d/controls/utils), Tests, Build-System und Dokumentation erforscht. Die kritischsten Bug-Behauptungen wurden anschließend manuell anhand der Quellen verifiziert. Befunde, die sich nicht halten ließen, sind aus dem Bericht entfernt (z. B. ein gemeldeter Bug in
InputControlBase.removeEventListener— der DOM matcht Listener nicht überpassive, das ist kein Fehler).
Die Library ist architektonisch solide und konzeptionell stark (klare Schichtung von vertex-objects → sprites/map2d, konsequenter Einsatz von @spearwolf/eventize/@spearwolf/signalize, ESM-only, sideEffects: false, NodeNext-konformes Source-Layout). Der größte Schwachpunkt ist nicht der Code, sondern die Diskrepanz zwischen Implementierungstiefe und Außenwirkung:
- Rendering-Kerne haben keine Unit-Tests.
sprites/(11 Dateien) undcontrols/(4 Dateien) enthalten null*.spec.ts-Dateien. Display-Klasse mit 517 LOC wird nur über die State-Machine indirekt geprüft. - Browser-Tests sind aktuell de facto Smoke-Tests. Nur 1 von 2
.test.js-Dateien hat realen Inhalt (hello-twopoint5d-canvas.test.jstestet "Display existiert + frameNo > 0"); die andere ist ein Dummy (number-or-the-beast.test.js). - TSDoc-Coverage ist niedrig (~6–8 % der Public-API mit
@param/@returns). Gepaart mit der "Read the source, Luke!"-Strategie macht das Onboarding mühsam. - Public API hat parallele Klassen-Hierarchien (
VertexObjectGeometryvs.VOBufferGeometry,VertexObjectPoolvs.VOBufferPool), die für Library-Nutzer verwirrend sind. - Build-Scripts haben triviale Redundanzen (
cbt,test:all,cisind wortgleich) und ungenutzte Dependencies (tsup,use-asset,ts-nodewerden nirgendwo referenziert). - Mehrere belegbare Resource-Cleanup-Lücken in
dispose()-Implementierungen.
Es gibt keine Show-Stopper, aber sehr viel niedrig hängendes Obst. Die Backlog-Items sind in vier Prioritätsklassen sortiert: 🔴 P1 (Bug oder Risiko), 🟠 P2 (Konsistenz/Wartbarkeit), 🟡 P3 (Modernisierung), 🟢 P4 (Nice-to-have).
Datei: packages/twopoint5d/src/vertex-objects/InstancedVOBufferGeometry.ts
Ursprünglicher Befund: Map.clear() entfernte zwar die Map-Einträge der extraInstancedPools, rief aber kein clear() auf den jeweiligen VOBufferPool-Instanzen auf. Zusätzlich fehlten extraInstancedBuffers.clear() und extraInstancedBufferSerials.clear().
Umgesetzte Lösung (über das ursprüngliche Fix-Snippet hinaus):
attachInstancedPool() hat einen optionalen dritten Parameter options?: {autoDispose?: boolean} bekommen (Default: true). Damit kann der Aufrufer steuern, wem der Pool gehört:
autoDispose: true(oder weggelassen) → der Pool wird beimdispose()der Geometry mit-aufgeräumt. Sinnvoll, wenn der Pool exklusiv zu dieser Geometry gehört (z. B. ad-hoc für einInstancedMesherzeugt).autoDispose: false→ die Geometry lässt den Pool unangetastet und entfernt ihn nur aus den eigenen Bookkeeping-Maps. Sinnvoll, wenn derselbe Pool an mehrere Geometries gehängt ist (Pro-Hint des bestehenden TSDoc).
dispose() räumt zusätzlich extraInstancedBuffers und extraInstancedBufferSerials auf und konsumiert die interne autoDispose-Map. detachInstancedPool() entfernt das passende autoDispose-Tracking-Eintrag mit.
Test-Coverage: sieben neue Cases in InstancedVertexObjectGeometry.spec.ts (describe('dispose()')-Block) decken Default-Verhalten, explizites true/false, gemischte Flags pro Pool, Map-Cleanup und das Zusammenspiel mit detachInstancedPool() ab.
Datei: packages/twopoint5d/src/sprites/AnimatedSprites/AnimatedSpritesMaterial.ts
Ursprünglicher Befund: super.dispose() lief vor dem Aufräumen des animsMap-Signals. Da das Parent TexturedSpritesMaterial.dispose() zuerst SignalGroup.destroy(this) aufruft, war das #animsMap-Signal danach bereits zerstört — die anschließenden value?.dispose() / set(undefined) / destroy()-Aufrufe funktionierten nur durch signalize's Lenz, dass zerstörte Signals den letzten Wert noch zurückliefern.
Umgesetzte Lösung: Reihenfolge umgedreht — die Texture wird disposed, das Signal auf undefined gesetzt und das lokale Handle zerstört, bevor super.dispose() die SignalGroup abräumt:
override dispose(): void {
this.#animsMap.value?.dispose();
this.#animsMap.set(undefined);
this.#animsMap.destroy();
super.dispose();
}Test-Coverage: neue AnimatedSpritesMaterial.spec.ts (8 Cases): Konstruktion mit/ohne animsMap-Option, Dispose-Verhalten (Texture-Release, No-Op ohne animsMap, animsMap-Reset auf undefined), explizite Order-Assertion gegen NodeMaterial#dispose via sinon.calledBefore, Signal/Effect-Leak-Check via getSignalsCount/getEffectsCount-Baseline, Idempotenz bei doppeltem Dispose.
Datei: packages/twopoint5d/src/vertex-objects/VOBufferPool.ts
Ursprünglicher Befund: pool.clear() setzte nur usedCount = 0, gab aber TypedArray-Referenzen nicht aktiv frei. Bei langen Sessions mit dynamischer Pool-Erzeugung (z. B. Tile-Streaming) wuchsen Heap-Allokationen unnötig, bevor der GC sie aufnahm.
Umgesetzte Lösung: explizite dispose()-Methode auf VOBufferPool plus VertexObjectPool-Override:
VOBufferPool#dispose()setztusedCount = 0, nulltbuffer.typedArrayfür jeden Eintrag inpool.buffer.buffersund leert diebuffers-Map. Damit kann das darunterliegendeArrayBuffervom GC eingesammelt werden, auch wenn downstreamTHREE.BufferAttributes die Array-Referenz noch kurz halten.VOBufferPool#isDisposedals Getter;dispose()ist idempotent (mehrfache Aufrufe sind No-Ops).VertexObjectPool#dispose()ruft zusätzlichonDestroyVOfür jeden noch lebenden VO auf, hängt mitVOUtils.clearBuffer()die Buffer-Referenz von jedem getrackten VO ab und leert den internen#voIndex. Das deckt auch VOs ab, die nachfreeVO()-Swaps in alten Slots zurückgeblieben waren.
Bewusst nicht im Scope: VOBufferGeometry#dispose() ruft weiterhin pool.clear() (nicht pool.dispose()) — Pools dürfen laut "Pro-Hint" in attachInstancedPool() an mehrere Geometries gehängt werden, der Geometry-Lifecycle darf den Pool also nicht einseitig zerstören. Wer den Pool aktiv freigeben will, ruft pool.dispose() selbst auf.
Test-Coverage: acht neue Cases im neuen describe('dispose()')-Block in VertexObjectPool.spec.ts decken Disposed-Flag, TypedArray-Release, Idempotenz, onDestroyVO-Fan-out (inkl. Zusammenspiel mit freeVO()-Swaps), Buffer-Ref-Unlinking auf VOs und den Edge-Case "kein lebender VO" ab.
Datei: packages/twopoint5d/src/display/Display.ts
Ursprünglicher Befund: Display.resize() werde nie automatisch durch ein Window-Event getriggert.
Auflösung als bewusste Design-Entscheidung: Display.resize() läuft am Anfang jedes Frames innerhalb von renderFrame() (Display.ts:447). Damit deckt der per-Frame-Aufruf nicht nur Window-Resizes, sondern auch Container-Reflows, devicePixelRatio-Änderungen, resize-to-Attribut-Mutationen und runtime-Swaps von resizeToElement einheitlich ab — ohne DOM-Listener, die in dispose() aufgeräumt werden müssten. Re-Computations sind günstig (Hash-Vergleich kürzt No-Ops ab). Ein zusätzlicher window.resize-Listener wäre redundant.
Umgesetzte Aktionen:
- Class-Level-JSDoc auf
Displayergänzt mit explizitem "Resize model"-Abschnitt: Resolution-Order (resize-to-Attribut → resizeToCallback → resizeToElement), Fullscreen-CSS-Toggle-Verhalten, Emission-Kontrakt vonOnDisplayResize. - Method-JSDoc auf
Display.resize(),Display.renderFrame(), sowie aufresizeToElement/resizeToCallback/resizeToAttributeEl/MaxResolution. - Browser-Test-Suite
packages/twopoint5d-testing/test/display-resize.test.jsmit 13 Cases auf Chromium + Firefox: HTMLElement-Host als Size-Source,resizeToElement-Option und Runtime-Swap,resizeToCallback-Override, Reaktion auf Container-Resize ohne DOM-Listener,OnDisplayResize-Emit-Dedup bei unverändertem Hash,resize-to="self"/"window"/ CSS-Selector, Fullscreen-CSS-Class-Toggle,pixelZoom-Skalierung mitpixelRatio === 1,MaxResolution-Clamp, sowie Initial-Frame-Emit-Garantie. - Dummy-Browsertest
number-or-the-beast.test.jsentfernt.
Datei: packages/twopoint5d/src/display/Display.ts
Ursprünglicher Befund (festgestellt beim Schreiben der Resize-Tests): Wenn sich die im Konstruktor gemessene Größe und die im ersten renderFrame() gemessene Größe unterschieden (häufig, weil zum Konstruktor-Zeitpunkt noch keine CSS-Maße angewandt sind), feuerte auf Frame 1: (1) ein Emit aus resize() (Hash geändert), gefolgt von (2) einem Emit aus dem if (isFirstFrame)-Branch in renderFrame(). Subscriber sahen zwei Resize-Events mit identischen Maßen.
Umgesetzte Lösung: Privates Flag #didEmitResize markiert, ob resize() in seinem letzten Aufruf bereits emittet hat; der First-Frame-Fallback in renderFrame() zündet jetzt nur noch dann, wenn resize() selbst nicht emittet hat. Damit gilt der Kontrakt: OnDisplayResize feuert auf Frame 1 garantiert genau einmal, und auf späteren Frames genau dann (genau einmal), wenn sich der Resize-Hash geändert hat. Class-JSDoc auf Display entsprechend präzisiert.
Test-Coverage: display-resize.test.js enthält jetzt zwei dedizierte Cases: "emits OnDisplayResize exactly once on the first frame" (assertet exakt 1 Emission auf Frame 1) und "does not double-emit OnDisplayResize on the first frame when the size differs from construction" (Regression-Schutz für genau das Szenario, das den Doppel-Emit bisher provoziert hat — Host wird nach dem Konstruktor, vor start(), vergrößert).
Detaillierte Analyse + Test-Coverage: siehe Backlog-TextureStore.md §2 (BUG-9). Statt automatischer Disposition beim refCount === 0 gibt es jetzt eine explizite Aufräum-Methode store.clearUnused(), die alle Resources mit refCount <= 0 disposed und entfernt.
Datei: packages/twopoint5d/src/map2d/chunk-quad-tree/ChunkQuadTreeNode.ts und packages/twopoint5d/src/map2d/AABB2.ts
Ursprünglicher Befund (über Analyse der bisher unproduktiv eingesetzten WIP-Klasse): mehrere latente Bugs (findChunksAt null-deref auf jedem subdivided Tree; calcAxis schoben das falsche chunk-Argument in drei Klassifikations-Listen, deren Inhalte deshalb Müll waren — die Counts blieben durch Glück korrekt; AABB2#isInsideAABB swappte (x,y) im Eckpunkt-Test; redundante OR-Klauseln in AABB2#isNorthWest/etc.; originX/originY per @ts-ignore als number typisiert obwohl initial null), plus signifikanter GC-Druck im subdivide() (O(n²) Achsenwahl mit map/filter/sort und drei Throwaway-Arrays pro calcAxis-Aufruf, plus per-Aufruf Function.bind) und im findChunks (per-Rekursionsebene Array#concat).
Umgesetzte Lösung:
AABB2#isInsideAABBneu geschrieben (klare 4-Kanten-Bedingung statt verschränkterisInside-Aufruf).AABB2-Quadrant-Helper (isNorthWest/isNorthEast/isSouthEast/isSouthWest) auf den effektiven Boolesken Ausdruck reduziert — alle 52 existierenden Quadrant-Tests bleiben grün.ChunkQuadTreeNode#findChunksAt()bekommt einen Leaf-Guard plusnull-Toleranz für fehlende Kindquadranten.calcAxisdurchscoreAxisersetzt: drei Integer-Counter statt drei Arrays; gibtnullfür nicht-teilbare Achsen direkt zurück.findAxisals Single-Pass-Min mit Dedup adjacent gleicher Origins (chunksist bereits sortiert) — eliminiertmap/filter/sortundFunction.bind. O(n × unique-origins) statt O(n²).subdivide()partitioniert in einem Durchlauf in vier Bucket-Arrays + einen Straddler-Bucket; Kindknoten übernehmen ihre Buckets per Referenz (privatermakeChild-Helper) — keinchunks.length=0/forEach(appendChunk)-Re-Entry mehr.findChunks(aabb, out?)bekommt einen optionalen Output-Parameter für allokationsfreie Hot-Path-Aufrufe (z. B.CameraBasedVisibility); rekursive Aufrufe pushen direkt in das geteilte Array.clear()-Methode hinzugefügt: setzt den Knoten auf den Initialzustand zurück und löst alle Kindreferenzen — geeignet für Tile-Streaming-Re-Builds.originX/originYund dienodes.{north,south}{East,West}-Slots tragen nun ehrlichenumber | null/ChunkQuadTreeNode<…> | null-Typen; das@ts-ignore-Trio im Klassen-Header entfällt.- Konstruktor-
[].concat(chunks)-Hack durchArray.isArray-Branching ersetzt.
Test-Coverage: neue ChunkQuadTreeNode.extended.spec.ts mit ~40 Cases (Konstruktor-Varianten, canSubdivide-Lebenszyklus, subdivide-Edge-Cases inkl. nicht-teilbare Eingabe und Idempotenz, gezielte Achsen-Heuristik-Assertion, Bucket-Erschöpfungsinvariante, appendChunk auf Blatt/Subtree/Straddler/lazy-Quadrant, findChunks Mehr-Quadrant-Coverage und Out-Param-Kontrakt, findChunksAt Happy-Path + null-Quadrant-Toleranz + Straddler, clear() inkl. Wiederverwendbarkeit, 1k-Chunk-Stress-Smoke, 400-Zellen-Korrektheits-Smoke). Plus 6 neue Cases in AABB2.spec.ts für asymmetrische Container, die den x/y-Swap-Bug reproduzieren.
Hinweis: ChunkQuadTreeNode ist via public-api.ts exportiert, hat aber bisher keinen Produktions-Konsumenten — die Lookbook-QuadTreeVisualization ist der einzige Caller. Das Hardening ist vorausschauende Arbeit für die geplante Map2D/CameraBasedVisibility-Integration.
Datei: packages/twopoint5d/src/map2d/CameraBasedVisibility.ts
Ursprünglicher Befund: pro Frame entstanden previousTiles.slice(0), ein neues visitedIds-Set, ein next-Stack, plus pro sichtbarem Tile ein TileBox, zwei Box3 (jeweils mit zwei Vector3 im Konstruktor), ein Vector3 (centerWorld), eine Map2DTileCoords mit AABB2 und ein 8-Element-Nachbar-Literal in der inneren Schleife. Zusätzlich war die Wiederverwendungs-Suche (findIndex + splice) O(n²) und die Distanz-Sortierung (insertAndSortByDistance) ebenfalls O(n²).
Umgesetzte Lösung:
- TileBox-Pool (
#tileBoxPool: Map<id, TileBox>): pro Tile-Id (auch über Frames hinweg) genau einTileBoxmit eigenenBox3/Vector3/Map2DTileCoords/AABB2-Shells. Jeden Frame werden nur die Inhalte überBox3.min/max.set(…),Vector3.set(…),AABB2.set(…)mutiert — keine Neuallokation der schweren Three.js-Objekte. - Cache-Invalidierung: ändert sich
tileWidth/tileHeight/xOffset/yOffset(perMap2DTileCoordsUtil.equals()), wird der gecachtetile.coords-Slot pro Pool-Eintrag verworfen —Box3/Vector3-Shells bleiben. previousTiles-Lookup: O(1)-Map<id, IMap2DTileCoords>statt O(n)-findIndex+ O(n)-splice— Gesamtkosten der Klassifikation (reuse / create / remove) sinken von O(n²) auf O(n).- Reusable Working-Buffers als Klassen-Felder:
#visitedIds,#nextStack,#previousTilesById,#scratchTranslate/#scratchOffset/#scratchCamDir/#scratchLineEnd/#scratchLineOfSight/#scratchPlaneIntersection.Set.clear()/length = 0stattnew. - Nachbarn-Iteration: 8 Offsets als modul-globaler
NEIGHBOR_DX_DY-Konstantenarray,for-Schleife stattArray#forEach— kein per-Tile-Callback-Allocation. - Sortierung: einmaliges
Array#sortmit(a,b) => a.distanceToCamera - b.distanceToCameraersetzt das O(n²)-insertAndSortByDistancewährend der Traversierung.
Helper-Vertrag (CameraBasedVisibilityHelpers liest tile.frustumBox, tile.box, tile.primary) bleibt unverändert.
Test-Coverage: neue CameraBasedVisibility.spec.ts (15 Cases): undefined-on-no-camera, Visible-Tiles-Tile-Center, parallel-zur-Ebene mit/ohne previousTiles, Cache-Pfad bei unveränderten Deps, Klassifikation create/reuse/remove über Frames, sortierte visibles, Helper-Kontrakt, tile.view-Aufbau, offset/translate-Werte, matrixWorld-Translation, sowie ein Low-GC-Regressionscheck, der die Identität der gepoolten TileBox-, Box3- und Vector3-Instanzen zwischen non-cached Frames mit identischer Tile-Sichtbarkeit asserted.
Dateien: packages/twopoint5d/src/stage/{IStage,IRenderable,IStageRendererHost,Stage2D,StageRenderer,Canvas2DStage,public-api}.ts, packages/twopoint5d/src/events.ts
Detaillierte Analyse: siehe Backlog-StageRenderer.md.
Iteration 1 (§3.1–§3.8, §4, §5) + Iteration 2 (§6) vollständig umgesetzt:
Stage2Dvon toten Clear-Properties (clearColor/clearAlpha/autoClear) befreit — die wurden vom Renderer nie gelesen.StageRendererbekommt ein explizitesclear: boolean-Flag;setClearColor(color, alpha?)aktiviert es als Convenience und ist fluent (this-Return). Ebensoadd,remove,attach,detach.IStage(Lifecycle:name+resize+updateFrame) und neuerIRenderable(renderTo(renderer)) sind sauber getrennt. Damit fällt derisStageRenderer-Discriminator weg,Stage2DundStageRendererimplementieren beide Interfaces.IStageRendererHostextrahiert;StageRenderer.parentakzeptiert nun jeden Host mitonResize/onRenderFrame(Display ist strukturell kompatibel) sowie verschachtelteStageRenderer.OnAddToParent-Event ergänzt (symmetrisch zuOnRemoveFromParent).- Warnung in
add()bei doppeltem Namen + nicht-defaultrenderOrder(vorher stiller Sort-Fail). setClearColor-Signatur gelockert (Color | null),setClearAlpha-Restore liegt jetzt imif (shouldClear)-Zweig (vorher Side-Effect pro Frame).- Class-JSDoc auf
StageRendererdokumentiert beide Frame-Loop-Modi (auto viaparent/ manuell), die Clear-Policy und diename/renderOrder-Eindeutigkeit. - Public
ClearStage(Marker-Stage zum Buffer-Clearen zwischen Siblings, depth-only default) — Idiom aus §5. RenderPipeline-Integration (§6):StageRenderer.pipeline?,outputRenderTarget?,buildOutputNode?,IPassProvider#asPassNode,StageRenderer.dispose()+invalidateOutputNode(). Drei Modes: C (internes RT), D (TSL-Komposition mitbloom/fxaa/etc), E (verschachtelte StageRenderer mit eigener Pipeline).packages/twopoint5d/src/stage/README.md: Cheat-Sheet mit Architektur-Diagramm, Hello-World, Manual vs. Auto-Mode, Layering, Nesting, Clear-Policy-Tabelle, Off-Screen-Rendering, Pipeline-Modes C/D/E, Resource-Lifecycle, Custom-Host.- Lookbook-Demos:
display-multi.astroaufgeräumt (doppeltes Frame-Driving entfernt),display-minimal.astroauf das fluent §4-Idiom umgestellt,quadtree-playgroundauf explizitessetClearColor(null, 0)migriert, neue Demosstage-postprocessing.astro(Mode D mitbloom) undstage-nested-pipelines.astro(Mode E: World-Renderer mit eigener Bloom-Pipeline + UI plain on top).
Test-Coverage: StageRenderer.spec.ts jetzt 31 Cases (zusätzlich für §6: outputRenderTarget redirect, Mode C internal-RT-Sampling, outputNode-Rebuild-Caching, Mode D buildOutputNode-Vertrag, Throw-on-missing-asPassNode, Mode E pre-Render-into-asPassNodeRT, dispose lifecycle, invalidateOutputNode). Erweiterte Stage2D.spec.ts (5 Cases) — renderTo mit/ohne Camera + Removal-Assertion; neue ClearStage.spec.ts (5 Cases); neuer Browser-Test stage-renderer.test.js (Display-Integration, Multi-Stage, Nesting, detach()-Unhook) und stage-pipeline.test.js (Mode C / Mode D / dispose).
Migration: siehe ### Migration Guide in [Unreleased] von packages/twopoint5d/CHANGELOG.md (Iteration 1 + Iteration 2: renderFrame → renderTo, entfernte Stage2D-Properties, IRenderable-Pflicht für eigene Stages, Auto- vs. Manual-Modus, clearAlpha === 0 ist kein Clear-Trigger mehr, empfohlenes fluent-Idiom, ClearStage für Zwischen-Clears, Adoption der pipeline-Integration aus Mode D, Komposition verschachtelter Renderer aus Mode E).
Nice-to-have für die Zukunft (kein Blocker): RenderTarget-Pool zur Mehrfachverwendung, gemeinsame Effekt-Builder als buildOutputNode-Factories — siehe Backlog-StageRenderer.md §9.
Dateien: packages/twopoint5d/src/texture/{TextureStore,TextureResource,TextureFactory,TileSet}.ts
Detaillierte Analyse: siehe Backlog-TextureStore.md. Alle dort priorisierten Welle-1/2/3/4/5-Items sowie die §5-Cleanups sind umgesetzt:
- Korrektheit: Daten-Mutation (
splice(0)→slice()), Double-Dispose entfernt, Image-Race + Texture-Leak gefixt (Abort-Flag + Cleanup-Disposal), statischesTextureStore.load(url)wartet jetzt tatsächlich aufwhenReady(),TextureResource#dispose()ist idempotent,SignalGroup.delete(this)statt deprecateddestroy(). - Reaktivität:
#frameBasedAnimationsData/#frameBasedAnimationszentral im Konstruktor (statt verteilt infromTileSet/fromAtlas), Atlas-Resources akzeptieren jetzt initiale Animationen,parse()-Update-Pfad propagiert Animationen in existierende Resources, gesamterparse()-Body inbatch(). - Performance: zentrale
TextureFactoryam Store (eine pro Renderer statt eine pro Resource), abortablefetch()für Atlas-JSON. - DX: neue API-Methoden
clearUnused(),whenResource(id),get(id, type, {signal}); exported Event-KonstantenTextureStoreEvents/TextureResourceEvents/TextureResourceSubtypes;console.error-Aufrufe durch strukturierte'error'-Events ersetzt (Payload:{source, url, error});defaultTextureClassesals Signal mit Struktur-Compare; Self-Cleanup derOnDispose/OnReady-Listener instore.on()-Unsubscribe-Funktion. - Cleanups:
TileSet.tileCountLimit === Infinity-Redundanz entfernt.
Test-Coverage: TextureStore.spec.ts von 2 auf 32 Cases erweitert (siehe Backlog-TextureStore §10 Tabelle). Volle CI-Suite weiterhin grün (1214 Tests).
Migration: siehe ### Migration Guide in [Unreleased] von packages/twopoint5d/CHANGELOG.md.
Offen (P4-Kategorie, kein Blocker): API-Renamings load()-Trio (§4.1) und get() → getAsync() (§4.2) sind breaking changes und in den nächsten Major-Sprint verschoben; Image-Dedup-Cache (§6.2); cmpTexCoords/cmpTileSetOptions per Object.keys (§6.5); Auto-Counter für anonyme FrameBasedAnimations-Namen (§6.6).
Datei: packages/twopoint5d/src/vertex-objects/public-api.ts
Es werden parallel exportiert:
VertexObjectGeometry(dünner Wrapper überVOBufferGeometrymit Typ-Narrowing)VOBufferGeometry(low-level)InstancedVertexObjectGeometry↔InstancedVOBufferGeometryVertexObjectPool↔VOBufferPool
Grep zeigt: intern (in sprites/, map2d/) wird fast ausschließlich die VertexObject*-Variante benutzt. Die VOBuffer*-Klassen sind faktisch Implementation-Detail, müssen aber wegen der Generic-Constraints exportiert werden. Optionen:
- Mindestens TSDoc-Banner an die
VOBuffer*-Klassen: "Internes API. NutzeVertexObject*außer du weißt, was du tust." - Oder: separate Sub-Path-Exports (
@spearwolf/twopoint5d/internals), so dass die top-levelindex.tsnur die User-facing Variante exponiert.
Dateien: sprites/TexturedSprites/TexturedSprite.ts, sprites/AnimatedSprites/AnimatedSprite.ts
TexturedSprite hat setColor(), setColorValues(), etc. — AnimatedSprite hat keine Color-API. Auch der Convenience-Getter/Setter texture existiert nur auf TexturedSprites, nicht auf AnimatedSprites (dort muss man auf das Material durchgreifen, um animsMap zu setzen). Entweder API harmonisieren oder im README explizit dokumentieren.
Datei: packages/twopoint5d/src/stage/projection/IProjection.ts:6
getViewRect(): [width, height, pixelRatioH, pixelRatioV] — Tuple-Returns sind in TypeScript-IDEs schwer lesbar. Nach {width, height, pixelRatioX, pixelRatioY} umstellen. Breaking-Change, aber gering, da Projection-Implementierungen intern sind.
Datei: packages/twopoint5d/src/stage/Stage2D.ts:40, 142–148
Das Flag wird intern auto-resettet, aber der User weiß das nur durch Code-Lesen. TSDoc mit Beispiel.
Datei: packages/twopoint5d/src/display/Display.ts:65–79
Wenn pixelZoom > 0 wird devicePixelRatio ignoriert. Kontraintuitiv. Vorschlag: Umbenennen zu pixelArtZoom oder TSDoc-Warnung.
Datei: packages/twopoint5d/src/map2d/CameraBasedVisibility.ts:56
Als Konstante mit Begründungs-Kommentar exponieren oder konfigurierbar machen.
InstancedVOBufferGeometry.ts:59,89–90:as any-Casts undVertexObjectPool<any>ohne Schema-Validation. Lösbar durch besser typisierte Overloads.Dependencies.equals()(utils/) hat undokumentierte implizite Semantik (z. B.null == null). Zumindest TSDoc-Beispiel.
Damit der Backlog nicht nur wie eine Mängelliste wirkt, hier explizit das Solid Stuff, das nicht angefasst werden sollte:
Display:eventize/retainkorrekt fürOnDisplayInit,OnDisplayStart,OnDisplayResize(Display.ts:149–150).Display.dispose()ruft explizitoff(this)(Display.ts:475) — Listener werden sauber abgeräumt.visibilitychange-Listener wird indispose()korrekt entfernt (Display.ts:253).TexturedSpritesMaterial.dispose()nutztSignalGroup.destroy()korrekt.StageRenderer(StageRenderer.ts:129–143) bindet Listener mitonce()anOnRemoveFromParent— elegantes Auto-Cleanup-Pattern, sollte als Vorbild für andere Lifecycle-Code dienen.
| Modul | Source-Dateien | Spec-Dateien | % | Bewertung |
|---|---|---|---|---|
vertex-objects/ |
26 | 10 | ~38 % | ✓ Gut, kritische Pfade abgedeckt |
map2d/ |
28 | 8 | ~29 % | CameraBasedVisibility.spec.ts (15 Cases) seit der Performance-Optimierung neu hinzugekommen |
texture/ |
14 | 5 | ~36 % | ✓ TextureAtlas exzellent (TextureAtlas.spec.ts, 230 Zeilen, randomisierte Permutationen) |
stage/ |
15 | 7 | ~47 % | ✓ Projection-Tests gut; Stage2D.spec.ts, StageRenderer.spec.ts (31 Cases) und ClearStage.spec.ts (5 Cases) seit den Iterationen 1+2 (inkl. Pipeline-Mode C/D/E) |
display/ |
10 | 2 | ~20 % | Chronometer + DisplayStateMachine als Vitest-Specs; das Resize-Verhalten der Display-Klasse wird seit display-resize.test.js (13 Cases, Browser) abgedeckt — übrige Lifecycle-Bereiche der Klasse weiterhin ungetestet |
utils/ |
7 | 5 | ~71 % | ✓ Sehr gut |
sprites/ |
11 | 0 | 0 % | 🔴 Komplett ungetestet |
controls/ |
4 | 0 | 0 % | 🔴 Komplett ungetestet |
Verifiziert via find packages/twopoint5d/src/{sprites,controls} -name "*.spec.ts" → keine Treffer.
packages/twopoint5d-testing/test/:
hello-twopoint5d-canvas.test.js: Smoke-Test (Display + frameNo > 0).number-or-the-beast.test.js: Dummy (expect(666).to.equal(666)). Bitte löschen oder durch realen Test ersetzen.
web-test-runner.config.js: Playwright Chromium + Firefox, esbuild-Plugin, 2 s Timeout — für GPU-Init zu eng. Keine Visual-Regression, keine Pixel-Asserts, keine Memory-Profiling.
Diese 15 Module sind das Herz der Library und haben null Tests. Beginnen mit:
sprites/BaseSprite.spec.ts: Instanziierung, Position/Scale/Rotation-Updates,make()-Pfade.sprites/TexturedSprites/TexturedSprite.spec.ts: Frame-Selection,setColor*, Texture-Atlas-Binding.sprites/AnimatedSprites/AnimatedSprite.spec.ts: Frame-Animation-Timing, Play/Pause-States.sprites/TexturedSprites/TexturedSpritesMaterial.spec.ts: Dispose-Verhalten (verbunden mit Bug 2.1 #2).controls/InputControlBase.spec.ts: Listener-Bookkeeping,subscribe/unsubscribe-Roundtrip, idempotentesaddEventListener.controls/PanControl2D.spec.ts: Multi-Pointer-Sequenz, Bounds-Clamping.
17 Module haben dispose()-Methoden, nur ~1 prüft sie. Pattern:
it('dispose() releases shared resources', () => {
const spy = sinon.spy(somePool, 'clear');
geometry.dispose();
expect(spy.calledOnce).to.be.true;
});- ✅
number-or-the-beast.test.jsgelöscht. - ✅
display-resize.test.jsergänzt (13 Cases, Chromium + Firefox). - Test-Timeout auf 5–10 s erhöhen (
web-test-runner.config.js). - Migration zu
@playwright/testmit nativen Screenshot-Asserts (expect(page).toHaveScreenshot()) für Sprite-Rendering und Animation-Frame-Verifikation. - Memory-Smoke-Test: 100 Frames rendern,
performance.memory.usedJSHeapSizedarf nicht monoton wachsen.
vitest --coveragein CI mit Threshold (initial 30 %, schrittweise auf 60 %).test.each()für Boundary-Cases (Tile-Offsets, Atlas-Indizes).
| Asset | Status |
|---|---|
Root-README.md |
✓ Gut, motivierend, leitet zu Lookbook |
packages/twopoint5d/README.md |
✓ Strukturiert mit Feature-Status (✓ stable / |
packages/twopoint5d/CHANGELOG.md |
✓ Aktuell (Stand Feb 2026) |
apps/lookbook/README.md |
🔴 Unverändertes Astro-Starter-Template |
| TSDoc auf Public-API | @param/@returns-Annotations |
| Externe Doku-Site | 🔴 Existiert nicht — .github/workflows/deploy.yml deployt nur npm, nicht die Lookbook |
| Concept-/Tutorial-Seiten | 🔴 Kein "Hello World" für Vertex-Object-Description |
| Modul | Lookbook-Demo | Status |
|---|---|---|
sprites/TexturedSprites |
textured-sprites/ |
✓ |
sprites/AnimatedSprites |
animated-sprites/, animated-billboards/ |
✓ |
vertex-objects |
instanced-quads/, crosses/ |
✓ |
texture/ |
implizit in Sprite-Demos | |
map2d/ |
3 map2d-*.ts im Root |
|
stage/ (Projections) |
nur indirekt | 🔴 keine dedizierte Demo |
display/ |
implizit überall | 🔴 keine dedizierte Demo |
controls/ |
— | 🔴 fehlt komplett |
Astro-Default raus. Stattdessen: was ist Lookbook, wie navigiert man, wo findet man welche Demo (mit Tabelle wie 4.2).
Minimalziel: jede Funktion/Klasse, die in einem public-api.ts re-exportiert wird, hat @param/@returns/@example. Aufwand: 5 Dateien × 5–10 min pro File ist zu optimistisch geschätzt — realistisch 1–2 Tage Fokuszeit für die Kern-Module.
Anfänger-Demo (apps/lookbook/src/.../hello-world/), die in 30 Zeilen Code einen einzelnen TexturedSprite zeigt — ohne BounceSprite-Subclass, ohne Magic Numbers. Begleitend eine Concept-Seite, die VertexObjectDescription erklärt.
.github/workflows/deploy.yml um Astro-Build + GitHub-Pages-Publish erweitern. Onboarding-Hürde fällt drastisch, wenn https://spearwolf.github.io/twopoint5d/ einfach existiert.
PanControl2D mit kurzer Doku.
Modul-Dependency-Graph, Erklärung der vertex-objects-Pipeline. Dient Contributors.
Datei: package.json:25–29 (verifiziert)
"cbt": "...clean lint build checkPkgTypes test:ci",
"test:all": "...clean lint build checkPkgTypes test:ci",
"ci": "...clean lint build checkPkgTypes test:ci"Auf eines reduzieren (Empfehlung: ci), die anderen als Aliase oder ganz weg.
Zentral in nx.json setzen (tui: { enabled: false }) statt jedes Script zu fluten.
Verifiziert via grep -r "tsup\|use-asset" packages apps scripts → keine Treffer.
| Dependency | Status | Empfehlung |
|---|---|---|
tsup |
nirgendwo importiert | entfernen |
use-asset |
nirgendwo importiert | entfernen |
ts-node |
nirgendwo benötigt (ESM-Projekt) | entfernen |
npm-run-all |
benutzt, aber pnpm hat natives run-s |
optional ersetzen |
esbuild |
wird via @web/dev-server-esbuild benötigt |
behalten |
Datei: apps/lookbook/project.json
Dadurch greifen --projects=tag:twopoint5d-Filter nicht. Mindestens ["twopoint5d"] setzen.
Folge: test-Target nutzt nur nx:run-script-Default ohne explizite inputs/outputs. Caching ist suboptimal.
Aktuell sind Inputs nur in packages/twopoint5d/project.json definiert. Globaler Default in nx.json würde Wiederholung in jedem Package-Project sparen:
"build": {
"dependsOn": ["^build"],
"inputs": [
"{projectRoot}/src/**/*.ts",
"!{projectRoot}/src/**/*.spec.ts",
"sharedTsconfigs"
],
"outputs": ["{projectRoot}/dist"],
"cache": true
}test-Default hat ebenfalls keine Outputs → Cache greift nicht.
.github/workflows/ setzt pnpm/action-setup@v4 mit run_install: true, ohne Caching-Step davor. Auf großen CI-Runs lohnt sich actions/cache für ~/.local/share/pnpm/store.
Verifiziert: enthält nur Bilder, keine package.json, kein Build-Eintrag. Commit bc361c9 ("chore: remove abandoned handbook app") hat den App-Code entfernt, aber das Asset-Verzeichnis übrig gelassen.
Optionen:
- Wenn die Bilder noch von der
apps/lookbookreferenziert werden → inapps/lookbook/public/umziehen. - Sonst
git rm -r apps/handbook/.
Aktuell: tsc → dist/lib/ + scripts/makePackageJson.mjs synthetisiert das Publish-package.json. Funktioniert, ist aber custom. Mögliche Modernisierungen:
- 🟢 P4:
publintundarethetypeswrong(letzteres ist bereits installiert) zur CI-Validierung des Publish-Artefakts. - 🟢 P4: Erwägen, ob
tsup(bereits als devDep installiert!) dentsc-Step ersetzen könnte — Vorteil: Banner, Source-Maps, evtl. dual ESM/CJS. Nachteil: würde die etabliertetsc-Pipeline ablösen, hoher Refactor-Aufwand. Nicht hochpriorisiert — der aktuelle Build funktioniert.
Astro + React + Tailwind + lil-gui — moderne, etablierte Stacks. Index-Page mit Tag-Filter und DemoCardsGrid ist gut gelöst, LookbookMetadata und LookBookApi sind sinnvolle Abstractions.
- (4.3) — siehe Doku-Sektion: README ersetzen, GitHub-Pages-Deploy, "Hello World"-Demo, Concept-Seite.
- 🟢 P4 — Magic Numbers in Beispielen (
width=300, height=150etc.) durch benannte Konstanten oder Code-Kommentare erklären.
- ✅
InstancedVOBufferGeometry.dispose()— extra-Pools korrekt aufräumen, inkl. neuerautoDispose-Option anattachInstancedPool()(§2.1). - ✅
AnimatedSpritesMaterial.dispose()— Reihenfolge korrigieren, plus 8 Unit-Tests inAnimatedSpritesMaterial.spec.ts(§2.1). - 🔴 Sprites-Test-Suite anlegen (BaseSprite, TexturedSprite, AnimatedSprite).
- 🔴 Controls-Test-Suite anlegen (InputControlBase, PanControl2D).
- 🔴
apps/lookbook/README.mdersetzen. - 🔴
number-or-the-beast.test.jslöschen.
- ✅
TextureResource.refCount→ expliziteTextureStore#clearUnused()-Methode (sieheBacklog-TextureStore.mdBUG-9). - ✅
VOBufferPool.dispose()(siehe §2.1). - 🟠 TSDoc auf Public-API (alle
public-api.ts-Exports). - ✅
Display.autoResize— als bewusste Design-Entscheidung dokumentiert (per-Frame-Resize-Modell), Test-Coverage intwopoint5d-testing(siehe §2.1). - 🟠
apps/handbook/-Aufräumung. - 🟠 Build-Scripts deduplizieren (
cbt/test:all/ci), ungenutzte Deps raus. - 🟠 Nx-Tags für
lookbook, Targets fürtwopoint5d-testing. - 🟠 Browser-Tests: Timeout hochsetzen, erste Screenshot-Asserts.
- 🟡
IProjection.getViewRect()→ Object-Return (Breaking). - 🟡 Vitest-Coverage in CI mit Threshold.
- 🟡 Lookbook-GitHub-Pages-Deploy.
- 🟡 "Your First Sprite" Demo + VertexObjectDescription-Concept-Seite.
- 🟡
nx.jsontargetDefaults erweitern (Inputs/Outputs für Caching). - 🟡
NX_TUI-Konsolidierung innx.json. - 🟡
controls/-Demo in Lookbook.
- 🟢
publint/arethetypeswrongin CI (arethetypeswrongläuft bereits viacheckPkgTypes, müsste nur als blocking gate definiert werden). - 🟢
Display.pixelZoom-Renaming. - 🟢
ARCHITECTURE.md. - ✅
CameraBasedVisibilityPer-Frame-Allocs reduzieren — TileBox-Pool, O(n)-Map-Lookup, reused Set/Stack/Vectors, Single-Sort statt O(n²)-Insert (siehe §2.1). - ✅
ChunkQuadTreeNodeBug-Bündel + GC-Druck —findChunksAt-Null-Guard,calcAxis/findAxis-Refactor (Counter + Single-Pass + Dedup, keinFunction.bind),subdividemit direkten Buckets,findChunks(aabb, out)Out-Param,clear(), ehrlichenumber | null-Typen, plus ~40 neue Tests inkl.AABB2.isInsideAABB-x/y-Swap-Reproducer (siehe §2.1). - 🟢
tsup-Migration evaluieren (oder devDep entfernen).
InputControlBase.removeEventListener"passive-Bug" (von einem Sub-Agenten gemeldet): geprüft — DOMremoveEventListenermatcht Listener nicht über daspassive-Flag, nurcaptureund Function-Reference matchen. Kein Bug.- Kompletter Refactor vom
tsczutsup: technisch denkbar, aber der Status-quo funktioniert und der Aufwand wäre groß. Nur als P4-Erwägung gelistet. - Cross-Browser Visual-Regression mit Percy/Chromatic: leistungsstark, aber kostenpflichtig und für ein Solo-OSS-Projekt unverhältnismäßig. Native Playwright-Screenshots reichen als erster Schritt.
Bericht erstellt durch Multi-Agenten-Analyse mit nachträglicher Quellverifikation. Alle Datei-/Zeilenverweise wurden gegen die aktuelle main-Version (Commit c4693d6) abgeglichen, soweit Stichproben das zuließen.