libccd is compiled into libakgl.so and is BSD-3-Clause, so the notice has to travel with the binary form. cmake --install now puts BSD-LICENSE at share/doc/akgl/BSD-LICENSE.libccd, and the README says which dependency is in that position and which are not -- everything else in deps/ is either a separate shared object carrying its own notice, a header, or compiled into nothing. The plan's last step was to delete akgl_rectangle_points, akgl_collide_point_rectangle and akgl_collide_rectangles. That step is not taken, and TODO.md carries the reasoning rather than leaving it to be rediscovered: akgl_collide_rectangles has two correct callers in the sidescroller asking a game-level overlap question that wants a bool, and it was fixed two commits into this same series -- deleting it now would be a strange thing to do to a caller. The other two are the weak case and are named as the candidates if a 0.9.0 wants to trim. akgl_RectanglePoints' comment still claimed akgl_collide_rectangles works corner by corner, which stopped being true when the cross case was fixed. Corrected in place, with the cross case named so the note explains why the shape changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
198 lines
9.9 KiB
C
198 lines
9.9 KiB
C
/**
|
|
* @file util.h
|
|
* @brief Axis-aligned collision tests, path resolution, and two test-only image helpers.
|
|
*
|
|
* The grab bag. Three unrelated groups live here: rectangle/point overlap for
|
|
* the physics backend, path resolution for the asset loaders, and a pair of
|
|
* pixel-comparison routines that exist only so tests can assert on what was
|
|
* actually drawn.
|
|
*
|
|
* All the geometry here is axis-aligned and treats edges as touching: a point
|
|
* exactly on a boundary is inside. There is no rotation support and no
|
|
* separating-axis test.
|
|
*/
|
|
|
|
#ifndef _AKGL_UTIL_H_
|
|
#define _AKGL_UTIL_H_
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <akerror.h>
|
|
#include <stdbool.h>
|
|
#include <akgl/staticstring.h>
|
|
|
|
/** @brief An integer point. Carries a `z` the collision routines do not use. */
|
|
typedef struct akgl_Point {
|
|
int x; /**< Horizontal position, in whatever space the caller is working in. */
|
|
int y; /**< Vertical position. */
|
|
int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */
|
|
} akgl_Point;
|
|
|
|
/**
|
|
* @brief The four corners of an axis-aligned rectangle, precomputed.
|
|
*
|
|
* The form akgl_collide_point_rectangle wants. akgl_rectangle_points derives one
|
|
* of these from an `SDL_FRect`.
|
|
*
|
|
* @note akgl_collide_rectangles used to be built on this, corner by corner, and
|
|
* missed the cross case that way -- two rectangles overlapping in a plus
|
|
* sign have no corner of either inside the other. It compares edge spans
|
|
* in `float32_t` since 0.8.0 and does not go through here at all.
|
|
*/
|
|
typedef struct akgl_RectanglePoints {
|
|
akgl_Point topleft; /**< (x, y). */
|
|
akgl_Point topright; /**< (x + w, y). */
|
|
akgl_Point bottomleft; /**< (x, y + h). */
|
|
akgl_Point bottomright; /**< (x + w, y + h). */
|
|
} akgl_RectanglePoints;
|
|
|
|
/**
|
|
* @brief Expand a rectangle into its four corner points.
|
|
*
|
|
* Coordinates are truncated from `float` to `int` on the way in, so a rectangle
|
|
* at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and
|
|
* wrong for sub-pixel work; callers needing the latter should not round-trip
|
|
* through this.
|
|
*
|
|
* @param dest Receives the corners. Required.
|
|
* @param rect The rectangle, in any coordinate space. Required. `w` and `h` are
|
|
* taken as extents from `x`/`y`, so a negative one produces a
|
|
* rectangle whose "bottom right" is above and left of its "top
|
|
* left" -- which every test here then reports as empty.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect);
|
|
/**
|
|
* @brief Test whether a point falls inside a rectangle, edges included.
|
|
*
|
|
* Compares against `topleft` and `bottomright` only, so it assumes @p r is
|
|
* well-formed -- the two corners actually being the minimum and maximum. `z` is
|
|
* ignored on both sides: this is a 2D test.
|
|
*
|
|
* @param p The point to test. Required.
|
|
* @param r The rectangle, as corners from akgl_rectangle_points. Required.
|
|
* @param collide Receives `true` when the point is inside or exactly on an edge,
|
|
* `false` otherwise. Required -- the return value is the error
|
|
* context. Not written on any failure path.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *r, bool *collide);
|
|
/**
|
|
* @brief Test whether two rectangles overlap, edges included.
|
|
*
|
|
* Tests all eight corners -- each rectangle's four against the other -- and
|
|
* stops at the first hit. Checking both directions is what catches the case
|
|
* where one rectangle is entirely inside the other and so has no corner within
|
|
* its neighbour.
|
|
*
|
|
* @param r1 First rectangle. Required.
|
|
* @param r2 Second rectangle. Required. Order does not matter.
|
|
* @param collide Receives `true` on any overlap or shared edge, `false`
|
|
* otherwise. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p r1, @p r2, or @p collide is `NULL`.
|
|
*
|
|
* @note Fixed in 0.8.0. This was eight corner-containment tests, which answer a
|
|
* different question and get the cross wrong: a tall thin rectangle
|
|
* crossing a short wide one overlaps without either enclosing a corner of
|
|
* the other, and every corner test said no. It is a span comparison on
|
|
* both axes now. Two consequences for a caller upgrading: that
|
|
* arrangement starts reporting `true`, and the comparison is in `float`
|
|
* rather than through akgl_Point's `int` members, so an overlap smaller
|
|
* than one pixel is no longer truncated away.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
|
|
|
/**
|
|
* @brief Resolve an asset path, trying the working directory before the given root.
|
|
*
|
|
* Asset files name their neighbours relatively -- a sprite definition names its
|
|
* spritesheet, a tilemap names its tilesets -- and "relative" has to mean
|
|
* relative to the file doing the naming, not to wherever the game was launched
|
|
* from. So this tries @p path against the process working directory first, and
|
|
* only if that does not exist joins it onto @p root and resolves that. Either
|
|
* way the result is absolute, with symlinks and `..` folded out.
|
|
*
|
|
* @param root Directory to fall back to, normally `dirname` of the file that
|
|
* contained @p path. Required, even when unused.
|
|
* @param path The path to resolve, relative or absolute. Required.
|
|
* @param dest Receives the resolved absolute path. Required, and must already be
|
|
* a claimed pool string -- this writes into it, it does not claim
|
|
* one for you.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`.
|
|
* @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in
|
|
* #AKGL_MAX_STRING_LENGTH.
|
|
* @throws ENOENT If neither spelling names an existing file. Any other `errno`
|
|
* `realpath(3)` can raise -- EACCES on an unsearchable directory,
|
|
* ELOOP, ENOTDIR -- propagates the same way.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
*
|
|
* @note The fallback path -- the common one, since most asset references are
|
|
* relative to their own file rather than to the working directory --
|
|
* returns straight out of the ENOENT handler and so never releases the
|
|
* error context it was handling. Each such call consumes one slot of
|
|
* libakerror's fixed 128-entry context array for the life of the process.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dest);
|
|
|
|
// These are REALLY slow routines that are only useful in testing harnesses
|
|
/**
|
|
* @brief Assert that two surfaces hold byte-identical pixels.
|
|
*
|
|
* A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in
|
|
* one pixel is a failure. Meant for test harnesses asserting on rendered output,
|
|
* not for anything on a frame path.
|
|
*
|
|
* @param s1 First surface. Required. Its `pitch * h` is what determines how many
|
|
* bytes are compared.
|
|
* @param s2 Second surface. Required.
|
|
* @return `NULL` when the pixels match, otherwise an error context owned by the
|
|
* caller. "Not equal" is reported as an error, not as an out-param.
|
|
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`, or either has no
|
|
* pixel buffer.
|
|
* @throws AKERR_VALUE If the surfaces differ in size, pitch, or pixel format,
|
|
* or if their pixels differ.
|
|
*
|
|
* Dimensions, pitch and pixel format are compared first, and a difference in
|
|
* any of them is reported as a mismatch. Until 0.5.0 they were not, so a
|
|
* smaller @p s2 was read past its end instead -- benign in practice and
|
|
* immediately fatal under a memory checker.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
|
/**
|
|
* @brief Draw two textures in turn, read the framebuffer back after each, and compare.
|
|
*
|
|
* The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do
|
|
* these two textures *render* the same", which is not the same question as "are
|
|
* these two textures identical", because the renderer's scaling and blending sit
|
|
* in between. Both are drawn into the same rectangle against a cleared target.
|
|
*
|
|
* @param t1 First texture. Required.
|
|
* @param t2 Second texture. Required.
|
|
* @param x Left edge of the region, in pixels. Used for the source
|
|
* rectangle, the destination, and the readback alike.
|
|
* @param y Top edge of the region.
|
|
* @param w Width of the region.
|
|
* @param h Height of the region.
|
|
* @param writeout Optional filename for a PNG of the *first* render, written
|
|
* under `SDL_GetBasePath()`. `NULL` skips it. This is a
|
|
* debugging aid -- when an image assertion fails, this is how
|
|
* you see what was actually drawn.
|
|
* @return `NULL` when the two renders match, otherwise an error context owned by
|
|
* the caller.
|
|
* @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`.
|
|
* @throws AKGL_ERR_SDL If the framebuffer cannot be read back.
|
|
* @throws AKERR_IO If @p writeout is given and the PNG cannot be written.
|
|
* @throws AKERR_VALUE If the two renders differ.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
*
|
|
* @note Until 0.5.0 both passes drew @p t1, so this always reported a match and
|
|
* every image assertion built on it asserted nothing. It draws @p t2 on
|
|
* the second pass now.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
|
|
|
|
#endif // _AKGL_UTIL_H_
|