# Spatial placement Place 3D objects by anchoring to real geometry, reading the bounds back, seating with a raycast, and verifying numerically. Placing objects in 3D by feeding parallel position constants is how an agent floats a lamp above a table or sinks a crate into the floor. Nothing is anchored to what actually landed, so a single wrong number is invisible until you look. The `spatial` group exists to place against **real geometry** instead, and the discipline is always the same four beats. > **The spatial rule** > > Anchor to a realized node, read its real world bounds back, seat the next piece with a raycast, and verify the result numerically. Never place by eyeballing one screenshot, and never chain off a constant you assumed. ## 1. Anchor, then read the bounds back Place the anchor piece, then ask the engine where it actually is in world space. `spatial bounds` returns the true world-space AABB (a union of the node's descendant geometry, not just the root's local box), so you build the next piece off measured reality. ```bash swallowtail spatial bounds --node-path Table # -> { "center": "Vector3(4, 0.75, -6)", "size": "Vector3(2, 0.9, 1.2)", # "min": "Vector3(3, 0.3, -6.6)", "max": "Vector3(5, 1.2, -5.4)" } ``` > **Note** > > `node set --property position` is **local to the parent**. `spatial align`, `place_on`, and `distribute` work in **world space** and write back the local position that lands the piece where you asked. Mixing the two is the usual cause of a piece that is right in isolation and wrong once parented. ## 2. Seat with a raycast, at the right tier There are three seating tiers. Pick the one the surface demands, cheapest first. | Tier | Command | Use when | | --- | --- | --- | | Mesh AABB | `spatial place_on --node-path X --surface-from Floor` | the ground is flat; drop onto the top of a surface's box | | Footprint down-ray | `spatial place_on --node-path X --samples 3 --conform` | the ground is sloped, stepped, or uneven; sample under the footprint and conform | | Geometry snap | `spatial snap --node-path Bolt --to Beam --mode vertex` | you need an exact vertex or face, not a bounding box | ```bash # flat: sit the lamp on the table's top surface swallowtail spatial place_on --node-path Lamp --surface-from Table # uneven: conform a crate to sloped terrain under its footprint swallowtail spatial place_on --node-path Crate --samples 3 --conform # exact: snap a bolt to the nearest real vertex of a beam swallowtail spatial snap --node-path Bolt --to Beam --mode vertex ``` For a free raycast (no target node), cast a ray and place at the hit: ```bash swallowtail spatial raycast --from "Vector3(4,30,-6)" --to "Vector3(4,-30,-6)" # -> { "hit": true, "position": "Vector3(4, 1.2, -6)", "normal": "Vector3(0, 1, 0)" } ``` > **Edit-time physics: rays yes, shape-overlap no** > > At edit time, ray queries (`spatial raycast`, the `place_on` down-ray bundle) work against the scene's colliders. Shape-overlap queries do not: freshly added bodies are not stepped into the broadphase, so `intersect_shape` returns nothing where `intersect_ray` hits. Shape overlap is a runtime concern, reachable via `runtime.eval` in a running game. ## 3. Build composites by chaining off realized bounds For a cluster (a desk with a lamp, papers, and a mug), place the anchor, `spatial bounds` it, then derive each child position from the measured box, not from a guessed constant. Every piece hangs off what actually landed. ```bash swallowtail spatial align --node-path Lamp --to Desk --axes xz --on-top swallowtail spatial distribute --nodes '["Book1","Book2","Book3"]' --axis x --spacing 0.25 swallowtail spatial look_at --node-path Camera --target Desk ``` `spatial relate` compares two nodes' world boxes: the center delta and distance, which axes are centered, the gap on each axis (negative means overlap depth), and whether the boxes intersect. An agent can assert an arrangement from those numbers instead of trusting it. `spatial find_in_region` lists what occupies a world box, and `spatial lint` flags geometry that floats or intersects. ## 4. Verify numerically A screenshot confirms nothing about a 0.05 m float. Read the result back and assert it. ```bash # did the lamp actually land on the table top? swallowtail spatial bounds --node-path Lamp swallowtail spatial relate --node-path Lamp --other Table # -> { "center_delta": "Vector3(0.0000, 0.7000, 0.0000)", "distance": 0.7, # "centered": { "x": true, "y": false, "z": true }, # "gap": "Vector3(-1.1500, 0.0000, -0.7500)", "overlaps": false, ... } # gap.y 0, centered on x and z: resting on the top. Negative x and z gaps are plan-view overlap. # sweep the scene for floaters and interpenetration swallowtail spatial lint --check-floating ``` > **Tip** > > `spatial lint --check-floating` only considers solid geometry (MeshInstance3D, CSGShape3D). A piece counts as supported when its box touches another solid's box, with 5 cm of contact slack, or when it sits over one with a gap of 0.5 m or less. `--float-threshold` changes that gap. Lights, decals, particles, and scattered MultiMesh have no "rests on a surface" meaning and are excluded, so the check does not drown in false positives. ## Why `world_aabb` is the correct bound `spatial bounds` uses a union of every descendant's world geometry, not `global_transform * get_aabb()` on the root alone. In Godot 4 a `Transform3D * AABB` already transforms all eight corners, so it is rotation-correct; the union is what makes a composite node report the bounds you need around the whole object. Anchor to that, and rotation and nested children stay honest.