Camera2D, and a test that pins its layout by arithmetic
GetScreenToWorld2D computes from every field of a Camera2D - offset, target, rotation and zoom - so a wrong field order produces a wrong coordinate rather than the same numbers back. That is the standard the texture lane arrived at the hard way: a struct round trip is symmetric and passes for any layout.
This commit is contained in:
commit
f5d4cd5188
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
50
vendor/raylib/raylib.flan
vendored
50
vendor/raylib/raylib.flan
vendored
@ -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
|
||||
|
||||
18
vendor/raylib/shim.c
vendored
18
vendor/raylib/shim.c
vendored
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user