292 lines
16 KiB
Plaintext
292 lines
16 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 (.id t)) (println "")
|
|
(print (.width t)) (println "")
|
|
(print (.height t)) (println "")
|
|
(print (.mipmaps t)) (println "")
|
|
(print (.format t)) (println ""))
|
|
|
|
(defn show-rect [r rl/Rectangle]
|
|
(print (.x r)) (println "")
|
|
(print (.y r)) (println "")
|
|
(print (.width r)) (println "")
|
|
(print (.height r)) (println ""))
|
|
|
|
;; ── 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-bool [name string b bool]
|
|
(print name) (print " ")
|
|
(println (if b "yes" "no")))
|
|
|
|
(defn show-v [v rl/Vector2]
|
|
(print (.x v)) (println "")
|
|
(print (.y v)) (println ""))
|
|
|
|
;; 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 name)
|
|
(println (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 (.r c)) (println "")
|
|
(print (.g c)) (println "")
|
|
(print (.b c)) (println "")
|
|
(print (.a c)) (println ""))
|
|
|
|
;; 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))
|
|
|
|
;; ── Collision, which is the best material a headless test gets ──────
|
|
;;
|
|
;; Every one of these is pure: raylib computes an answer out of the fields,
|
|
;; so a wrong field order gives a wrong answer rather than the same struct
|
|
;; back. Each case below is paired with one that must come out the other
|
|
;; way, because a predicate that always said yes would pass a single case.
|
|
(let [r (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})]
|
|
;; Inside on both axes, then outside on y only. Swap width and height and
|
|
;; both of these flip, which is what makes the pair worth more than either.
|
|
(show-bool "point in rect" (rl/collision-point-rec? (rl/Vector2 {:x 5.0 :y 3.0}) r))
|
|
(show-bool "point below rect" (rl/collision-point-rec? (rl/Vector2 {:x 5.0 :y 5.0}) r))
|
|
;; Overlapping by one unit, then clear of it. Pins x against width.
|
|
(show-bool "rects overlap"
|
|
(rl/collision-recs? r (rl/Rectangle {:x 9.0 :y 1.0 :width 10.0 :height 10.0})))
|
|
(show-bool "rects apart"
|
|
(rl/collision-recs? r (rl/Rectangle {:x 11.0 :y 1.0 :width 10.0 :height 10.0}))))
|
|
|
|
;; Centres five apart with radii summing to six, then seven apart. The radius
|
|
;; is a scalar beside two Vector2s, so this pins it against their fields.
|
|
(show-bool "circles touch"
|
|
(rl/collision-circles? (rl/Vector2 {:x 0.0 :y 0.0}) 3.0
|
|
(rl/Vector2 {:x 5.0 :y 0.0}) 3.0))
|
|
(show-bool "circles clear"
|
|
(rl/collision-circles? (rl/Vector2 {:x 0.0 :y 0.0}) 3.0
|
|
(rl/Vector2 {:x 7.0 :y 0.0}) 3.0))
|
|
|
|
;; The one that answers with a number rather than a yes: a horizontal segment
|
|
;; at y = 7 crossed by a vertical one at x = 3, so the answer is (3 7).
|
|
;;
|
|
;; Asymmetric rather than the two diagonals of a square meeting at (5 5),
|
|
;; which would give the same answer however the fields were ordered — but be
|
|
;; clear about what that buys, because it is less than it looks.
|
|
;;
|
|
;; **Nothing in this section pins Vector2's own two fields, and nothing
|
|
;; axis-aligned can.** Exchanging x and y is a reflection: it is applied to
|
|
;; the inputs on the way in and undone on the way out, so the printed answer
|
|
;; is identical. Verified by actually swapping the shim's typedef — every
|
|
;; collision case here still passes. Distances are worse still, being
|
|
;; unchanged by the reflection in the first place.
|
|
;;
|
|
;; The rotated camera above is what pins them, and it works precisely because
|
|
;; a 90-degree rotation is not axis-aligned, so the reflection does not
|
|
;; commute with it. That case is load-bearing; do not delete it because the
|
|
;; collision ones look like they cover the same ground.
|
|
;;
|
|
;; What this section *does* pin is Rectangle, completely — swapping width and
|
|
;; height turns three of the four predicates below the wrong way.
|
|
(match (rl/collision-lines (rl/Vector2 {:x 0.0 :y 7.0}) (rl/Vector2 {:x 10.0 :y 7.0})
|
|
(rl/Vector2 {:x 3.0 :y 0.0}) (rl/Vector2 {:x 3.0 :y 10.0}))
|
|
(Some p) (show-v p)
|
|
None (println "no crossing"))
|
|
;; The rest of the collision family, each with the case that must come out
|
|
;; the other way. Bound and linking is not the same as working: a wrapper
|
|
;; whose arguments are in the wrong order links perfectly and answers
|
|
;; nonsense, and until something calls it nothing says so.
|
|
(let [r (rl/Rectangle {:x 0.0 :y 0.0 :width 10.0 :height 4.0})]
|
|
;; Circle against rect: just touching at the right edge, then clear of it.
|
|
(show-bool "circle meets rect"
|
|
(rl/collision-circle-rec? (rl/Vector2 {:x 12.0 :y 2.0}) 3.0 r))
|
|
(show-bool "circle clears rect"
|
|
(rl/collision-circle-rec? (rl/Vector2 {:x 14.0 :y 2.0}) 3.0 r)))
|
|
|
|
;; Circle against a segment, which is the one that pins the radius against
|
|
;; the two endpoints rather than against a single centre.
|
|
(show-bool "circle meets line"
|
|
(rl/collision-circle-line? (rl/Vector2 {:x 5.0 :y 2.0}) 3.0
|
|
(rl/Vector2 {:x 0.0 :y 0.0})
|
|
(rl/Vector2 {:x 10.0 :y 0.0})))
|
|
(show-bool "circle clears line"
|
|
(rl/collision-circle-line? (rl/Vector2 {:x 5.0 :y 4.0}) 3.0
|
|
(rl/Vector2 {:x 0.0 :y 0.0})
|
|
(rl/Vector2 {:x 10.0 :y 0.0})))
|
|
|
|
(show-bool "point in circle"
|
|
(rl/collision-point-circle? (rl/Vector2 {:x 2.0 :y 0.0})
|
|
(rl/Vector2 {:x 0.0 :y 0.0}) 3.0))
|
|
(show-bool "point outside circle"
|
|
(rl/collision-point-circle? (rl/Vector2 {:x 4.0 :y 0.0})
|
|
(rl/Vector2 {:x 0.0 :y 0.0}) 3.0))
|
|
|
|
;; A right triangle with the square corner at the origin. The inside point is
|
|
;; inside for one vertex order and not the other, so this is one of the few
|
|
;; here that notices which vertex is which.
|
|
(show-bool "point in triangle"
|
|
(rl/collision-point-triangle? (rl/Vector2 {:x 1.0 :y 1.0})
|
|
(rl/Vector2 {:x 0.0 :y 0.0})
|
|
(rl/Vector2 {:x 8.0 :y 0.0})
|
|
(rl/Vector2 {:x 0.0 :y 6.0})))
|
|
(show-bool "point outside triangle"
|
|
(rl/collision-point-triangle? (rl/Vector2 {:x 7.0 :y 5.0})
|
|
(rl/Vector2 {:x 0.0 :y 0.0})
|
|
(rl/Vector2 {:x 8.0 :y 0.0})
|
|
(rl/Vector2 {:x 0.0 :y 6.0})))
|
|
|
|
;; On the segment, then beside it. The threshold is the last argument, so a
|
|
;; wrapper that lost it among the four coordinates answers with whatever was
|
|
;; in that register.
|
|
(show-bool "point on line"
|
|
(rl/collision-point-line? (rl/Vector2 {:x 5.0 :y 0.0})
|
|
(rl/Vector2 {:x 0.0 :y 0.0})
|
|
(rl/Vector2 {:x 10.0 :y 0.0}) 1))
|
|
(show-bool "point off line"
|
|
(rl/collision-point-line? (rl/Vector2 {:x 5.0 :y 4.0})
|
|
(rl/Vector2 {:x 0.0 :y 0.0})
|
|
(rl/Vector2 {:x 10.0 :y 0.0}) 1))
|
|
|
|
;; The only one that crosses a *slice*, so it is the only one where ptr+len
|
|
;; has to arrive as raylib's pointer-and-count. A wrong length reads past the
|
|
;; array or stops short, and either way the square stops being a square.
|
|
(let [square [(rl/Vector2 {:x 0.0 :y 0.0}) (rl/Vector2 {:x 8.0 :y 0.0})
|
|
(rl/Vector2 {:x 8.0 :y 8.0}) (rl/Vector2 {:x 0.0 :y 8.0})]]
|
|
(show-bool "point in poly"
|
|
(rl/collision-point-poly? (rl/Vector2 {:x 4.0 :y 4.0}) (slice square 0 4)))
|
|
(show-bool "point outside poly"
|
|
(rl/collision-point-poly? (rl/Vector2 {:x 12.0 :y 4.0}) (slice square 0 4)))
|
|
;; The same point against the same array, three corners instead of four:
|
|
;; inside the square, outside the triangle the first three make. This is
|
|
;; the case that proves the *length* crosses — everything above would pass
|
|
;; with a hardcoded count, or with the pointer alone.
|
|
(show-bool "in square, four corners"
|
|
(rl/collision-point-poly? (rl/Vector2 {:x 2.0 :y 6.0}) (slice square 0 4)))
|
|
(show-bool "out of triangle, three"
|
|
(rl/collision-point-poly? (rl/Vector2 {:x 2.0 :y 6.0}) (slice square 0 3))))
|
|
|
|
;; Parallel, so they never meet: None rather than a point nobody wrote.
|
|
(match (rl/collision-lines (rl/Vector2 {:x 0.0 :y 0.0}) (rl/Vector2 {:x 1.0 :y 2.0})
|
|
(rl/Vector2 {:x 5.0 :y 0.0}) (rl/Vector2 {:x 6.0 :y 2.0}))
|
|
(Some p) (show-v p)
|
|
None (println "no crossing"))
|
|
|
|
;; An enum reaching a declare-c parameter through (Enum n): the axis is a
|
|
;; loop variable, which is what nothing could express before. No pad is
|
|
;; attached, so raylib answers its own resting value per axis — 0.0 for the
|
|
;; four stick axes and -1.0 for the two triggers, which rest at the negative
|
|
;; end. That split is what makes this pin worth having: it shows six
|
|
;; distinct i32s arriving as six distinct GamepadAxis members rather than
|
|
;; one constant answered six times.
|
|
(print "axes")
|
|
(dotimes [i 6]
|
|
(print " ")
|
|
(print (rl/get-gamepad-axis-movement 0 (rl/GamepadAxis i))))
|
|
;; And an axis that is no declared member. raylib bounds-checks it itself and
|
|
;; answers 0.0; the conversion does not refuse it, for the same reason the
|
|
;; printer shows an out-of-range enum as its number.
|
|
(print " past-end ")
|
|
(print (rl/get-gamepad-axis-movement 0 (rl/GamepadAxis 9)))
|
|
(println "")
|
|
|
|
0)
|