Builtin Library
README.md
transform
Math helpers for positions, rotations, and directions on transforms.
Exposed as the global Transform table via --!global Transform — no
explicit require needed in user code. Functions that take an entity
accept either an entity ID string or an entity proxy table from
entity("id").
Exports
Look-at and entity-aware helpers:
Transform.lookAtQuat(fx, fy, fz, tx, ty, tz) -> (qx?, qy?, qz?, qw?)— quaternion from origin toward target. Nil when degenerate.Transform.lookAt(entity, txOrTarget, ty?, tz?) -> (boolean, string?)— make an entity face a world position or another entity. Both slots read world space: the subject and an entity target are read asentity(id).positionand the aim is written asentity(id).rotation, so a parent under either one still leaves the aim on the point named. Returns whether the rotation was written, and the reason when it was not.Transform.distance(x1, y1, z1, x2, y2, z2) -> number— Euclidean distance between two points.Transform.distanceBetween(entityA, entityB) -> number?— distance between two entities' world positions. Nil when either is unresolvable.Transform.direction(fromX, fromY, fromZ, toX, toY, toZ) -> (dx, dy, dz)— unit direction vector.Transform.directionBetween(entityA, entityB) -> (dx, dy, dz)— unit world-space direction between two entities' world positions.
Rotation shapes:
A quaternion constructor here returns the four components as four separate values, so a caller either names them or braces the call to make one table:
local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.rad(90))
entity("cam").localRotation = { Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) }
A rotation-taking surface reads that table through
Transform.toQuaternion, which also takes euler DEGREES — so
{ qx, qy, qz, qw }, { x =, y =, z =, w = }, { pitch, yaw, roll } and
{ pitch =, yaw =, roll = } all mean the same thing wherever a rotation is
assigned: entity(id).rotation / .localRotation, entityOps.spawn,
entityOps.transform, and the capture viewpoints.
Transform.toQuaternion(rotation, label?) -> { qx, qy, qz, qw }— the shared reading of a rotation a caller wrote. Raises when the value matches no form, naming what arrived; a value that is one of the shapes a quaternion helper returns is named as such along with the packing it goes in as.Transform.tryQuaternion(rotation, label?) -> ({ qx, qy, qz, qw } | nil, message?)— the same reading without raising, for a surface that wants to raise the message at its own caller's line.Transform.readVec3(value, label?) -> { x, y, z }— the same for a vector.Transform.snapVec3(v, step) -> { x, y, z }— quantize a vector to a step grid.
Quaternion construction / conversion:
Transform.quatFromYaw(yaw),Transform.quatFromYawPitch(yaw, pitch),Transform.quatFromAxisAngle(ax, ay, az, angle)— quaternion constructors.Transform.quatIdentity()— identity quaternion.Transform.euler(qx, qy, qz, qw) -> (yaw, pitch, roll)and the named aliasTransform.quatToEuler.Transform.eulerToQuat(yaw, pitch?, roll?)— euler-to-quaternion in YXZ order.
Lerps and interpolation:
Transform.lerp(ax, ay, az, bx, by, bz, t) -> (x, y, z)— vec3 lerp.Transform.lerp1(a, b, t) -> number— scalar lerp.Transform.normalizeAngle(a) -> number— wrap angle into[-pi, pi].Transform.lerpAngle(a, b, t) -> number— shortest-arc angle lerp.Transform.slerp(ax, ay, az, aw, bx, by, bz, bw, t) -> (qx, qy, qz, qw)— quaternion slerp with shortest-path and near-parallel fallback.
Quaternion operations:
Transform.quatMul(...) -> (qx, qy, qz, qw)—qa * qbcomposition.Transform.quatInverse(qx, qy, qz, qw) -> (qx, qy, qz, qw)— inverse (= conjugate for unit quats).Transform.quatRotateVec(qx, qy, qz, qw, vx, vy, vz) -> (x, y, z)— rotate a vec3 by a quaternion.
Pose helpers:
Transform.orbit(centerX, centerY, centerZ, radius, height, angle) -> (x, y, z, qx, qy, qz, qw)— orbital pose facing the center.Transform.worldToLocal(...)/Transform.localToWorld(...)— pose-space conversions.
Nested Transform.vec.* namespace (component-wise vec3):
Transform.vec.add,sub,scale,dot,cross,length,normalize.
Types:
Vec3 = { x: number, y: number, z: number }EntityRef = string | { entityId: string }
Usage
-- Look-at by coordinates or by target entity:
Transform.lookAt("cam", 0, 1, 0)
-- an entity target resolves to that entity's world position
local aimed, why = Transform.lookAt("cam", "box")
-- Orbit pose around a point:
local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t)
entity.find("cam").localPosition = { x, y, z }
entity.find("cam").localRotation = { qx, qy, qz, qw }
-- Quaternion math:
local qx, qy, qz, qw = Transform.quatFromYawPitch(math.pi / 4, 0)
local sx, sy, sz, sw = Transform.slerp(0, 0, 0, 1, qx, qy, qz, qw, 0.5)
-- Component-wise vec3 helpers:
local nx, ny, nz = Transform.vec.normalize(1, 1, 0)
Notes
- The
--!global Transformdirective promotes the module's typed functions onto the runtime universe's globals bucket, soTransform.*is available without any per-sourcerequire. - Entity-aware functions (
lookAt,distanceBetween,directionBetween) report a missing entity or a missing transform in their return value rather than raising:lookAtanswersfalse, "unresolved"/"no-transform"/"incomplete-target"/"degenerate",distanceBetweenanswersnil, anddirectionBetweenanswers zeros. - Quaternion APIs operate on raw
(qx, qy, qz, qw)tuples for parity with the entity proxy'slocalRotation.get/set. UseTransform.quatIdentity()rather than hand-rolling(0, 0, 0, 1). Transform.slerpflips the second quaternion ifdot < 0to take the shortest path, and falls back to lerp+normalize when the inputs are withindot > 0.9995to avoid1/0near-parallel issues.