A camera whose layout a round trip could not have pinned
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.
This commit is contained in:
parent
d9b035be3c
commit
856f7dad7b
@ -24,6 +24,53 @@
|
|||||||
(print-f64 (f64 (.width r))) (newline)
|
(print-f64 (f64 (.width r))) (newline)
|
||||||
(print-f64 (f64 (.height 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
|
(defn main [] i32
|
||||||
(rl/set-trace-log-level :warning)
|
(rl/set-trace-log-level :warning)
|
||||||
|
|
||||||
@ -71,4 +118,22 @@
|
|||||||
;; visibly wrong sprite in the interactive run, and nowhere else.
|
;; 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)
|
(rl/set-shapes-texture (rl/Texture2D {:id 7 :width 0 :height 17 :mipmaps 2 :format 4}) rect)
|
||||||
(show-texture (rl/get-shapes-texture)))
|
(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)
|
0)
|
||||||
|
|||||||
@ -158,13 +158,27 @@ let () =
|
|||||||
would have passed with any of those permuted — storing and returning is
|
would have passed with any of those permuted — storing and returning is
|
||||||
symmetric. What the last case cannot pin, because nothing raylib
|
symmetric. What the last case cannot pin, because nothing raylib
|
||||||
computes without a GL context reads them, is width, height and mipmaps
|
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 =
|
let raylib_out =
|
||||||
"17\n34\n51\n68\n\
|
"17\n34\n51\n68\n\
|
||||||
6\n1\n4\n3\n\
|
6\n1\n4\n3\n\
|
||||||
7\n13\n17\n2\n4\n3.5\n7.25\n11.5\n13.75\n\
|
7\n13\n17\n2\n4\n3.5\n7.25\n11.5\n13.75\n\
|
||||||
1\n1\n1\n1\n7\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
|
in
|
||||||
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
|
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
|
||||||
outputs "raylib ffi, headless" "programs/raylib-ffi.flan" raylib_out;
|
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))
|
(get-shapes-texture-rectangle-raw (addr r))
|
||||||
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 ──────────────────────────────────────────────────────────
|
;; ── Shapes ──────────────────────────────────────────────────────────
|
||||||
;;
|
;;
|
||||||
;; Rectangle intersection, which raylib computes from all four fields in
|
;; 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 char r, g, b, a; } Color;
|
||||||
typedef struct { unsigned int id; int width, height, mipmaps, format; } Texture2D;
|
typedef struct { unsigned int id; int width, height, mipmaps, format; } Texture2D;
|
||||||
typedef struct { float x, y, width, height; } Rectangle;
|
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 InitWindow(int width, int height, const char *title);
|
||||||
extern void CloseWindow(void);
|
extern void CloseWindow(void);
|
||||||
@ -54,6 +55,10 @@ extern void DrawTextureEx(Texture2D texture, Vector2 position, float rotation,
|
|||||||
float scale, Color tint);
|
float scale, Color tint);
|
||||||
extern void DrawTextureRec(Texture2D texture, Rectangle source,
|
extern void DrawTextureRec(Texture2D texture, Rectangle source,
|
||||||
Vector2 position, Color tint);
|
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
|
/* 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
|
* 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) {
|
const Vector2 *position, const Color *tint) {
|
||||||
DrawTextureRec(*texture, *source, *position, *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