v1.14.0 — Room for one more 🗜️ (26-F: the import gate and the decimation worker; the Football template) - #221
Merged
Merged
Conversation
- b8d1b7c rewrote the entry to say no template was published, which was true then; the template went live 2026-09-19 (PR #218 + scenes v2 tag) - the entry now names both ways in: Templates > Games > Football and the toolbox's Build pitch - a FORWARD correction on release/next: v1.13.0 stays as tagged, and the in-app What's new (rendered from the tag) keeps the old wording until 1.14 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[docs] 1.13 changelog: Football now has its Games-tab template
…rs the scene - THE FINDING: a model file went from the file dialog straight into the scene. The wire gate (26-C) and the .tpscene file-open ask count OBJECTS, and one model is one object, so a 2.4M-triangle scan passed every gate there was. - importBudget.js (a LEAF, pure): modelCost walks a parsed tree for triangles, vertices, draws (per material group), the largest single mesh, and texture bytes counted once per SOURCE; importVerdict judges "the scene plus this model" with sceneBudget's own tierOf on the SAME table the meter reads - no second rule. RENDER_PASSES = 2 (26-E measured: the shadow pass draws every mesh again), which puts the prediction in the meter's unit - asserted against the real renderer (predicted 399,420 tris/frame for a 200k GLB, measured +400,092). - sceneBudget.js: IMPORT_BUDGETS (largest mesh 500k/100k verts, largest texture 4096/2048px, texture memory 512-1024/256-384MB) beside BUDGETS so tierOf stays the one rule; they are not meter rows (nothing samples them per 500ms). - WHEN IT ASKS: an axis that is red AND this model took it there, or a model heavy on its own. An already-red scene does not ask about every cube dropped into it. - importGate.js: admitModel asks through the app's ONE confirm dialog (the same showChoice the file-open ask uses) - "Load anyway / Cancel", with an extraChoices seam phase 3 fills. fileHandler: parseModelFile split out of importFile (parse -> ask -> place); Cancel disposes the parsed tree and leaves the scene byte-identical (no object, no undo entry, nothing sent). importGeneratedGlb asks too (rejects on Cancel so the AI job reports it). Debug hook: importBudget + importGate (211/211/211). Counterfactuals (suite import-budget): - gate call removed from importFile -> 3 red + the crossing dialog never opens - RENDER_PASSES = 1 -> 8 red (prediction, headset case, crossing, calls, meter agreement) - already-red clause removed -> the "does not ask about every smaller model" check red Suites: import-budget 33/33 (new). Held vs base (A/B'd with src reverted): ingest-gate 29/29, scene-budget 34/34, animation-persist 22/22, dispose 14/14 green; animated-models (peers in phase), fbx-mtl-import (.mtl data URL), import-duplicates (3 Settings checks) red IDENTICALLY on base; explorer-drop "DOM drop routes through the Explorer handler" flakes on base too (1 of 3 base runs). Unit 143/143. svelte-check 341/47 (= base). Build green with the server stopped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ake back
- decimateWorker.js: a module Worker (Vite `new URL(..., import.meta.url)`), one job per
fresh Worker, typed arrays TRANSFERRED both ways. The main thread only copies arrays
out and builds geometry back (9-13ms for a 1.2M-triangle model; the simplification
ran 1.4s in the Worker while frames kept coming, longest gap 17ms).
- THE DECIMATOR IS meshoptimizer, NOT three's SimplifyModifier (plans/QUESTIONS-26-f Q1):
measured quadratic here - 40k tris 1.0s, 80k 4.8s, so 26-E's 200k case ~30s and a
2.4M scan hours. meshoptimizer 1.1.1 (MIT, wasm inside the JS, already in the lockfile
via @types/three) did 200k -> 50k in 133ms keeping normals+uvs and reports its error.
Now a direct dependency; pre-bundled in vite.config (dev otherwise reloaded the page
on the first reduction - "optimized dependencies changed").
- decimateCore.js (imports NOTHING; runs in the Worker AND in node): weld a soup by
position+uv first (unwelded, nothing collapses), simplify each material group
separately with LockBorder (no crack, no triangle changes material), compact the
vertex buffer, report the error. planTargets (scene room shared in proportion,
per-mesh vertex cap, 10% floor, meshes <1000 tris untouched) and planTextureCap.
- decimate.js (leaf): reduceModel(root, plan) in place - same meshes, same uuids,
new geometry; skinned/morph meshes skipped WITH the reason; __topo dropped; textures
above the ceiling drawn down on the SAME Texture (flipY/wrap untouched).
importBudget.reductionPlan: aim each asking axis at the edge of red (5% margin), an
already-red scene at the green ceiling alone.
- fileHandler: importFile(..., {reduce}) places a reduced import with a `reduced`
stamp on the root (additive userData), RETAINS the original File (LRU, 256MB) and
restoreOriginalImport swaps the original back at the same uuid (children too when the
tree shape is unchanged). The swap is a new `importswap` history kind holding the LIVE
objects - registered in fileHandler, which nothing in history's static subtree
reaches (walked: 56 modules, none of ours). Replicates as the ordinary delete + GLTF
object sync.
- FOUND ON THE WAY (both measured red first):
· the create/delete kinds' undo re-sends toJSON plain number arrays; binarypack
overflows past ~40k numbers and broadcast swallows it - a first version built on
them left the peer with NOTHING at that uuid after Ctrl+Z (6k-tri model). Hence the
importswap kind. Pre-existing for any undo of a medium mesh delete (handover).
· commandsHandler.sendObjects: the ROOT group message of a placed import carried no
userData, so a root stamp (this `reduced`, AI `aiGen` provenance) never reached a
peer. Now the same additive field the nested-group branch already sends.
Counterfactuals (suite import-decimate unless noted):
- simplify on the main thread instead of the Worker -> frames 4 in 1362ms, longest gap
1333ms (2 red)
- weld skipped -> soup 14160 -> 14160 (e2e red + unit red)
- no LRU eviction -> retention check red (true,true,true)
- restore via create/delete batch instead of importswap -> "B follows the undo" red,
B held [] (measured during development)
- root group message without userData -> "stamp rides the ordinary sync" red (measured)
- the long-task observer read 0ms even in the main-thread counterfactual; the check is
now the rAF gap, which is what a frozen tab actually is
Suites: import-decimate 37/37 (new, two peers). Held: import-budget 33/33, ingest-gate
29/29, object-sync 12/12, dispose 14/14, net-handshake 9/9, shader-graph 67/67; undo
"the placed position replicates" red on base too, "redo restores the pose" a two-peer
flake (green 4 of 5 on this tree); animated-models "peers in phase" red on base.
Unit 143/143 (decimateCore 10 new). svelte-check 341/47 (= base). Build green with the
server stopped; the Worker is emitted as its own chunk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… / Cancel in one dialog - ONE STORY: the Reduce choice is a third way out of the SAME dialog phase 1 opens (the file-open ask's showChoice), never a second dialog. Its label is the quality judgement made visible before anyone commits: "Reduce to ~9,500 triangles" / "Reduce to 4096px textures", from the same planner that then does it (plannedTriangles/textureCapFor). - Offered only where it can act: a draw-call ask gets no Reduce (decimation keeps every mesh); an animated model gets none and the dialog SAYS why (its rig reaches peers as the original file); a texture-only ask reduces textures and leaves geometry alone. - While it runs: a "Reducing X... you can keep working" card (the Worker is the point). After: a report - "20k -> 9,500 triangles (-52%), no point moved more than 0.04% of its size", texture sizes, skipped meshes and WHY, "still above ..." when the 10% floor could not reach the budget - with Restore original IN the report. - The way back outlasts the toast: the object menu carries "Restore original model" while the original is held; a copy whose original is not held (a peer, after a reload, evicted) shows it DISABLED with the reason. The retained-originals map moved into decimate.js (a leaf) so the menu can ask synchronously; the action reaches fileHandler by dynamic import (objectMenu stays out of the history family). - importGeneratedGlb offers the same Reduce (the AI mesh path is an import too). - importGate.admitModel gained `note` (one sentence for why an option is absent). Counterfactuals (suite import-reduce-gate): - no extraChoices -> "ONE dialog, three ways out" + label checks red, run aborts at the click - extraChoices also for animated models -> the animated check red - menu entry enabled whether or not the original is held -> the peer-copy check red - report toast without Restore original -> "offered IN the report" red, run aborts Suites: import-reduce-gate 22/22 (new). Held: import-decimate 37/37, import-budget 33/33, ingest-gate 29/29, object-sync 12/12, dispose 14/14, multiselect-menu 11/11, convert-to-mesh 57/57, save-as-formats 39/39; origin-rows "B has the cubes in the group" + "peer B agrees" red identically with commandsHandler at base (2 of 2 runs). Unit 143/143. svelte-check 341/47 (= base). Build green with the server stopped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[feat] 26-F: import budget + decimation Worker (Reduce / Load anyway / Cancel)
…ll template Named at write time rather than left as "## Unreleased", per RELEASING.md - an unnamed heading is what got the hardening entries filed under a shipped release. 26-F was the last held phase of roadmaps 25-27: it waited three waves for the measuring rig so that a decimation target would be a measured number rather than a guess. With it in, that whole batch is closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes roadmaps 25-27 completely. 26-F was the last held phase — it waited through three waves on purpose, because a decimation target had to be a measured number rather than a guess, and #211's rig supplied the measurement.
Evidence
Three new suites green —
import-budget33/33,import-decimate37/37 (two peers),import-reduce-gate22/22 — and unit went to 143/143. svelte-check held at 341/47 at every commit.Decisions worth reading
vite.configpre-bundles it, because without that dev reloaded the page on the first reduction.reducedstamp survives, and the menu entry then says plainly that the original is no longer held instead of silently failing.A replication bug this lane found outside its own scope
commandsHandler.sendObjectsnever sent userData on the ROOT group message of a placed import, so a root stamp — AI-generation provenance, and nowreduced— never reached peers at all. It now sends the same additive field the nested-group branch already did.Known reds, each confirmed identical on base
animated-models"peers in phase" ·fbx-mtl-import".mtl texture resolved to a data URL" · threeimport-duplicatesSettings checks ·explorer-drop"prefab replicated in place on B" ·undo"the placed position replicates" · and newly added to the known list,origin-rows"B has the cubes in the group" and "peer B agrees", which fail identically withcommandsHandlerat base.🤖 Generated with Claude Code