Skip to content

Check what holds every object up, so nothing floats - #37

Open
ZheningHuang wants to merge 1 commit into
qc/collision-convex-mtvfrom
qc/support-check
Open

Check what holds every object up, so nothing floats#37
ZheningHuang wants to merge 1 commit into
qc/collision-convex-mtvfrom
qc/support-check

Conversation

@ZheningHuang

Copy link
Copy Markdown
Member

Stacked on #36.

The clash check asks whether two things occupy the same volume. This asks the opposite question: does every object touch the thing that holds it up. Indoors there are only four answers — the floor, a wall, the top of another object, or the ceiling — and an object matching none of them is floating.

The support relation is derived, never declared, so the scene graph falls out of the same pass: the surface an object rests on IS its parent. That turns "everything traces back to structure" into a graph check rather than more geometry.

Floor
├── Table0
│   ├── Book0
│   └── Cup0
└── Sofa0
Wall0
└── Shelf0
    └── Mug0

Mug0 → Shelf0 → Wall0 never touches the floor and is still correct — which is why the rule is "the chain ends at structure", not "the object is above the floor".

How

Rays, not boxes. An object's box top is not its support surface: a table's box top is the tabletop, but a chair's is the backrest, and nothing rests on a backrest. Rays start from the object's own extreme vertices rather than a grid over its footprint — a grid sends most of its rays through the empty air between a chair's legs, while the extreme vertices are the feet.

Which face probes which support. The bottom face only answers the downward question; a picture on a wall has nothing at all beneath it. So each hypothesis probes the face pointing at its own candidate:

hypothesis probe
floor / on another object bottom face, rays cast down — which body is hit decides the parent
wall hanging back face vs the wall plane (exact arithmetic, no rays)
ceiling hanging top face vs the ceiling plane

Walls and the ceiling are planes we already know from the SHELL, so only the downward case needs rays — the only case where the identity of the supporting body is in question.

Report only. A floating object is a missing support, not a misplaced one — the shelf it belongs on was never built — so dropping it to the floor would hide the real defect. Nothing here writes Room.py.

Consumes the same bodies dict as collision_mesh.build_bodies, so the two checks share one decomposition of the room and cannot disagree about what an object is.

Thresholds set against a real room, not guessed

  • Wall mounts get their own, much looser tolerance. A bracket or batten legitimately holds a thing off the wall: on Elliott-Studio a wall-hung TV and a wall cabinet both stand 6 cm proud, and at the resting tolerance both read as floating in mid-air. Safe to loosen because the downward test runs first and wins, so nothing standing on the floor can be captured by it.
  • The contact-face band is 2 cm, not razor-thin. A base a millimetre out of level would otherwise offer a hairline strip of feet. Widening is free — a candidate that does not reach its support is dropped by its ray gap anyway.
  • Buried rays count toward the stability hull. A face sunk into its support is still held up by it, and its burial is already its own finding; judging stability on the unburied rays alone turned one defect into two.

Two cases the first real room forced

  • Objects can rest on several bodies — a plank across two boxes, a tray bridging two shelf levels. Keeping only the modal one puts a steady plank's centre in the gap between the contact patches and calls it unstable.
  • A pair buried in each other is reported from both sides, and some burial is correct (a sink in its counter). Now one finding per pair, filtered through the same EXPECTED_CONTAINMENT table the clash check uses, so the two gates cannot contradict each other.

support_pairs() exposes the graph as an allow-list: resting on something is touching it, so a book on a shelf is contact the clash check should expect. That is the job EXPECTED_CONTAINMENT does by category, except derived from geometry — so it covers pairs nobody thought to enumerate.

It finds a bug in the current gate

On Elliott-Studio (20 objects) the collision gate reports four objects floating — Storage1/2/8/9. All four are wall-mounted cabinets: check_all exempts only _FLOOR_STANDING categories and storage is in that set. The support check resolves all four to their walls.

I have not wired collision.py's grounding section to defer to this, since #36 is still in flight there. Happy to as a follow-up.

