diff --git a/test/programs/raylib-ffi.flan b/test/programs/raylib-ffi.flan index f336fe7..b1b80ee 100644 --- a/test/programs/raylib-ffi.flan +++ b/test/programs/raylib-ffi.flan @@ -24,6 +24,53 @@ (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) @@ -71,4 +118,22 @@ ;; 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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index a34bde7..c2d8e55 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -192,13 +192,27 @@ let () = would have passed with any of those permuted — storing and returning is symmetric. What the last case cannot pin, because nothing raylib computes without a GL context reads them, is width, height and mipmaps - against each other. *) + against each other. + + The camera conversions are the strongest headless material here: both + are pure arithmetic over every field of a Camera2D and two Vector2s, + and both directions are asserted as absolute answers. A round trip + would not be — the inverse cancels a permuted layout exactly, the same + way store-and-return does. The rotated pair is the only thing in the + package that pins Vector2's own two fields, because every + component-wise formula is merely mirrored by exchanging x and y and so + compares equal; a rotation mixes them. It reports ok/bad rather than a + number because sinf and cosf make the answer 27.9999981, and this + table compares stdout byte for byte. *) let raylib_out = "17\n34\n51\n68\n\ 6\n1\n4\n3\n\ 7\n13\n17\n2\n4\n3.5\n7.25\n11.5\n13.75\n\ 1\n1\n1\n1\n7\n\ - 7\n0\n17\n2\n4\n" + 7\n0\n17\n2\n4\n\ + 28\n24\n140\n90\n\ + rotated screen-to-world ok\n\ + rotated world-to-screen ok\n" in if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin outputs "raylib ffi, headless" "programs/raylib-ffi.flan" raylib_out; diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index e50822c..8f22f36 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -146,6 +146,56 @@ (get-shapes-texture-rectangle-raw (addr r)) r)) +;; ── Camera2D ──────────────────────────────────────────────────────── +;; +;; The 2D camera: everything drawn between begin-mode-2d and end-mode-2d is +;; transformed by it. `offset` is where the camera's target lands on screen — +;; half the window size is what centres it — `target` is the world point that +;; goes there, and rotation is in degrees. +;; +;; `zoom` of 0 makes the transform singular and both conversions below hand +;; back NaN rather than failing. raylib does not guard it and neither does +;; this; 1.0 is the identity and a fresh (Camera2D {}) is therefore NOT usable +;; as one — it has to be given a zoom. + +(defstruct Camera2D [offset Vector2 target Vector2 rotation f32 zoom f32]) + +(declare begin-mode-2d-raw [camera (Ptr Camera2D)] "flan_rl_begin_mode_2d") + +(defn begin-mode-2d [camera Camera2D] + (let [c camera] + (begin-mode-2d-raw (addr c)))) + +(declare end-mode-2d [] "flan_rl_end_mode_2d") + +;; The two conversions are pure arithmetic over every field of the camera, so +;; unlike the rest of the camera they run with no window and no GL context. +;; That is what the acceptance table uses to pin Camera2D's layout, and — via +;; a rotated camera, which is the only call here that mixes x into y — it is +;; also the only thing that pins Vector2's two fields against each other. + +(declare get-screen-to-world-2d-raw + [position (Ptr Vector2) camera (Ptr Camera2D) out (Ptr Vector2)] + "flan_rl_get_screen_to_world_2d") + +(defn get-screen-to-world-2d [position Vector2 camera Camera2D] Vector2 + (let [p position + c camera + out (Vector2 {})] + (get-screen-to-world-2d-raw (addr p) (addr c) (addr out)) + out)) + +(declare get-world-to-screen-2d-raw + [position (Ptr Vector2) camera (Ptr Camera2D) out (Ptr Vector2)] + "flan_rl_get_world_to_screen_2d") + +(defn get-world-to-screen-2d [position Vector2 camera Camera2D] Vector2 + (let [p position + c camera + out (Vector2 {})] + (get-world-to-screen-2d-raw (addr p) (addr c) (addr out)) + out)) + ;; ── Shapes ────────────────────────────────────────────────────────── ;; ;; Rectangle intersection, which raylib computes from all four fields in diff --git a/vendor/raylib/shim.c b/vendor/raylib/shim.c index a993548..80fc5ac 100644 --- a/vendor/raylib/shim.c +++ b/vendor/raylib/shim.c @@ -22,6 +22,7 @@ typedef struct { float x, y; } Vector2; typedef struct { unsigned char r, g, b, a; } Color; typedef struct { unsigned int id; int width, height, mipmaps, format; } Texture2D; typedef struct { float x, y, width, height; } Rectangle; +typedef struct { Vector2 offset, target; float rotation, zoom; } Camera2D; extern void InitWindow(int width, int height, const char *title); extern void CloseWindow(void); @@ -54,6 +55,10 @@ extern void DrawTextureEx(Texture2D texture, Vector2 position, float rotation, float scale, Color tint); extern void DrawTextureRec(Texture2D texture, Rectangle source, Vector2 position, Color tint); +extern void BeginMode2D(Camera2D camera); +extern void EndMode2D(void); +extern Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera); +extern Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera); /* A Flan string arrives as ptr+len and is not NUL-terminated, so a C API that * wants a C string needs a copy. Two callers want one: the window title and a @@ -146,3 +151,16 @@ void flan_rl_draw_texture_rec(const Texture2D *texture, const Rectangle *source, const Vector2 *position, const Color *tint) { DrawTextureRec(*texture, *source, *position, *tint); } + +void flan_rl_begin_mode_2d(const Camera2D *camera) { BeginMode2D(*camera); } +void flan_rl_end_mode_2d(void) { EndMode2D(); } + +void flan_rl_get_screen_to_world_2d(const Vector2 *position, + const Camera2D *camera, Vector2 *out) { + *out = GetScreenToWorld2D(*position, *camera); +} + +void flan_rl_get_world_to_screen_2d(const Vector2 *position, + const Camera2D *camera, Vector2 *out) { + *out = GetWorldToScreen2D(*position, *camera); +}