kwabs22 Claude Opus 5.5 commited on
Commit
e0f9b43
Β·
1 Parent(s): f9a8763

Weld before decimating so stalled meshes reach their budget; stop repeated ladder rungs

Browse files

QA round 2 found decimation flooring far above target (4,893 faces in a
400-face page; ladder rungs 1000/250/60 all 1,141 faces). Asset files split
vertices at UV and normal seams, so edges aren't shared and the decimator
stalls. decimate now welds first and re-welds between passes while that
still helps. A split-seam test mesh that stalled at 3,840 now reaches 400.

The ladder labels rungs with their real face count and stops descending
when a rung doesn't reduce. Inline meshes in the page state faces, budget
and floor, so an over-budget mesh is visible instead of silent.

Also archives QA verification round 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

SPEC.md CHANGED
@@ -106,6 +106,9 @@ that exists but maps to the wrong thing shows up before submitting.
106
 
107
  Ladder rungs: original β†’ 20k / 5k / 1k / 250 / 60 faces β†’ convex hull β†’ oriented
108
  box. Where the object stops reading as its word is judged in the discussion.
 
 
 
109
 
110
  ### Mini HTML
111
 
@@ -114,6 +117,8 @@ data object holding the sentence, the reading (word β†’ commits), relations,
114
  unresolved questions, sun, ground, and each object's position, yaw and colour.
115
  Each object also has either inline low-poly geometry (≀400 faces in a scene,
116
  rungs ≀1000 faces in a ladder) or a primitive spec. Vertices are rounded to cm.
 
 
117
  The geometry is inline, so a saved page works after Gradio's temporary file URLs expire.
118
 
119
  **Geometry is for the handoff, not the loop.** Even at 400 faces a page costs
 
106
 
107
  Ladder rungs: original β†’ 20k / 5k / 1k / 250 / 60 faces β†’ convex hull β†’ oriented
108
  box. Where the object stops reading as its word is judged in the discussion.
109
+ Decimation welds split vertices (UV and normal seams) between passes. Rungs are
110
+ labelled with the faces they actually have, and the ladder stops descending when
111
+ a rung doesn't reduce, so a stalled mesh never shows as several identical rungs.
112
 
113
  ### Mini HTML
114
 
 
117
  unresolved questions, sun, ground, and each object's position, yaw and colour.
118
  Each object also has either inline low-poly geometry (≀400 faces in a scene,
119
  rungs ≀1000 faces in a ladder) or a primitive spec. Vertices are rounded to cm.
120
+ Each inline mesh states `faces`, `budget` and `floor`: `floor: true` means the mesh
121
+ wouldn't decimate down to the budget, so the page carries more than asked.
122
  The geometry is inline, so a saved page works after Gradio's temporary file URLs expire.
123
 
124
  **Geometry is for the handoff, not the loop.** Even at 400 faces a page costs
feedback/vli-qa-feedback-2026-10-30.md CHANGED
@@ -105,4 +105,22 @@ Seed-0 baseline reproduced bit-identically from the first session: uid `4459b90d
105
  5. `reject n1` (primitive tier) β†’ `Error: no usable generation asset for 'cat' (generation tier is not wired up yet)` ⚠️ *(documented gap, now confirmed end-to-end)*
106
  6. `check_words "the cat sat and the lamp glowed"` β†’ `ok: true`, lamp `available_in {simready: 1, objaverse: 79}`, `maps_to: "desk_lamp"` βœ…
107
 
108
- **Suggested follow-ups for the agent's own QA:** `simplify_item` end-to-end (ladder rungs + `LADDER_MAX_FACES` skip), NC/ND license skip recording with a live `decision.skipped` entry, `use` back-swap idempotence after a skip shifted indices, and `check_words` on a low-memory/fallback build to observe dictionary drift.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  5. `reject n1` (primitive tier) β†’ `Error: no usable generation asset for 'cat' (generation tier is not wired up yet)` ⚠️ *(documented gap, now confirmed end-to-end)*
106
  6. `check_words "the cat sat and the lamp glowed"` β†’ `ok: true`, lamp `available_in {simready: 1, objaverse: 79}`, `maps_to: "desk_lamp"` βœ…
107
 
108
+ **Suggested follow-ups for the agent's own QA:** `simplify_item` end-to-end (ladder rungs + `LADDER_MAX_FACES` skip), NC/ND license skip recording with a live `decision.skipped` entry, `use` back-swap idempotence after a skip shifted indices, and `check_words` on a low-memory/fallback build to observe dictionary drift.
109
+ ---
110
+
111
+ ## 5. Verification round 2 (2026-10-30, after commits 447d1d2 / f9a8763) β€” deployed Space, live MCP
112
+
113
+ | Fix | Result |
114
+ |---|---|
115
+ | P0-0 html modes | βœ“ default `url` = file link + empty text (verified on `translate_to_scene` and `refine_scene`); `inline` returns the page text. Iterate-loop cost dropped from ~60k to ~2-3k tokens/call β€” measured on this session itself. `skip` code-verified, not yet exercised live |
116
+ | P0-0 rounding | βœ“ pages show 1 cm (2-decimal) coords |
117
+ | P1-1 reroll | βœ“ walks the pool by next-alternative rotation; bonus: exercised the license-skip path live β€” `1f32bc` skipped `by-nc` and recorded in `decision.skipped`, landed on `138377` (`by-sa`), old choice parked at pool end, q0 labels refreshed |
118
+ | P1-2 uid labels | βœ“ `cat [4459b9]`, `cat [138377]`, … |
119
+ | P1-3 primitive reject | βœ“ clean pre-empted error: `'cat' has no tier left after primitive: generation is not wired up yet. Use shape or height_m instead, or rephrase.` |
120
+ | P1-4 example sentence | βœ“ `the cat sat and the lamp glowed` runs with no primitives; lamp via the **SimReady tier working live for the first time** (ForgeCAD/simready-assets `desk_lamp`, metric height 0.472 m from asset, CC-BY-4.0). Note: the SimReady index now builds from `ForgeCAD/…`, not `nvidia/…` β€” P2-7 drift applies to it |
121
+ | P2-5 face budget | **βœ— partially β€” decimation floors, it doesn't fail.** Ladder proved it: 135,934 β†’ 19,999 β†’ 5,000 β†’ **1,141**, then 250/60 rungs return 1,141 (identical GLBs). Quadric decimation stalls per-mesh when it runs out of weldable structure (`watertight: false`, ~1.1 verts/tri in the stalled output); 4459b9's floor is 4,893, so the page still ships 4,893 despite budget 400 |
122
+
123
+ **Round-3 items:**
124
+ 1. **Decimate floor:** weld between passes β€” `out.merge_vertices()` then re-run `simplify_quadric_decimation` in a loop until the target or no further progress (Objaverse GLBs split vertices; the decimator starves on unshared edges). If a floor persists, surface it in the page (`"faces": n, "budget": 400, "floor": true`) instead of silently exceeding.
125
+ 2. **Ladder duplicate rungs:** the rung guard compares against the requested level, so a stalled decimation still appends lower rungs (1000/250/60 all rendered as the same 1,141-face mesh). Stop descending when a rung fails to actually reduce.
126
+ 3. Still open: `use` back-swap after a skip shifts indices; live `skip`-mode; the UI visual click-test (mode radio + preview-from-file β€” code verified, needs eyes).
vli/html.py CHANGED
@@ -28,8 +28,9 @@ def _sky_hex(sun):
28
 
29
 
30
  def _geometry(mesh, budget):
 
31
  m = decimate(mesh, budget) if len(mesh.faces) > budget else mesh
32
- return {"faces": int(len(m.faces)),
33
  "p": np.round(m.vertices, 2).ravel().tolist(), # cm: plenty at low poly, fewer tokens
34
  "i": m.faces.ravel().astype(int).tolist()}
35
 
 
28
 
29
 
30
  def _geometry(mesh, budget):
31
+ """Inline mesh; "floor" marks a mesh that wouldn't decimate down to the budget."""
32
  m = decimate(mesh, budget) if len(mesh.faces) > budget else mesh
33
+ return {"faces": int(len(m.faces)), "budget": budget, "floor": bool(len(m.faces) > budget),
34
  "p": np.round(m.vertices, 2).ravel().tolist(), # cm: plenty at low poly, fewer tokens
35
  "i": m.faces.ravel().astype(int).tolist()}
36
 
vli/simplify.py CHANGED
@@ -22,12 +22,31 @@ def base_color(mesh):
22
  return np.array([180, 180, 180, 255], dtype=np.uint8)
23
 
24
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  def decimate(mesh, faces):
 
 
 
 
 
26
  color = base_color(mesh)
27
- # fast-simplification rejects read-only arrays (cached views), so hand it fresh copies
28
- src = trimesh.Trimesh(np.array(mesh.vertices, dtype=np.float64), np.array(mesh.faces, dtype=np.int64),
29
- process=False)
30
- out = src.simplify_quadric_decimation(face_count=faces)
 
 
31
  out.visual = trimesh.visual.ColorVisuals(out, face_colors=np.tile(color, (len(out.faces), 1)))
32
  return out
33
 
@@ -41,8 +60,13 @@ def ladder(mesh):
41
  color = base_color(mesh)
42
  rungs = [("original", mesh)]
43
  for n in FACE_LEVELS:
44
- if n < 0.8 * len(rungs[-1][1].faces):
45
- rungs.append((f"{n} faces", decimate(mesh, n)))
 
 
 
 
 
46
  rungs.append(("convex hull", _recolor(mesh.convex_hull, color)))
47
  box = mesh.bounding_box_oriented
48
  rungs.append(("bounding box", _recolor(trimesh.Trimesh(box.vertices, box.faces), color)))
 
22
  return np.array([180, 180, 180, 255], dtype=np.uint8)
23
 
24
 
25
+ def _welded(mesh):
26
+ """Geometry only, with coincident vertices merged, in fresh writable arrays.
27
+
28
+ Asset files split vertices at UV and normal seams; the decimator can't collapse
29
+ edges that aren't shared, so an unwelded mesh stalls far above its target.
30
+ fast-simplification also rejects read-only arrays (cached views), hence the copies.
31
+ """
32
+ m = trimesh.Trimesh(np.array(mesh.vertices, dtype=np.float64), np.array(mesh.faces, dtype=np.int64))
33
+ return trimesh.Trimesh(np.array(m.vertices, dtype=np.float64), np.array(m.faces, dtype=np.int64),
34
+ process=False)
35
+
36
+
37
  def decimate(mesh, faces):
38
+ """Reduce toward `faces`, re-welding between passes while that still helps.
39
+
40
+ The result can stay above `faces` when the mesh has no more structure to collapse
41
+ (many disconnected parts): callers compare len(result.faces) with what they asked.
42
+ """
43
  color = base_color(mesh)
44
+ out = _welded(mesh)
45
+ while len(out.faces) > faces:
46
+ nxt = _welded(out.simplify_quadric_decimation(face_count=faces))
47
+ if len(nxt.faces) > 0.95 * len(out.faces):
48
+ break
49
+ out = nxt
50
  out.visual = trimesh.visual.ColorVisuals(out, face_colors=np.tile(color, (len(out.faces), 1)))
51
  return out
52
 
 
60
  color = base_color(mesh)
61
  rungs = [("original", mesh)]
62
  for n in FACE_LEVELS:
63
+ prev = len(rungs[-1][1].faces)
64
+ if n >= 0.8 * prev:
65
+ continue
66
+ rung = decimate(mesh, n)
67
+ if len(rung.faces) > 0.8 * prev:
68
+ break # decimation stalled: lower levels would repeat this mesh
69
+ rungs.append((f"{len(rung.faces)} faces", rung))
70
  rungs.append(("convex hull", _recolor(mesh.convex_hull, color)))
71
  box = mesh.bounding_box_oriented
72
  rungs.append(("bounding box", _recolor(trimesh.Trimesh(box.vertices, box.faces), color)))