flan/vendor/raylib/vector.fln

217 lines
11 KiB
Plaintext

;;;; Vector arithmetic over raylib's Vector2 and Vector3, in Flan.
;;;;
;;;; This is raymath, and raymath is the one part of raylib that cannot be
;;;; bound at all. raymath.h defines every one of its functions `static
;;;; inline` (RMAPI expands to it), so Vector2Add and Clamp and Remap have no
;;;; symbol in libraylib for `declare-c` to name — not a signature the
;;;; importer gets wrong, not a struct the package has not described, but
;;;; nothing to link against. TODO.org, "raymath is written in Flan, because
;;;; static inline has no symbol", records it and names the two ways out: write
;;;; the arithmetic in Flan, or a small C file re-exporting them as symbols.
;;;;
;;;; It is written in Flan, and the measurement that decided it was already
;;;; taken: examples/shapes-following-eyes.fln is an example whose every
;;;; line is vector maths, ported without a vector library, and its own
;;;; header reports that this cost nothing — the C does not use raymath there
;;;; either. A C shim would buy identical arithmetic at the price of a
;;;; compilation unit in the build, a second place raylib's semantics are
;;;; written down, and a third target's worth of it for the web build.
;;;;
;;;; Every function here is raymath's, semantics included, and the ones where
;;;; that is not obvious say so. The one worth knowing without reading: at
;;;; zero length, v2-normalize and v3-normalize answer the zero vector rather
;;;; than dividing and producing NaNs. raymath makes that choice and a caller
;;;; who has raymath in mind would be surprised by the other one.
;;;;
;;;; ── Why this is a file of its own ───────────────────────────────────
;;;;
;;;; The split is on `declare-c`, not on "idiomatic". raylib.fln is the
;;;; package's statement about C: every line in it is a declaration or a thin
;;;; wrapper over one, it is the file the header check reads hand-written
;;;; signatures out of, and a wrong line in it stops the build. There is not
;;;; one `declare-c` below and there never will be, because there is nothing
;;;; to declare — so nothing here can be checked against a header, and
;;;; nothing here can be made wrong by raylib changing. A reader who wants to
;;;; know what the package claims about C should not have to walk past four
;;;; hundred lines of float arithmetic to find out, and 1300 lines of
;;;; raylib.fln is already the argument against adding to it.
;;;;
;;;; A package is a directory, so this is simply another .fln beside the
;;;; others and is qualified `rl/` like the rest of it.
;;;;
;;;; ── Names ───────────────────────────────────────────────────────────
;;;;
;;;; `v2-` and `v3-`, not `vector2-`. These appear nested inside each other —
;;;; `rl/v2-add(p, rl/v2-scale(d, t))` is the ordinary shape — and the longer
;;;; spelling puts more characters between the reader and the arithmetic than
;;;; it puts meaning. The prefix still says the type, which is the part a
;;;; language without generics needs it to say.
;;;;
;;;; ── What is NOT here, on purpose ────────────────────────────────────
;;;;
;;;; `clamp` and `lerp`. Both are already in the prelude — clamp as a macro
;;;; (prelude.ml, "clamp is a macro and not a function"), lerp as a function
;;;; — and both are unqualified names every program already has;
;;;; examples/textures-fog-of-war.fln calls the prelude's clamp today. A
;;;; second `rl/lerp` would not even be the same function: the prelude writes
;;;; the weighted sum `(1-t)a + tb`, which returns b exactly at t = 1.0,
;;;; where raymath writes `a + t*(b - a)`, which does not once rounding is
;;;; involved. Shipping both under names one letter apart is a bug waiting
;;;; for whoever picks the wrong one. So: use the prelude's, and what is
;;;; added below is the neighbours the prelude does not have.
;; ── f32, the scalars raymath has and the prelude does not ───────────
;; Where `value` falls between `start` and `end`, as 0.0 at start and 1.0 at
;; end. raymath spells this `Normalize`, which collides with the vector
;; normalize two sections down and means something unrelated to it; it is the
;; inverse of lerp and is named for that. start = end is a division by zero,
;; as it is in raymath: an empty range has no answer and inventing one would
;; hide the caller's bug.
fn inverse-lerp(value: f32, start: f32, end: f32) -> f32
(value - start) / (end - start)
;; raymath's Remap, to the character: inverse-lerp on the input range, then
;; lerp on the output range, and NOT clamped to either. A value outside the
;; input range maps outside the output range, which is what makes it usable
;; for extrapolation — a caller who wants it bounded writes the prelude's
;; clamp around it and can see that they did.
;;
;; Written as one expression rather than as lerp(out-start, out-end,
;; inverse-lerp(...)) because the prelude's lerp is the weighted-sum form
;; and raymath's Remap is the a + t*(b - a) form; composing them would be a
;; different function in the last bit.
fn remap(value: f32, in-start: f32, in-end: f32, out-start: f32, out-end: f32) -> f32
(value - in-start) / (in-end - in-start) * (out-end - out-start) + out-start
;; raymath's Wrap. Brings a value into [min, max) by subtracting whole spans
;; of it — an angle past 2π, a scrolling offset past the tile width. floor
;; and not truncation, so a value below min wraps up instead of sticking.
fn wrap-f32(value: f32, lo: f32, hi: f32) -> f32
value - (hi - lo) * floor-f32((value - lo) / (hi - lo))
;; ── Vector2 ─────────────────────────────────────────────────────────
fn v2-add(a: Vector2, b: Vector2) -> Vector2
Vector2{.x a.x + b.x, .y a.y + b.y}
fn v2-sub(a: Vector2, b: Vector2) -> Vector2
Vector2{.x a.x - b.x, .y a.y - b.y}
;; Componentwise, which is raymath's Vector2Multiply and is not a dot product
;; or anything else that deserves the word "multiply" unqualified. It is what
;; a non-uniform scale is written as.
fn v2-mul(a: Vector2, b: Vector2) -> Vector2
Vector2{.x a.x * b.x, .y a.y * b.y}
fn v2-scale(v: Vector2, k: f32) -> Vector2 = Vector2{.x v.x * k, .y v.y * k}
fn v2-negate(v: Vector2) -> Vector2 = Vector2{.x 0.0 - v.x, .y 0.0 - v.y}
fn v2-dot(a: Vector2, b: Vector2) -> f32 = a.x * b.x + a.y * b.y
;; The squared forms are not micro-optimisation dressed up: comparing two
;; distances, or a distance against a radius, is the common case and neither
;; needs the square root. raymath has both for the same reason.
fn v2-length-sqr(v: Vector2) -> f32 = v.x * v.x + v.y * v.y
fn v2-length(v: Vector2) -> f32 = sqrt-f32(v.x * v.x + v.y * v.y)
fn v2-distance-sqr(a: Vector2, b: Vector2) -> f32
let dx = a.x - b.x
dy = a.y - b.y
dx * dx + dy * dy
fn v2-distance(a: Vector2, b: Vector2) -> f32
let dx = a.x - b.x
dy = a.y - b.y
sqrt-f32(dx * dx + dy * dy)
;; Zero in, zero out — raymath's Vector2Normalize guards on `length > 0` and
;; returns {0, 0}, and this does the same. The alternative is dividing by
;; zero and answering a vector of NaNs, which then propagates through every
;; subsequent frame's arithmetic and reports itself somewhere else entirely.
;; The guard is the whole reason this is a function and not two divisions
;; written at the call site.
fn v2-normalize(v: Vector2) -> Vector2
let length = sqrt-f32(v.x * v.x + v.y * v.y)
if length > 0.0
let inv = 1.0 / length
Vector2{.x v.x * inv, .y v.y * inv}
else
Vector2{.x 0.0 .y 0.0}
;; The signed angle from a to b, in radians, via atan2 of the 2D cross
;; product over the dot. Signed and not absolute, so it says which way to
;; turn; raymath's Vector2Angle is this and not the acos form.
fn v2-angle(a: Vector2, b: Vector2) -> f32
atan2-f32(a.x * b.y - a.y * b.x, a.x * b.x + a.y * b.y)
;; a + t*(b - a) componentwise, which is raymath's Vector2Lerp exactly. The
;; note in the file header applies: the prelude's scalar lerp is the
;; weighted-sum form and this is not, so the two do not agree in the last bit
;; at t = 1.0. raymath's is kept here because a vector path that disagrees
;; with raylib's own would be the surprise.
fn v2-lerp(a: Vector2, b: Vector2, t: f32) -> Vector2
Vector2{.x a.x + t * (b.x - a.x), .y a.y + t * (b.y - a.y)}
;; Counter-clockwise by `angle` radians in raylib's screen space, which has y
;; growing downward — so on screen it turns the other way from the way the
;; maths reads. raymath's Vector2Rotate, unchanged.
fn v2-rotate(v: Vector2, angle: f32) -> Vector2
let c = cos-f32(angle)
s = sin-f32(angle)
Vector2{.x v.x * c - v.y * s, .y v.x * s + v.y * c}
;; ── Vector3 ─────────────────────────────────────────────────────────
fn v3-add(a: Vector3, b: Vector3) -> Vector3
Vector3{.x a.x + b.x, .y a.y + b.y, .z a.z + b.z}
fn v3-sub(a: Vector3, b: Vector3) -> Vector3
Vector3{.x a.x - b.x, .y a.y - b.y, .z a.z - b.z}
fn v3-mul(a: Vector3, b: Vector3) -> Vector3
Vector3{.x a.x * b.x, .y a.y * b.y, .z a.z * b.z}
fn v3-scale(v: Vector3, k: f32) -> Vector3
Vector3{.x v.x * k, .y v.y * k, .z v.z * k}
fn v3-negate(v: Vector3) -> Vector3
Vector3{.x 0.0 - v.x, .y 0.0 - v.y, .z 0.0 - v.z}
fn v3-dot(a: Vector3, b: Vector3) -> f32 = (a.x * b.x + a.y * b.y) + a.z * b.z
;; Right-handed, which is the convention raylib's camera uses: the cross of
;; the x axis with the y axis is the z axis.
fn v3-cross(a: Vector3, b: Vector3) -> Vector3
Vector3({.x a.y * b.z - a.z * b.y, .y a.z * b.x - a.x * b.z, .z a.x * b.y - a.y * b.x})
fn v3-length-sqr(v: Vector3) -> f32 = (v.x * v.x + v.y * v.y) + v.z * v.z
fn v3-length(v: Vector3) -> f32 = sqrt-f32((v.x * v.x + v.y * v.y) + v.z * v.z)
fn v3-distance-sqr(a: Vector3, b: Vector3) -> f32
let dx = a.x - b.x
dy = a.y - b.y
dz = a.z - b.z
(dx * dx + dy * dy) + dz * dz
fn v3-distance(a: Vector3, b: Vector3) -> f32
let dx = a.x - b.x
dy = a.y - b.y
dz = a.z - b.z
sqrt-f32((dx * dx + dy * dy) + dz * dz)
;; Zero in, zero out, exactly as v2-normalize and for the same reason.
fn v3-normalize(v: Vector3) -> Vector3
let length = sqrt-f32((v.x * v.x + v.y * v.y) + v.z * v.z)
if length > 0.0
let inv = 1.0 / length
Vector3{.x v.x * inv, .y v.y * inv, .z v.z * inv}
else
Vector3{.x 0.0 .y 0.0 .z 0.0}
fn v3-lerp(a: Vector3, b: Vector3, t: f32) -> Vector3
Vector3({.x a.x + t * (b.x - a.x), .y a.y + t * (b.y - a.y), .z a.z + t * (b.z - a.z)})