Vec2 & Vec3
The SDK's vector types. Fields are plain mutable numbers (v.x = 3 works), but every method is
pure — a.add(b) returns a NEW vector and never touches a. The only mutators are set and
copy. Both types share the same API; Vec3 adds 3D-only operations (cross product as a vector,
quaternion/matrix transforms). See math value semantics.
Everywhere a vector is accepted, a raw tuple works too (Vec2Like / Vec3Like): a wrapper, a
[x, y] / [x, y, z] tuple, a number[], or a Float32Array. Vectors are iterable, so they
spread and destructure like tuples.
At a glance
let pos = new Vec3(0, 0, 0)
const target = new Vec3(3, 0, -4)
const toTarget = target.sub(pos) // (3, 0, -4)
const dir = toTarget.normalize() // (0.6, 0, -0.8) — toTarget is untouched
pos = pos.add(dir.scale(2)) // (1.2, 0, -1.6): add() returns a NEW vector
const inRange = toTarget.lengthSq() < 36 // true — cheaper than length() < 6
const node = new Node()
node.position = [0, 1, 0] // raw tuples work wherever a Vec3 is accepted
const [x, y, z] = node.position // vectors destructure like tuplesCreating
new Vec2(x?, y?) // components; default (0, 0)
new Vec2([3, 4]) // or copy any Vec2Like
new Vec3(x?, y?, z?) // default (0, 0, 0)
Vec2.from(v: Vec2Like): Vec2
Vec3.from(v: Vec3Like): Vec3
Vec2.zero / .one / .up / .down / .left / .right
Vec3.zero / .one / .up / .down / .left / .right / .forward / .backthe static constants are getters — each access returns a fresh instance, so mutating
one is safe. Vec3.forward is −Z (cameras look down −Z), Vec3.back is +Z. Vec2.up is
+Y (2D world space is Y-up).
Mutation — the only two methods that change this
v.set(x, y): this // Vec3: set(x, y, z)
v.copy(other: Vec2Like): thisArithmetic (pure — each returns a new vector)
a.add(b) a.sub(b) a.mul(b) a.div(b) // component-wise; b may be a tuple
a.scale(s) // multiply by a scalar
a.scaleAndAdd(b, s) // a + b·s in one call
a.negate()
a.withX(x) a.withY(y) // copy with one component replaced (Vec3: withZ)Products & measures
a.dot(b): number
a.length(): number a.lengthSq(): number
a.distanceTo(b): number a.distanceSqTo(b): number
a.angle(b): number // angle between the vectors, RADIANS (0 if either is zero)
a.normalize(): Vec // unit vector; a zero vector normalizes to zeroPrefer the Sq variants for comparisons — they skip the square root.
Interpolation & clamping
a.lerp(b, t) // component-wise blend, t usually 0..1
a.clamp(min, max) // component-wise clamp between two vectors
a.min(b) a.max(b) // component-wise min / maxVec2 only
a.cross(b): number // scalar z of the 3D cross — signed parallelogram area
a.perp(): Vec2 // (-y, x): 90° CCW perpendicular
a.rotate(rad, origin?) // rotate CCW by RADIANS around origin (default [0, 0])
a.heading(): number // atan2(y, x) — the vector's angle in RADIANSVec3 only
a.cross(b): Vec3
a.reflect(normal) // mirror about a UNIT normal: v − 2(v·n)n
a.project(onto) // vector projection of a onto `onto` (zero `onto` → zero)
a.rotateX(rad, origin?) // rotate the point around an axis through origin, RADIANS
a.rotateY(rad, origin?)
a.rotateZ(rad, origin?)
a.rotate(q) // rotate by a Quat or raw [x, y, z, w]
a.transform(m) // transform as a POINT by a Mat4 / raw column-major length-16 array
// — applies translation and the perspective divideFor a direction (no translation), use Mat4.transformDirection instead of
transform.
Interop
a.equals(b, eps? /* 1e-6 */): boolean // relative-epsilon compare
a.clone(): Vec
a.toArray(): [number, number] // Vec3: [number, number, number]
const [x, y] = a; [...a] // iterableCompiler output — __compWrite / __compOp
node.position.x = 3 spelled literally would write into the fresh copy the getter returns and
change nothing. chisel's comp_write pass rewrites that exact shape — <owner>.<prop>.<x|y|z|w>
with =, +=, -=, *= or /= — into these helpers, which have the owner apply the write:
node.position.x = 3 // → __compWrite(node, "position", "x", 3)
node.position.y += 1 // → __compOp(node, "position", "y", 0, 1) op: 0 +=, 1 -=, 2 *=, 3 /=The owners: Node, Node2D and Camera2D (position), Physics (velocity,
angularVelocity), CharacterController and CharacterController2D (velocity); any other
object gets the plain write as spelled. Only the direct spelling is covered — a copy stored in a
variable first is still a copy. They are compiler output — never call them by hand; write
the component assignment.
Pitfalls
// ✗ expecting methods to mutate — they never do
dir.normalize() // discards the result; dir unchanged
dir = dir.normalize() // ✓ reassign
// ✗ mutating a STORED node transform copy — getters return fresh values
const p = node.position; p.x = 3 // silent no-op (see conventions)
node.position.x = 3 // ✓ the direct spelling is compiled into a write through the nodeSee also
- Mathf — scalar helpers (
clamp,damp,smoothDampon arrays) - Quat — rotations;
Vec3.rotate(q)isQuat.rotateVec3from the other side - Mat4 — full transforms;
transformPoint/transformDirection - Conventions — value semantics, tuple interop