Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part oftoolbox capture.toolbox·originates fromworld 07158574-5…

viewpoints

The named camera stations a capture can be taken from, and the rule that decides what a name like `front` means.

by◐lumi·posted 2mo ago
What it does

viewpoints

The named camera stations a capture can be taken from, and the rule that decides what a name like front means.

A viewpoint is a direction with an image-up: where the camera stands relative to what it is looking at, and which way is up in the resulting picture. Naming one replaces reverse-engineering a {yaw, pitch} pair, and names the seven stations that answer "is this built correctly" — the six sides and the corner.

ViewpointSees
frontthe side the subject faces
backthe opposite side
rightthe subject's right
leftthe subject's left
topplan view, the subject's front toward the top of the image
bottomthe underside
isothe corner view where all three axes project equally

An entity faces its local -Z, so the camera that sees its front sits on its +Z. top and bottom take their image-up from the subject's forward axis, because world up is undefined when you are looking straight down it — that is what puts the subject's front at the top of a plan view instead of leaving the roll to whatever the yaw happened to be.

Basis

basis says which axes the name is measured against, and it has no default:

  • "local" — the subject's own axes. front is the side the subject faces, whichever way it is turned, and the frame is fitted to the subject's own extents (entity:orientedBounds()).
  • "world" — the world axes. front is the side facing world +Z, whatever the subject is doing.

For a crate turned 40 degrees these are different pictures, so the caller states which one it wants. A default would mean one call site's front is the crate's front and another's is the world's, and the difference only shows up in the image. With no subject to take axes from — a capture framed on a world position rather than an entity — both values mean the world axes, and both are accepted.

Orthographic size

orthoHeightFor answers the question an orthographic frame asks that a perspective one does not: how big is the frame? A perspective camera backs off until the subject fits; an orthographic camera does not converge, so its size is decided by the subject alone — project the bounds' eight corners onto the camera's up and right axes and take whichever needs more room.

Its half extents and axes must be stated in the same frame. A resolved viewpoint carries both: frame axes to use with local-frame extents, and its world axes to use with world extents.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

capture.toolbox/viewpoints.module/init.luau The named camera stations a capture can be taken from, and the rule that decides what a name like "front" means. Pure math and validation: nothing here touches an entity, a camera or the renderer, so the framing a capture stands on can be reasoned about (and tested) on its own. `shared.module` resolves the subject and drives the camera; this decides where that camera goes and which way is up in the image.

normalize(x: number, y: number, z: number) → void

argtypedescription
xnumber
ynumber
znumber

basisFrom(fx: number, fy: number, fz: number, hx: number, hy: number, hz: number) → void

Build an orthonormal camera basis from a look direction and an up hint. `right` comes from forward x up, and `up` is rebuilt from the result, so the returned three are perpendicular even when the hint was not exactly so.

argtypedescription
fxnumber
fynumber
fznumber
hxnumber
hynumber
hznumber

resolve(name: any, basis: any, rotation: any?) → void

Resolve a viewpoint name and a basis into camera axes. `basis` is required and has no default. With a subject present, "front" can mean the side the subject faces or the side facing world +Z, and those are different pictures — so the caller says which rather than a default deciding and each call site guessing differently. `rotation` is the subject's world rotation, needed only for basis "local". Returns a `ResolvedViewpoint`, or `(nil, reason)`.

argtypedescription
nameany
basisany
rotationany?

orthoHeightFor(half: { number }, axes: any, aspect: number?, margin: number?) → number

The world-space vertical extent an orthographic frame must cover to hold a box of `half` half-extents seen along `axes`. An orthographic frame does not converge, so its size is decided by the subject alone: project the box's eight corners onto the camera's up and right axes and take whichever needs more room, the height directly or the width divided back through the aspect. `half` and `axes` must be stated in the same frame — pass a resolved viewpoint's `frame` axes with local-frame extents, or its world axes with world extents.

argtypedescription
half{ number }
axesany
aspectnumber?
marginnumber?
⌬ Types
ResolvedViewpoint = {

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

6items
▣
module · born here
❒asset
# 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 as `entity(id).position` and the aim is written as `entity(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: ```lua 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 alias `Transform.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 * qb` composition. - `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 ```luau -- 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 Transform` directive promotes the module's typed functions onto the runtime universe's globals bucket, so `Transform.*` is available without any per-source `require`. - Entity-aware functions (`lookAt`, `distanceBetween`, `directionBetween`) report a missing entity or a missing transform in their return value rather than raising: `lookAt` answers `false, "unresolved"` / `"no-transform"` / `"incomplete-target"` / `"degenerate"`, `distanceBetween` answers `nil`, and `directionBetween` answers zeros. - Quaternion APIs operate on raw `(qx, qy, qz, qw)` tuples for parity with the entity proxy's `localRotation.get`/`set`. Use `Transform.quatIdentity()` rather than hand-rolling `(0, 0, 0, 1)`. - `Transform.slerp` flips the second quaternion if `dot < 0` to take the shortest path, and falls back to lerp+normalize when the inputs are within `dot > 0.9995` to avoid `1/0` near-parallel issues.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.