flan/examples/core-3d-picking.flan
Joseph Ferano 25f56c24a8 A begin/end pair that cannot come apart, and what it still cannot promise
vendor/raylib/modes.flan: five macros over the five pairs the package binds
-- with-drawing, with-mode-2d, with-mode-3d, with-texture-mode,
with-scissor-mode. A second file with no declare-c in it, split out on
vector.flan's reasoning: raylib.flan is the package's statement about C and
nothing here names C, so nothing here can be made wrong by raylib changing.

Each expands to (do (begin-... args) body... (end-...)) -- the calls the
author used to type, in the order they typed them. No let, no gensym: nothing
binds a name, so there is nothing for a caller's name to collide with.

What it removes is the End* that is missing, wrong, or no longer beside its
Begin*. What it cannot remove is a body leaving through the unwind path: a
return or an invoke-restart skips the rest of the do and the End* with it.
defer is the obvious fix and is refused inside a loop body, which is where a
pair always lives -- checked, not assumed. So sand.flan's discipline stays:
keep the restart boundary outside the pair.

35 call sites converted across examples/ and sand.flan. The one left is
core-scissor-test.flan, whose Begin and End sit in two separate `when`s with
the drawing between them -- a conditional pair is a shape a bracketing macro
cannot express.

test/programs/rl-with.flan covers with-scissor-mode, which no example can,
with a frame function unreachable from main so it needs no libraylib;
rl-with-reject.flan is the arity half.
2026-09-13 18:00:35 +07:00

154 lines
7.0 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)
(defvar camera rl/Camera3D)
(defvar cube-position rl/Vector3)
(defvar 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.
(defvar ray rl/Ray)
(defvar 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 :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) :first-person))
(when (rl/mouse-button-pressed? :right)
(if (rl/cursor-hidden?) (rl/enable-cursor) (rl/disable-cursor)))
(when (rl/mouse-button-pressed? :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))))