Skip to content

Local and world frames

A geometry.Element carries data in two different coordinate frames, and nothing in the type system stops you from mixing them. Mixing them is wrong without erroring: the code runs, the numbers look plausible, and the answer is silently rotated or offset.

Field / method Frame Units
Element.Verts element-local metres
Element.Placement local → world transform
Element.BBoxMin / BBoxMax world metres
Element.WorldVerts() world metres
Element.WorldNormal(v) world direction

Meshes are local. Placements and bounding boxes are world.

The trap

// WRONG — mixes frames.
dir := sub(e.Verts[3:6], e.Verts[0:3]) // local direction
pos := midpoint(e.BBoxMin, e.BBoxMax)  // world position

Deriving a direction from Verts and a position from the BBox mixes the two frames. There is no error, no panic, and no NaN — just a direction expressed in the element's own rotated frame paired with a position in the building's.

The rule

Use WorldVerts for positions and WorldNormal for directions.

world := e.WorldVerts()                       // []float32, world X,Y,Z triples
up := e.WorldNormal([3]float64{0, 0, 1})      // local +Z as a world direction

Why directions need their own call

WorldNormal applies only the 3×3 rotation part of the placement. A direction must not pick up the placement's translation — a face normal does not move when the wall it belongs to moves.

Magnitude is preserved, not normalised: a placement composed from IfcAxis2Placement3D is orthonormal and right-handed, so a unit local direction comes back unit-length and a scaled one comes back scaled by the same factor. That orthonormality is also why rotating the direction is the correct transform and no inverse-transpose is needed.

Do not derive a direction by differencing two WorldVerts. It gives the right answer for a rigid placement, but it rounds through float32 twice and loses precision that WorldNormal keeps in float64.

Precision

WorldVerts runs the transform in float64 and rounds to float32 on the way out, so its result agrees with BBoxMin/BBoxMax to float32 precision — not exactly. That gap grows with distance from the origin, which matters on a georeferenced model whose coordinates are in the hundreds of thousands of metres. Code needing exact world positions there should transform through Placement in float64 itself.

WorldVerts also allocates a fresh slice on every call. Hoist it out of a hot loop.

Edge cases

Verts holding fewer than 3 floats returns nil. A trailing partial triple is dropped rather than emitted half-transformed.

GLB output is a third frame

Scene.WriteGLB writes Y-up GLB, because that is what glTF viewers expect. Everything described on this page is IFC-native Z-up. The conversion happens inside WriteGLB and does not affect any field above.