GetScreenToWorld2D and GetWorldToScreen2D are pure arithmetic over every field of a Camera2D, so they run with no window at all — the best headless material the package has had. Both directions are asserted as absolute answers rather than as a round trip, because an inverse cancels a permuted layout exactly the way store-and-return does. The rotated case earns its awkwardness: exchanging x and y in Vector2 mirrors every component-wise formula and the answer comes back mirrored too, so nothing until now could tell the two floats apart. A rotation mixes them. It reports ok/bad against a tolerance because 90 degrees goes through sinf and the answer is 27.9999981, and the table compares stdout byte for byte at -O0 and -O2.
140 lines
7.0 KiB
Plaintext
140 lines
7.0 KiB
Plaintext
(import rl "vendor:raylib")
|
|
|
|
;; The raylib boundary, headless. GetColor, the shapes texture and rectangle
|
|
;; intersection all need no window, so the whole crossing — a struct out of C
|
|
;; through an out-pointer, a struct into C through a pointer, a keyword
|
|
;; resolved against an enum — is exercised without a display.
|
|
;;
|
|
;; What is being checked is that a struct's FIELDS mean the same thing on both
|
|
;; sides. Note what does not check that: handing raylib a struct and reading it
|
|
;; back, because storing and returning is symmetric and a permuted layout
|
|
;; survives it unchanged. Every case below is asymmetric — raylib does
|
|
;; something to the fields that depends on which is which.
|
|
|
|
(defn show-texture [t rl/Texture2D]
|
|
(print-i64 (i64 (.id t))) (newline)
|
|
(print-i64 (i64 (.width t))) (newline)
|
|
(print-i64 (i64 (.height t))) (newline)
|
|
(print-i64 (i64 (.mipmaps t))) (newline)
|
|
(print-i64 (i64 (.format t))) (newline))
|
|
|
|
(defn show-rect [r rl/Rectangle]
|
|
(print-f64 (f64 (.x r))) (newline)
|
|
(print-f64 (f64 (.y r))) (newline)
|
|
(print-f64 (f64 (.width r))) (newline)
|
|
(print-f64 (f64 (.height r))) (newline))
|
|
|
|
;; ── Camera2D ────────────────────────────────────────────────────────
|
|
;;
|
|
;; The two conversions are pure arithmetic and need no window, which makes
|
|
;; them the strongest headless material in the package: each one reads every
|
|
;; field of a Camera2D and every field of two Vector2s.
|
|
;;
|
|
;; They are asserted in both directions separately and never as a round trip.
|
|
;; world->screen->world is the store-and-return trap wearing a different hat:
|
|
;; the inverse cancels a permuted layout exactly, so it passes for any order.
|
|
;;
|
|
;; The camera below is chosen so that no field is silently unpinned — offset
|
|
;; and target differ, zoom is 2.0 and not the identity 1.0, and every
|
|
;; component is a distinct dyadic value that prints exactly.
|
|
(defconst cam (rl/Camera2D {:offset (rl/Vector2 {:x 100.0 :y 50.0})
|
|
:target (rl/Vector2 {:x 8.0 :y 4.0})
|
|
:rotation 0.0
|
|
:zoom 2.0}))
|
|
|
|
;; Absolute values, not a round trip: screen (140,90) is world (28,24) because
|
|
;; ((140-100)/2)+8 = 28 and ((90-50)/2)+4 = 24. Swap offset and target in the
|
|
;; defstruct and this reads (143,-16); swap rotation and zoom and the zoom
|
|
;; becomes 0, the transform is singular, and both come back NaN.
|
|
(defn show-v [v rl/Vector2]
|
|
(print-f64 (f64 (.x v))) (newline)
|
|
(print-f64 (f64 (.y v))) (newline))
|
|
|
|
;; What no geometric call can pin on its own is Vector2's own two fields:
|
|
;; exchange x and y everywhere and every component-wise formula is simply
|
|
;; mirrored, so the answer comes back mirrored too and compares equal. A
|
|
;; *rotated* camera is the exception — it mixes x into y — so the case below
|
|
;; is the one thing in this file that fixes which float is which.
|
|
;;
|
|
;; It cannot be compared as text: 90 degrees goes through sinf and cosf and
|
|
;; the answer is 27.9999981, not 28, and the table compares stdout byte for
|
|
;; byte at -O0 and -O2. So the comparison happens here, with a tolerance, and
|
|
;; what is printed is the verdict. A wrong-but-close value passing is not a
|
|
;; risk worth naming: a permuted layout is out by whole units. Swapping x and
|
|
;; y in Vector2 makes this print "rotated bad" — the same camera then reads
|
|
;; back as (-12,24).
|
|
(defn near? [a f32 b f32] bool
|
|
(let [d (- a b)]
|
|
(< (if (< d 0.0) (- 0.0 d) d) 0.0001)))
|
|
|
|
(defn show-near [name string v rl/Vector2 x f32 y f32]
|
|
(print-str name)
|
|
(print-line (if (and (near? (.x v) x) (near? (.y v) y)) " ok" " bad")))
|
|
|
|
(defn main [] i32
|
|
(rl/set-trace-log-level :warning)
|
|
|
|
;; A Color is four bytes in RGBA order, so 0x11223344 is 17 34 51 68 and not
|
|
;; the little-endian reading of the packed integer. An identity would pass a
|
|
;; weaker test than this one.
|
|
(let [c (rl/get-color 0x11223344)]
|
|
(print-i64 (i64 (.r c))) (newline)
|
|
(print-i64 (i64 (.g c))) (newline)
|
|
(print-i64 (i64 (.b c))) (newline)
|
|
(print-i64 (i64 (.a c))) (newline))
|
|
|
|
;; Rectangle, pinned completely. The intersection of (0,0,10,4) and
|
|
;; (6,1,10,10) is (6,1,4,3) — four different numbers, each derived from a
|
|
;; different pair of fields, so swapping any two fields changes the answer.
|
|
(show-rect (rl/get-collision-rec (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})
|
|
(rl/Rectangle {:x 6.0 :y 1.0 :width 10.0 :height 10.0})))
|
|
|
|
;; Texture2D, as far as a machine with no GPU can go. raylib keeps the
|
|
;; shapes texture without touching GL, and substitutes a default when
|
|
;; `texture.id`, `source.width` or `source.height` is zero — that guard is
|
|
;; the only asymmetry a headless test gets.
|
|
(let [rect (rl/Rectangle {:x 3.5 :y 7.25 :width 11.5 :height 13.75})]
|
|
;; (A) Valid, five distinct values: they come back, so the struct crosses
|
|
;; intact in both directions and raylib stored it rather than defaulting.
|
|
(rl/set-shapes-texture (rl/Texture2D {:id 7 :width 13 :height 17 :mipmaps 2 :format 4}) rect)
|
|
(show-texture (rl/get-shapes-texture))
|
|
(show-rect (rl/get-shapes-texture-rectangle))
|
|
|
|
;; (B) id zero, everything else positive: the default 1 1 1 1 7 comes
|
|
;; back. The 7 is the only distinct field in it, so this pins `format` as
|
|
;; the last field, and the substitution happening at all pins `id` as the
|
|
;; field the guard reads.
|
|
(rl/set-shapes-texture (rl/Texture2D {:id 0 :width 13 :height 17 :mipmaps 2 :format 4}) rect)
|
|
(show-texture (rl/get-shapes-texture))
|
|
|
|
;; (C) width zero, id positive: still stored, because the guard does not
|
|
;; look at the texture's width. Without this case, (B) would pass just as
|
|
;; well with `id` and `width` swapped — the zero would land in the guarded
|
|
;; slot either way.
|
|
;;
|
|
;; That is the limit of what is checkable here: nothing raylib computes
|
|
;; without a GL context reads width, height or mipmaps, so their order
|
|
;; among themselves is not pinned by this test. A swap there shows up as a
|
|
;; visibly wrong sprite in the interactive run, and nowhere else.
|
|
(rl/set-shapes-texture (rl/Texture2D {:id 7 :width 0 :height 17 :mipmaps 2 :format 4}) rect)
|
|
(show-texture (rl/get-shapes-texture)))
|
|
|
|
;; Camera2D, each direction on its own. See the note above show-v for why
|
|
;; this is not a round trip.
|
|
(show-v (rl/get-screen-to-world-2d (rl/Vector2 {:x 140.0 :y 90.0}) cam))
|
|
(show-v (rl/get-world-to-screen-2d (rl/Vector2 {:x 28.0 :y 24.0}) cam))
|
|
|
|
;; And the rotated camera, which is what pins Vector2's own two fields.
|
|
(let [spun (rl/Camera2D {:offset (rl/Vector2 {:x 100.0 :y 50.0})
|
|
:target (rl/Vector2 {:x 8.0 :y 4.0})
|
|
:rotation 90.0
|
|
:zoom 2.0})]
|
|
(show-near "rotated screen-to-world"
|
|
(rl/get-screen-to-world-2d (rl/Vector2 {:x 140.0 :y 90.0}) spun)
|
|
28.0 -16.0)
|
|
(show-near "rotated world-to-screen"
|
|
(rl/get-world-to-screen-2d (rl/Vector2 {:x 28.0 :y 24.0}) spun)
|
|
60.0 90.0))
|
|
|
|
0)
|