Phase 5: region select + client-side glTF export

Adds the Region Export API (GET /api/export/:serverId/:dimension/
:x1/:z1/:x2/:z2, chunk-coordinate range, capped at 64 chunks server-
side) streaming raw chunkSections rows for a footprint, plus a fully
client-side pipeline that turns that into a downloadable glTF (.glb):
a JS port of the Rust worker's greedy mesher (voxel-mesh.js, mirrored
test-for-test against worker/src/mesh.rs) and block-color palette
(block-colors.js, mirrored against worker/src/palette.rs), a base64
section decoder, mesh merging with per-section world offsets, and a
hand-rolled minimal GLB writer (gltf-export.js) — hand-rolled rather
than Babylon's GLTF2Export since this project's Babylon usage never
meshes client-side (only renders pre-built buffers) and a live
Scene/Engine isn't needed or unit-testable. The whole mesh+export
pipeline runs inside a Web Worker (export-worker.js) so a multi-chunk
export doesn't block the UI thread. The map UI gets a drag-to-select
region tool (chunk-snapped, live rectangle preview, cap enforced
client-side too) and an Export glTF button.

Built test-first per the project's TDD workflow; the exported .glb
was independently validated with @gltf-transform/cli against real
seeded terrain data (correct triangle count, vertex count, and
bounding box for the selected area).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-09 00:10:05 +02:00
parent b2f9b6c0e9
commit c78661efb5
18 changed files with 1048 additions and 0 deletions
+14
View File
@@ -43,6 +43,20 @@ html(lang="en")
button.px-2.py-1.bg-neutral-700.rounded.text-xs(x-on:click="cancelMarkerForm") Cancel
p.text-xs.text-amber-400(x-show="markerStatus" x-text="markerStatus")
aside.w-80.flex.flex-col.border-l.border-neutral-700.bg-neutral-800(x-show="server")
div.border-b.border-neutral-700.p-2.space-y-2
div.flex.items-center.justify-between
h2.text-sm.font-semibold Region export
button.px-2.py-1.rounded.text-xs(
x-bind:class="selectingRegion ? 'bg-amber-600' : 'bg-neutral-700'"
x-on:click="toggleSelectingRegion"
x-text="selectingRegion ? 'drag on map…' : '+ select region'")
p.text-xs.text-neutral-400(x-show="regionStatus" x-text="regionStatus")
div.flex.gap-1(x-show="regionBounds")
button.flex-1.px-2.py-1.bg-emerald-700.rounded.text-xs(
x-bind:disabled="exporting" x-on:click="exportRegion"
x-text="exporting ? 'exporting…' : 'Export glTF'")
button.px-2.py-1.bg-neutral-700.rounded.text-xs(x-on:click="cancelRegionSelection") Cancel
div.border-b.border-neutral-700.p-2.space-y-2(style="max-height: 40%; overflow-y: auto;")
div.flex.items-center.justify-between
h2.text-sm.font-semibold Markers