How anchor snapping works

The mechanism behind placing an object by anchor instead of by coordinates, and how to reason about it.

Snapping places an object by asserting that one of its anchors coincides with a pose you already know, instead of computing an absolute position for the object's body frame.

That distinction is the whole idea. You rarely know where an object's body origin should be. You do know where its rim, its base, or its grasp point should be, because that is the part touching something else.

Why the world works this way

Physical labs are full of seats and receptacles. A plate sits in a holder. A tube sits in a rack slot. A lid sits on a jar. In each case the relationship is not "this object is at these coordinates", it is "this feature of one object meets that feature of another".

Anchors let you say that directly. See Anchors for what an anchor is and objects/*.object_model.yaml for the fields.

An anchor is a named frame rigidly bolted to a URDF link. Because it hangs off a link rather than off the world, an anchor on an articulated fixture moves when that fixture's joints move. load_object_anchor() runs forward kinematics from the object's current joint configuration, so the pose it hands back is always current.

The two roles

A snap involves two anchors, playing different parts.

The seat anchor lives on the receiving object. It answers "where should the thing end up". You read it with load_object_anchor(), which returns a dict containing xyz, rpy, wxyz, standoff, width, gripper_variant, and object_id.

The placed anchor lives on the object being moved. It answers "which part of me meets that seat". You name it as a string.

Then you compose them:

seat = load_object_anchor(holder_id, "<seat_anchor_name>")
snap_object_anchor_to_world_pose(object_id, "<placed_anchor_name>", seat["xyz"], seat["wxyz"])

What the runtime actually computes

snap_object_anchor_to_world_pose() does not move the anchor. It solves for the body pose that would put the anchor where you asked.

  1. It reads the object's live world pose, world_P_object.
  2. It reads the placed anchor's live world pose, world_P_anchor.
  3. It derives the rigid offset between them: body_P_anchor = world_P_body.inv() @ world_P_anchor.
  4. It applies the inverse of that offset to your target: world_P_body_new = world_P_target @ body_P_anchor.inv().
  5. It calls snap_object_to_world_pose() with the result.

Step 3 is why this is exact rather than approximate. The offset from body to anchor is fixed by the object model, so recovering it from live state and re-applying it introduces no error.

Snapping pins, it does not parent

This is the part people get wrong most often.

snap_object_to_world_pose() detaches the object from whatever it was mounted to and re-attaches it as a fixed world object at the computed pose. The result is a pose, not a parent-child link. If the receiving fixture later rotates or translates, the snapped object stays exactly where it was put and the two drift apart.

🔁

After you move a fixture that has objects seated in it, re-snap those objects. Reading the seat anchor again picks up the fixture's new joint configuration, so the same two lines of code produce the corrected pose.

Attaching to an arm is the one case where following is automatic. attach_object_to_arm(object_id, arm) mounts the object under the arm's tcp link, so it rides along with motion until detach_object_from_arm(object_id). get_object_tcp_transform(object_id) returns the tcp_P_object transform for an object currently attached.

Grasping and the standoff

An anchor may carry a grasp block. It has three fields: width (gripper opening at the grasp point, in metres), standoff (retract distance for the pre-grasp pose, in metres, defaulting to 0.05), and gripper_variant (a free-form label, defaulting to "stock").

Grasp anchors follow the tcp frame convention, in which +Z is the approach direction, pointing along the tool body and into the grasp. A grasp anchor is therefore directly usable as an inverse kinematics target for the tcp link.

The pre-grasp pose is that same frame backed off along -Z by standoff. anchor_preapproach(anchor, default_standoff=0.0) does this for you, using the anchor's own standoff when it is greater than zero and your fallback otherwise.

So the approach is not an arbitrary direction chosen per skill. It falls out of the anchor's orientation. Whoever authored the object model decided how the gripper should come in by deciding which way +Z points, and every skill that uses that anchor inherits the decision.

Why a small vertical offset shows up in seat anchors

Fixtures often carry a pair of anchors for the same slot: one at the height an object is released, and a second a few millimetres lower for where it actually comes to rest.

The reason is physical. An object released into a seat settles under gravity by a small amount, and the amount is not perfectly repeatable. If a place operation releases at the load height and a later pick descends to the same load height, the fingers close at the object's nominal resting height rather than beneath it.

Encoding the settled pose as its own anchor makes both halves agree. Place releases at the load anchor and then snaps the object to the settled anchor. The later pick approaches the settled anchor. On real hardware this means the fingers close under the settled object instead of pinching at exactly its resting height, which is far more tolerant of the millimetre of variation you get every cycle.

That is a hardware benefit, not a simulation convenience. The simulation determinism is a side effect of getting the physical geometry right.

Failure symptoms

What you seeLikely cause
The object floats one object-height above the seat, or sinks into itThe placed anchor and the seat anchor mean different things. One is a base contact point, the other a grasp pose at mid-height.
The object lands in the right place but rotatedThe two anchors agree on position but their axis conventions disagree. Check which way +Z points on each.
The object was correct, then a fixture moved and it is now hanging in mid-airExpected. Snapping pins to a world pose. Re-snap after the fixture moves.
A place puts the object somewhere plausible but consistently offThe object was attached to the arm by some route that never asserted a known grip, so the body-to-anchor offset the snap recovers is real but the carry pose was never canonical.

The honest limitation

Snapping asserts a pose. It does not measure one.

In simulation that is the point: it removes accumulated drift and keeps long protocols repeatable. But it also means the 3D view will render a clean, perfectly seated object whether or not the real gripper has a good hold on anything.

⚠️

A clean simulation run is not evidence that a marginal grasp is safe on hardware. The viewport is showing you the pose you asserted, not a measurement of the physical arm. Verify grasps physically, at reduced speed, with a hand on the stop, before trusting them in an unattended run.

Related


Did this page help you?