Measure collision fixes from convex pieces, and gate the glb on its own - #36
Open
ZheningHuang wants to merge 3 commits into
Open
Measure collision fixes from convex pieces, and gate the glb on its own#36ZheningHuang wants to merge 3 commits into
ZheningHuang wants to merge 3 commits into
Conversation
… its own
Three changes to the collision QC, in the order the problems were found.
1. THE FIX DISTANCE WAS WRONG, so correct fixes were being thrown away.
FCL decides contact from the raw triangles — the only model that gets a tucked
chair right — but the furniture meshes are not watertight (Table0 alone has 846
connected components), so FCL's penetration DEPTH on them is garbage. The fix
distance therefore came from the two objects' world AABBs, which over-states it
by 1.6-2.8x and can only push along a world axis. Over-stated moves exceed
MAX_NUDGE, get reverted, and the clash ships.
Convex pieces are closed by construction, so their arithmetic is meaningful.
Detection is untouched; the decomposition is asked only HOW FAR to move a pair
the raw triangles have already confirmed is touching, so its ~9 mm
over-approximation can never invent a clash — only size a real one. A single
hull would be far too coarse to detect with: it swallows the entire 80 mm gap
under a tucked chair.
Across the seven built rooms: 13 of 17 fixes exceeded MAX_NUDGE and were
reverted, now 2. Total requested displacement 8.15 m -> 3.49 m. The two that
still fail are genuine — objects overlapping by 0.3-0.8 m, which is a placement
error, not a mismeasured nudge.
Accuracy is governed by CoACD's `preprocess_resolution`, not its `threshold`:
24 -> 128 pieces moves the error 17.2 -> 17.4 mm, while the voxel cell size
tracks it ~1:1. Decomposition runs only for bodies already in contact and is
cached beside the glb on canonical (un-placed) geometry, so eight chairs from
one asset decompose once. Cold 112 s on the worst room, warm ~2 s, and a clean
room pays nothing.
2. THE CHECK NEEDED TWO FILES THAT COULD DISAGREE.
`check_all` wanted a glb AND whichever Room.py was newest — and `checks.py`
picks its layout by mtime from up to five candidates. Wall centrelines,
floor_z/ceiling_z and per-object category are not recoverable from geometry, so
they are now written into the glb as standard glTF `extras` at build time.
`check_glb(path)` is one deterministic function of one file. Verified on all
eight built rooms: findings are identical to the Room.py path, where before
running from the glb alone found 1 of 12 on Elliott-Studio. Unstamped glbs
return {} and fall back, so older builds still work.
`room_qc/collision.py` is the gate — reads only, exit 0 clean / 1 clashes.
Grounding and containment are reported but do not fail it: a floating object
means a missing support and `outside_room` means the scan's wall loop is wrong,
neither of which the collision resolver can repair.
3. NOTHING EVER RE-CHECKED AFTER FIXING.
`correct.py` printed "rebuild the room, then re-run to verify 0 clashes" and
nothing did. `publish` runs it once, so a room ships with whatever the first
pass left — Panda-2 went 8 moves, then 6, then still 6 across runs.
`resolve.py` closes it. A fix only translates objects in the floor plane, so
the same translation applied to the in-memory meshes is what a rebuild would
produce: the loop runs detect -> nudge -> detect entirely in memory and writes
Room.py once. Panda-2 converges 6 -> 0 in 7 rounds; the room whose six fixes
were all previously reverted converges in 5. Verified end to end — resolve,
rebuild in Blender, gate the rebuilt glb: 0 clashes.
Why it stopped is part of the report, because a clash count cannot tell "give
me more rounds" from "this needs a human": converged / capped / nothing_movable
/ max_rounds.
`extract_shell` moves to `room_ops/shell.py` (re-exported, no caller changes) so
`room_ops` can read a SHELL without importing `agent` — the layering test caught
that on the first attempt.
publish still runs correct-then-checks; wiring it to resolve -> build -> gate is
left for a follow-up so this stays reviewable.
pyproject added coacd for the convex fix-distance path but uv.lock was never regenerated, so CI's `uv sync --all-groups --locked` failed before running a single test.
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.
Three changes to collision QC, in the order the problems were found.
1. The fix distance was wrong, so correct fixes were thrown away
FCL decides contact from the raw triangles — the only model that gets a tucked chair right. But the furniture meshes are not watertight (
Table0alone has 846 connected components), so FCL's penetration depth on them is garbage — a desk flush against a wall once reported 1.16 m. The fix distance therefore came from the two objects' world AABBs, which over-states it by 1.6–2.8× and can only push along a world axis. Over-stated moves exceedMAX_NUDGE, get reverted, and the clash ships.Convex pieces are closed by construction, so their arithmetic is meaningful. Detection is untouched — the decomposition is asked only how far to move a pair the raw triangles have already confirmed is touching, so its ~9 mm over-approximation can never invent a clash, only size a real one.
A single convex hull would be far too coarse to detect with — measured, it swallows the entire 80 mm gap under a tucked chair and reports contact:
Effect across the seven built rooms
11 of 17 fixes flip from reverted to applied. The clearest case is a room where all six chair fixes asked for
0.625 m(the chair's own AABB, not a penetration depth) against a 0.30 m cap; the real moves are 0.127–0.250 m. The two that still fail are genuine — objects overlapping by 0.3–0.8 m, which is a placement error, not a mismeasured nudge.Cost
Accuracy is governed by CoACD's
preprocess_resolution, not itsthreshold— 24 → 128 pieces moves the error 17.2 → 17.4 mm, while the voxel cell size tracks it ~1:1:Decomposition runs only for bodies already in contact, and is cached beside the glb keyed on canonical (un-placed) geometry, so eight chairs cut from one asset decompose once. Cold 112 s on the worst room (was 465 s before restricting to bodies in contact), warm ~2 s, and a clean room pays nothing.
2. The check needed two files that could disagree
check_allwanted a glb and whicheverRoom.pywas newest — andchecks.pypicks its layout by mtime from up to five candidates, so it could grade a room that no longer exists.Wall centrelines,
floor_z/ceiling_zand per-object category are not recoverable from geometry, so they are now written into the glb as standard glTFextrasat build time.check_glb(path)is one deterministic function of one file.Verified on all eight built rooms: findings identical to the
Room.pypath. Before, running from the glb alone found 1 of 12 on Elliott-Studio. Unstamped glbs return{}and fall back, so older builds still work.room_qc/collision.pyis the gate — reads only, exit0clean /1clashes. Grounding and containment are reported but do not fail it: a floating object means a missing support, andoutside_roommeans the scan's wall loop is wrong — neither is something the collision resolver can repair.3. Nothing ever re-checked after fixing
correct.pyprinted "rebuild the room, then re-run to verify 0 clashes" and nothing did.publishruns it once, so a room ships with whatever the first pass left — Panda-2 went 8 moves, then 6, then still 6 across runs.resolve.pycloses it. A fix only translates objects in the floor plane, so the same translation applied to the in-memory meshes is what a rebuild would produce — the loop runs detect → nudge → detect entirely in memory and writesRoom.pyonce.That second room is the one whose six fixes were all previously reverted.
Verified end to end —
resolve --apply→ rebuild in Blender → gate the rebuilt glb:0 clashes, exit 0. So the in-memory shortcut is checked against the real artifact, not trusted.Why it stopped is part of the report, because a clash count cannot tell "give me more rounds" from "this needs a human":
converged/capped/nothing_movable/max_rounds.Layering
extract_shellmoves toroom_ops/shell.py(re-exported fromgeometry.py, no caller changes) soroom_opscan read a SHELL without importingagent.tests/test_architecture.pycaught that on the first attempt.Verification
ruff check src testscleanNot in this PR
publishstill runscorrect→checks; wiring it toresolve→build→gateis left for a follow-up so this stays reviewable.