Two findings survive on that room, both real: Sofa0 rests on the floor along a single 5 cm strip (the mesh is tilted ~2.7°), and Refrigerator0 is 0.092 m into StorageRun0 — which the collision gate independently reports as a 0.771 m overlap, so it is a genuine clash and EXPECTED_CONTAINMENT was left alone.

Notes

python -m litereality_agent.pipeline.room_qc.support --room <room dir> [--graph]

Test plan

  • tests/test_support.py — 10 tests, synthetic boxes, no capture/Blender/glb needed: the ordinary floor→table→book chain; a wall-hung shelf with a mug on it (the case the bottom face cannot answer); a plank across two boxes (must read stable); a cup half off the table edge (must read unstable); a bowl sunk into the tabletop; a lamp in mid-air; a support cycle.
  • Full suite green against Measure collision fixes from convex pieces, and gate the glb on its own #36's tree: 578 passed, 2 skipped.
  • ruff check clean on the new files.

The clash check asks whether two things occupy the same volume. This asks
the opposite question: does every object TOUCH the thing that holds it up.
Indoors there are only four answers -- the floor, a wall, the top of another
object, or the ceiling -- and an object matching none of them is floating.

The support relation is derived, never declared, so the scene graph falls
out of the same pass: the surface an object rests on IS its parent. That
turns "everything traces back to structure" into a graph check rather than
more geometry.

Rays, not boxes. An object's box top is not its support surface: a table's
box top is the tabletop, but a chair's is the BACKREST, and nothing rests on
a backrest. Rays start from the object's own extreme vertices rather than a
grid over its footprint, because a grid sends most of its rays through the
empty air between a chair's legs while the extreme vertices ARE the feet.

Which face probes which support matters as much as the rays. The bottom face
only answers the DOWNWARD question -- a picture on a wall has nothing at all
beneath it -- so each hypothesis probes the face pointing at its own
candidate: bottom for floor/on-object, back for a wall, top for the ceiling.
Walls and the ceiling are planes we already know from the SHELL, so those
two are exact arithmetic and cost nothing.

Report only. A floating object is a MISSING SUPPORT, not a misplaced one --
the shelf it belongs on was never built -- so dropping it to the floor would
hide the real defect. Nothing here writes Room.py.

Three thresholds were set against a real room (Elliott-Studio) rather than
guessed:

  * wall mounts get their own, much looser tolerance. A bracket or batten
    legitimately holds a thing off the wall; a wall-hung TV and a wall
    cabinet both stand 6 cm proud and at the resting tolerance read as
    floating in mid-air. Safe to loosen because the downward test runs first
    and wins, so nothing standing on the floor can be captured by it.
  * the contact-face band is 2 cm, not razor-thin: a base a millimetre out of
    level would otherwise offer a hairline strip of feet. Widening is free,
    since a candidate that does not reach its support is dropped by its gap.
  * buried rays count toward the stability hull. A face sunk into its support
    is still HELD UP by it and its burial is already its own finding; judging
    stability on the unburied rays alone turned one defect into two.

Two more cases the first real room forced:

  * objects can rest on SEVERAL bodies -- a plank across two boxes, a tray
    bridging two shelf levels. Keeping only the modal one puts a steady
    plank's centre in the gap between the patches and calls it unstable.
  * a pair buried in each other is reported from both sides, and some burial
    is correct (a sink in its counter). One finding per pair, filtered
    through the same EXPECTED_CONTAINMENT table the clash check uses, so the
    two gates cannot contradict each other.

`support_pairs` exposes the graph as an allow-list: resting on something is
touching it, so a book on a shelf is contact the clash check should expect.
That is the job EXPECTED_CONTAINMENT does by category, except derived from
geometry, so it covers pairs nobody thought to enumerate.

On Elliott-Studio (20 objects) this resolves four objects that the collision
gate reports as floating -- Storage1/2/8/9 are wall-mounted cabinets, and
check_all exempts only _FLOOR_STANDING categories, which `storage` is in.

Relocking for rtree also picks up coacd, which #36 added to pyproject but
never locked.

Docs in doc/QC/support_check.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant