flan/examples/core-3d-picking.flan
Joseph Ferano 52e9c92b0f The enum prefix goes uniform across all eleven
The author's ruling: "I think the prefix reads better, keep it" — so two
prefixed enums out of eleven was the inconsistency, not the prefix.
TraceLogLevel takes log-, CameraProjection projection-, CameraMode camera-,
GamepadButton button-, GamepadAxis axis-, Gesture gesture-, MouseCursor
cursor-, TextureFilter filter-, PixelFormat pixel-.

Two of those are judgement. CameraProjection and CameraMode share raylib's
CAMERA_ and deliberately do not share a Flan prefix: they are two questions
asked of the same struct, and :projection-perspective beside :camera-orbital
says which is being answered. GamepadButton and GamepadAxis take the short
stems rather than a shared gamepad-, which keeps :button-left-face-up and
:axis-left-trigger readable.

It is a reading choice and not a collision fix, and bindings, raylib.flan and
docs/BUILT.md all say so: a keyword resolves against the expected type and
nothing else, so :point at a TextureFilter site was never ambiguous. What the
prefix buys is the call site read on its own.

The three constant exception lines are keyed on the member's full Flan
spelling and moved with it. flan generate-c vendor/raylib is green against
raylib-5.5.h, and the check was confirmed non-vacuous by breaking it:
filter-trilinearr reported TEXTURE_FILTER_TRILINEARR rather than passing.
test_flan pins one member of each of the eleven to the C name the rule
reaches, read out of the real bindings file.

sand.flan line 121 is (rl/set-trace-log-level :warning) and is the author's
to respell. TraceLogLevel carries a warning alias beside log-warning, mapped
by name in bindings, so the suite stays green until he does; FIX.org has the
three-edit removal recipe.
2026-09-21 07:59:48 +07:00

154 lines
7.1 KiB
Plaintext

;;;; raylib [core] example - 3d picking
;;;;
;;;; examples/core/core_3d_picking.c. The one `core` example in this batch,
;;;; and it is here for surface and not for category: it is the only example
;;;; anywhere upstream that uses Ray and RayCollision without also loading a
;;;; Model, so it is the only way those two structs get a caller at all.
;;;;
;;;; Picking is the round trip the corpus was missing. core-world-screen.flan
;;;; already runs get-world-to-screen — a point in the scene to a pixel. This
;;;; is the inverse and it is a different shape: a pixel does not name a
;;;; point, it names a *line* through the scene, which is what a Ray is, and
;;;; the answer to "what did I click on" is where that line first meets
;;;; something, which is what a RayCollision is. Two structs the package could
;;;; not describe, for one operation that cannot be written without them.
;;;;
;;;; What was added, all in vendor/raylib/raylib.flan:
;;;;
;;;; - `Ray` — two Vector3s, origin and direction.
;;;; - `RayCollision` — a C `bool`, a float and two Vector3s. This is the
;;;; one of the three new structs a layout check can actually fail:
;;;; `hit` is one byte and `distance` is four, so there are three padding
;;;; bytes between them that a permutation destroys. BoundingBox's two
;;;; Vector3s are interchangeable and nothing would catch swapping them.
;;;; - `BoundingBox` — shared with examples/models-box-collisions.flan.
;;;; - get-screen-to-world-ray and draw-ray, hand-written beside
;;;; get-world-to-screen and the cube draws for the reasons `bindings`
;;;; gives: the first reads the same camera as its inverse, and the second
;;;; is inside a frame.
;;;;
;;;; get-ray-collision-box came free out of the generated half the moment Ray
;;;; and BoundingBox existed, along with the sphere, triangle and quad forms.
;;;; get-ray-collision-mesh is still refused and will stay refused: Mesh owns
;;;; seven arrays and a GPU handle, and describing it is a different job.
;;;;
;;;; raylib 5.5 renamed GetMouseRay to GetScreenToWorldRay and left the old
;;;; name as a #define. A #define is not a symbol, so there is nothing to bind
;;;; it to and nothing lost by not trying.
(import rl "vendor:raylib")
(defconst screen-width 800)
(defconst screen-height 450)
(defonce camera rl/Camera3D)
(defonce cube-position rl/Vector3)
(defonce cube-size rl/Vector3)
;; The picking ray, kept between frames because draw-ray draws it every frame
;; whether or not it hit anything — that is how the example shows where the
;; click went.
(defonce ray rl/Ray)
(defonce collision rl/RayCollision)
;; The same centre/size to min/max conversion as in
;; examples/models-box-collisions.flan. Written out here rather than shared
;; because an example is a single file the reader can follow end to end.
;;
;; It used to be eight expressions of field arithmetic; it is the half-extent
;; subtracted and added, which is what the C means, now that the package
;; carries vector arithmetic — see vendor/raylib/vector.flan. That file
;; exists because raymath is `static inline` and has no symbol to bind, so
;; v3-sub and v3-add are Flan and not C.
(defn box-around [centre rl/Vector3 size rl/Vector3] rl/BoundingBox
(let [half (rl/v3-scale size 0.5)]
(rl/BoundingBox {.min (rl/v3-sub centre half)
.max (rl/v3-add centre half)})))
(defn main [] ()
(rl/init-window screen-width screen-height
"raylib [core] example - 3d picking")
(defer (rl/close-window))
(set camera (rl/Camera3D {.position (rl/Vector3 {.x 10.0 .y 10.0 .z 10.0})
.target (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0})
.up (rl/Vector3 {.x 0.0 .y 1.0 .z 0.0})
.fovy 45.0
.projection :projection-perspective}))
(set cube-position (rl/Vector3 {.x 0.0 .y 1.0 .z 0.0}))
(set cube-size (rl/Vector3 {.x 2.0 .y 2.0 .z 2.0}))
;; Both start zeroed, which is the C's `= { 0 }`. A zero Ray draws as a
;; degenerate line at the origin and a zero RayCollision has hit false, so
;; neither needs a "not yet" flag beside it.
(set ray (rl/Ray {.position (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0})
.direction (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0})}))
(set collision (rl/RayCollision {.hit false .distance 0.0
.point (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0})
.normal (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0})}))
(rl/set-target-fps 60)
(until (rl/window-should-close?)
;; Update. The first-person controls only run while the cursor is
;; captured, which is what the right button toggles — otherwise the mouse
;; could not be used to aim at anything.
(when (rl/cursor-hidden?) (rl/update-camera (addr camera) :camera-first-person))
(when (rl/mouse-button-pressed? :mouse-right)
(if (rl/cursor-hidden?) (rl/enable-cursor) (rl/disable-cursor)))
(when (rl/mouse-button-pressed? :mouse-left)
(if (not (.hit collision))
;; The pixel under the pointer, as a line through the scene, tested
;; against the cube's bounds.
(do
(set ray (rl/get-screen-to-world-ray (rl/get-mouse-position) camera))
(set collision
(rl/get-ray-collision-box ray (box-around cube-position cube-size))))
;; A second click deselects. Only `hit` is cleared; the ray stays put
;; and keeps being drawn, as in the C.
(set (.hit collision) false)))
;; Draw
(rl/with-drawing
(rl/clear-background rl/raywhite)
(rl/with-mode-3d camera
(if (.hit collision)
(do
(rl/draw-cube cube-position (.x cube-size) (.y cube-size) (.z cube-size)
rl/red)
(rl/draw-cube-wires cube-position (.x cube-size) (.y cube-size)
(.z cube-size) rl/maroon)
;; A slightly larger wireframe around the selection, which is the
;; whole visual feedback of the example.
(rl/draw-cube-wires cube-position (+ (.x cube-size) 0.2)
(+ (.y cube-size) 0.2) (+ (.z cube-size) 0.2)
rl/green))
(do
(rl/draw-cube cube-position (.x cube-size) (.y cube-size) (.z cube-size)
rl/gray)
(rl/draw-cube-wires cube-position (.x cube-size) (.y cube-size)
(.z cube-size) rl/darkgray)))
;; raylib draws the ray a thousand units long, so it reads as a line
;; crossing the scene rather than as a segment with an end.
(rl/draw-ray ray rl/maroon)
(rl/draw-grid 10 1.0))
(rl/draw-text "Try clicking on the box with your mouse!" 240 10 20
rl/darkgray)
(when (.hit collision)
(rl/draw-text "BOX SELECTED"
(/ (- screen-width (rl/measure-text "BOX SELECTED" 30)) 2)
(i32 (* (f32 screen-height) 0.1)) 30 rl/green))
(rl/draw-text "Right click mouse to toggle camera controls" 10 430 10
rl/gray)
(rl/draw-fps 10 10))))