editor: tile paint and collision strokes are one undo step, no collision option for tiles (visual-only model copy in the build), freed gl buffers and stale pick ids, export level as gltf/glb with textures and vertex colours

This commit is contained in:
MineTec committed 2026-11-07 11:57:22 +01:00
1 parent 59531a60b8
commit 2f22a56cec
30 files changed
+1737 -27

No files matched your search

+7 -5
View File
@@ -45,11 +45,13 @@ undo/history, project IO, build, play-test; property/fuzz tests; stress run of t
## Not fixed (reasons)
* Damaged-file dialogs show raw exception text ("ClassCastException …"); cosmetic, left to the UX review.
* After a failed open, the status bar keeps "Loading <file>" (`StatusBar.progressDone` keeps the last message) – cosmetic (UX).
* `GlRenderer.dispose()` does not delete `dynVao`/`dynVbo` (only at window close) – negligible.
* `SceneRenderer.ids`/`nodes` keep pick ids of deleted nodes until the next level (small, bounded by edits).
* Tile-paint erase strokes are one undo step per tile (`DeferredRemove` has no merge) – UX, not a defect.
* ~~Damaged-file dialogs show raw exception text~~ – fixed 2026-11-07 (edui): `ErrorDialog` (all `ctx.error` calls) shows one
plain sentence ("The project file is damaged.", "The file x does not exist.") and the exception + stack trace behind **Details**.
* ~~After a failed open, the status bar keeps "Loading <file>"~~ – fixed 2026-11-07 (edui): a failed background task sets
"<task> failed"; EditorStress `corrupt` now fails if the status still says "Loading…".
* ~~`GlRenderer.dispose()` does not delete `dynVao`/`dynVbo`~~ – fixed 2026-11-07 (edgeo), see editor-render.md.
* ~~`SceneRenderer.ids`/`nodes` keep pick ids of deleted nodes~~ – fixed 2026-11-07 (edgeo): dropped every 256 removals.
* ~~Tile-paint erase strokes are one undo step per tile~~ – fixed 2026-11-07 (edgeo): `cmd.Stroke`, one step per drag.
* `installTarget` treats a file with the same kind/base/level name as the previous play-test copy; two different
projects with the same level name and base still share one file (rare; renaming one avoids it).
* `LevelProjectSource`: when all TestLevel chapters 2..49 are taken, the 49th is used twice – practically unreachable.
+16
View File
@@ -277,6 +277,22 @@ behind: game look / editor).
(Ranges: several agents shared the CPU.) Scene conversion from WAD 160–290 ms. On any real GPU
this is far below 1 ms of GPU time; the CPU side is ~150 draw calls per frame.
## GPU resources (2026-11-07)
* `GlRenderer.dispose()` frees everything it created: batches, texture array, pick FBO, programs and
the dynamic `dynVao`/`dynVbo` (gizmo, overlays).
* Chunks that lose their last member are deleted with their batch (before: an empty chunk kept a
`Chunk` entry until the next full rebuild). Shared per-mesh batches (`meshBatches`, objects and
tiles without vertex colours) whose mesh no instance uses any more are freed once there are more
than 256 (limit doubles with the live count) – replaced meshes used to stay until a level switch.
* `Viewport.liveGlBuffers()` counts live VAO/VBO pairs of all batches (diagnostics).
`ui.view.GlLeakCheck` (test sources, X display): 3 rounds over Village 1 / world 4 / Village 2, each
load followed by 60 rounds of tile moves, object moves, delete + undo while rendering → the count
is identical per level in every round (84 / 87 / 76), 0 with no level loaded.
* `SceneRenderer` drops the pick ids of nodes that are no longer in the scene every 256 removals
(nodes that come back by undo get a new id; selected nodes keep theirs) – `SceneRendererIdsTest`.
Tiles marked "no collision" use a copy of their model mesh without the collision overlay.
## Known limits / next steps
* Only the first vertex colour frame and the primary tile model (no alt/swap models).
+50 -2
View File
@@ -62,6 +62,20 @@ height Page Up/Down. Painting onto a tile at the same height swaps its model ins
copy the vertex colours of a tile with the same model. **Tile swaps** (doors/bridges, WFPC 0x10000):
the Tile section shows the swap entry, sets the alternative model from the palette, removes or adds
a swap; layer "Tile swap alternatives" draws the alternative model in cyan.
One drag is **one undo step** (2026-11-07): paint, erase and swap issue `cmd.Stroke` commands with
the gesture's key; `Stroke.mergeWith` concatenates them (a `Batch` only merges commands of the same
shape, `DeferredRemove`/`AddNode` never merge). The collision tool's paint/toggle strokes use the
same command.
**No collision (visual only)** (2026-11-07): tile property `noCollision` (Inspector → Build, "No
collision"). `WadBuilder.visualOnly` replaces such tiles in the build by copies whose model (and
swap models) is a copy without collision polygons (`gen:visual:<model id>`, one per model, appended
to TRAK); other tiles of the model keep it. `Tile.worldCollisionPolygons()` is empty for them, so
`CollisionIndex`, checks, `FloorFinder`, `Ground` and the death-plane coverage ignore them; the
renderer draws them without the collision overlay. Default off = byte-identical builds.
In game (apptest :23, `NoCollisionDemo`, TestLevel 6): two walls baked from box brushes into scene
tiles, the north one visual only – the llama walks through it, the south one stops it
(`img/editor/geo_nocollision_game.jpg`, top row through, bottom row blocked).
### Collision tool (K)
Turns the collision layer on. Hovering highlights the polygon (or, on brushes, the whole face) under
@@ -100,6 +114,33 @@ and keeps the placement.
(round trip verified: same shape and textures, `img/editor/geo_library_insert.jpg` – right the
library tiles, left the re-imported OBJ as a mesh brush).
### glTF export (File → Export level as glTF…, `editor.tools.gltf`)
* `GltfExport.collect(scene, Options(objects, gameView), skip)` runs a private `SceneRenderer` on the
scene and takes exactly what the viewport draws: tile instances (scene tiles + compiled brushes and
meshes), optionally object models; `excluded` tiles and (option) hidden nodes are skipped.
Vertices are baked to world (export) space, deduplicated per primitive; one primitive per
page × blend class × double-sided, grouped into the meshes/nodes `level` and `objects`.
* Attributes: POSITION (exact min/max), NORMAL (unit; zero vertex normals → face normal; degenerate
triangles dropped), TEXCOORD_0 (page-normalised, v = 0 at the top as in glTF), COLOR_0 (u8
normalised RGBA = engine vertex colour ×2 clamped × flat/translucent face colour, as the viewport
shader). Materials: base colour texture = page PNG (embedded bufferView), `MASK` 0.5 for pages
with cut-out pixels, `BLEND` for alpha/additive classes (glTF has no additive), `doubleSided`,
`KHR_materials_unlit` (optional extension). u16 indices up to 65535 vertices, else u32.
* `.glb` (JSON + BIN chunk) or `.gltf` (one file, buffer as base64 data URI).
* Axes: export space (y up, z = −game z) like the OBJ export and the mesh importer, so a round trip
through **Import mesh** is exact. **Game orientation** mirrors z (and winding) for viewers – see
the open handedness question below.
* Verified: `GltfExportTest` (structural validation as the Khronos validator would: GLB header and
chunk padding, view/accessor bounds, equal attribute counts, exact POSITION min/max, unit normals,
UV range, index range, PNGs decode; .gltf data URI; round trip through `MeshImport` with the same
triangle count, bounds, textures and colours; mirrored bounds; objects; excluded tiles; brush box).
The official validator (npm) could not be run here (external code not allowed). **Blender 2.9
(local install)** imports the village .glb without complaint: meshes `level` 26 355 tris /
`objects` 9 684, 16 + 15 materials, 14 images, vertex colours and UVs
(`img/editor/geo_gltf_blender.jpg`, Workbench render); the editor's own Import mesh reads it back
(26 403 triangles, 16 materials, 15 textured, `img/editor/geo_gltf_export.jpg`).
Village: 36 087 triangles with objects, 2.9 MB .glb; without objects 2.1 MB.
### Checks (Level → Check, Build)
Tiles outside the grid; tile models (also compiled brush/mesh tiles) over **8000 vertices**;
**overlapping** identical tiles; **no floor collision under the player start** (cell-based like the
@@ -156,9 +197,16 @@ between neighbouring floors at the same height; brush compiler warnings.
## Known limits
* Collision materials of brushes are per face class, not per individual polygon; tile models per
polygon. Collision of tile-model polygons cannot be switched off (paint 30 or hide the tile).
polygon. Collision of a whole tile can be switched off ("No collision"), single tile-model polygons cannot.
* Water and death planes are cell based: resize/move/90° only. Gaps check only unrotated
box/platform/floor tops.
* Library insert keeps tiles cell-aligned (anchor = whole cells); objects are not part of the library
(see the objects agent's prefabs / object catalogue). glTF export not done (OBJ only).
(see the objects agent's prefabs / object catalogue).
* glTF export: whole level only (no "selection" variant), no collision export (the OBJ export has it),
no texture animations, additive surfaces become plain alpha blending. **Handedness**: the editor
view is left-handed like the game (editor-render.md), but the mesh importer and the OBJ/glTF
exports treat file coordinates as export space – so a right-handed tool (Blender) shows exported
levels mirrored relative to the game, and an asymmetric model imported from Blender probably
appears mirrored in game. Not verified in game; "Game orientation" in the glTF export is the
workaround for viewing.
* `TextureImport` packs imported textures at one size per insert (largest texture, ≤ 256).
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB