MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Bounds.hpp
Go to the documentation of this file.
1#pragma once
2
4
5namespace MayaFlux::Kinesis {
6
7// =============================================================================
8// AABB2D
9// =============================================================================
10
11/**
12 * @struct AABB2D
13 * @brief Axis-aligned bounding rectangle in a 2D coordinate space.
14 *
15 * Used as a fast-reject hint in Portal::Forma::Element. All coordinates
16 * are in NDC by convention when used in Forma: x and y in [-1, +1],
17 * center origin, +Y up, matching ViewTransform.hpp and Windowing.hpp.
18 *
19 * Named constructors convert from other coordinate spaces into NDC.
20 */
21struct AABB2D {
22 glm::vec2 min;
23 glm::vec2 max;
24
25 [[nodiscard]] bool contains(glm::vec2 p) const noexcept
26 {
27 return p.x >= min.x && p.x <= max.x
28 && p.y >= min.y && p.y <= max.y;
29 }
30
31 [[nodiscard]] bool overlaps(const AABB2D& other) const noexcept
32 {
33 return min.x <= other.max.x && max.x >= other.min.x
34 && min.y <= other.max.y && max.y >= other.min.y;
35 }
36
37 [[nodiscard]] float width() const noexcept { return max.x - min.x; }
38 [[nodiscard]] float height() const noexcept { return max.y - min.y; }
39
40 [[nodiscard]] glm::vec2 center() const noexcept { return (min + max) * 0.5F; }
41
42 [[nodiscard]] AABB2D translated(glm::vec2 offset) const noexcept
43 {
44 return { .min = min + offset, .max = max + offset };
45 }
46
47 [[nodiscard]] AABB2D expanded(float margin) const noexcept
48 {
49 return { .min = min - glm::vec2(margin), .max = max + glm::vec2(margin) };
50 }
51
52 [[nodiscard]] static AABB2D from_ndc(glm::vec2 center, glm::vec2 half) noexcept
53 {
54 return { .min = center - half, .max = center + half };
55 }
56
57 /**
58 * @brief Construct from pixel-space top-left rect, converted to NDC.
59 * @param x Left edge in pixels.
60 * @param y Top edge in pixels (top-left origin, +Y down).
61 * @param w Width in pixels.
62 * @param h Height in pixels.
63 * @param win_w Window width in pixels.
64 * @param win_h Window height in pixels.
65 */
66 [[nodiscard]] static AABB2D from_pixel(
67 float x, float y, float w, float h,
68 uint32_t win_w, uint32_t win_h) noexcept
69 {
70 auto nx = [&](float px) {
71 return (px / static_cast<float>(win_w)) * 2.0F - 1.0F;
72 };
73 auto ny = [&](float py) {
74 return 1.0F - (py / static_cast<float>(win_h)) * 2.0F;
75 };
76 return { .min = glm::vec2(nx(x), ny(y + h)), .max = glm::vec2(nx(x + w), ny(y)) };
77 }
78
79 /**
80 * @brief Construct from a TextureBuffer NDC position and scale.
81 * @param ndc_position Center as returned by TextureBuffer::get_position().
82 * @param ndc_scale Extent as returned by TextureBuffer::get_scale().
83 */
84 [[nodiscard]] static AABB2D from_buffer_transform(
85 glm::vec2 ndc_position, glm::vec2 ndc_scale) noexcept
86 {
87 glm::vec2 half = ndc_scale * 0.5F;
88 return { .min = ndc_position - half, .max = ndc_position + half };
89 }
90};
91
92// =============================================================================
93// BoundingSphere
94// =============================================================================
95
96/**
97 * @struct BoundingSphere
98 * @brief Spherical fast-reject hint in world space.
99 *
100 * Used as the cheap pre-filter for 3D spatial predicates, analogous to
101 * AABB2D in the 2D path. A sphere is a better pre-filter than an axis-aligned
102 * box in 3D: rotation-invariant, one dot product to test a point, one
103 * discriminant to test a ray.
104 */
106 glm::vec3 center;
107 float radius;
108
109 [[nodiscard]] bool contains(const glm::vec3& p) const noexcept
110 {
111 glm::vec3 d = p - center;
112 return glm::dot(d, d) <= radius * radius;
113 }
114
115 /**
116 * @brief Returns true if the ray passes through or originates inside the sphere.
117 * @param ray World-space ray with normalized direction.
118 */
119 [[nodiscard]] bool overlaps_ray(const Ray& ray) const noexcept
120 {
121 glm::vec3 oc = ray.origin - center;
122 float b = glm::dot(oc, ray.direction);
123 float c = glm::dot(oc, oc) - radius * radius;
124 return b * b - c >= 0.0F;
125 }
126};
127
128// =============================================================================
129// AABB3D
130// =============================================================================
131
132/**
133 * @struct AABB3D
134 * @brief Axis-aligned bounding box in 3D world space.
135 *
136 * Counterpart to AABB2D for three-dimensional geometry. Used by
137 * Kinesis::aabb() to describe the extent of any PositionCarrying span,
138 * and as a fast-reject pre-filter in 3D spatial predicates.
139 *
140 * No coordinate convention is enforced; the box is whatever the caller's
141 * position data implies (world space, object space, NDC-extended, etc.).
142 */
143struct AABB3D {
144 glm::vec3 min;
145 glm::vec3 max;
146
147 [[nodiscard]] bool contains(const glm::vec3& p) const noexcept
148 {
149 return p.x >= min.x && p.x <= max.x
150 && p.y >= min.y && p.y <= max.y
151 && p.z >= min.z && p.z <= max.z;
152 }
153
154 [[nodiscard]] bool overlaps(const AABB3D& other) const noexcept
155 {
156 return min.x <= other.max.x && max.x >= other.min.x
157 && min.y <= other.max.y && max.y >= other.min.y
158 && min.z <= other.max.z && max.z >= other.min.z;
159 }
160
161 [[nodiscard]] glm::vec3 center() const noexcept { return (min + max) * 0.5F; }
162 [[nodiscard]] glm::vec3 extent() const noexcept { return max - min; }
163 [[nodiscard]] glm::vec3 half_extent() const noexcept { return (max - min) * 0.5F; }
164
165 [[nodiscard]] AABB3D translated(const glm::vec3& offset) const noexcept
166 {
167 return { .min = min + offset, .max = max + offset };
168 }
169
170 [[nodiscard]] AABB3D expanded(float margin) const noexcept
171 {
172 return { .min = min - glm::vec3(margin), .max = max + glm::vec3(margin) };
173 }
174
175 /**
176 * @brief Construct from a center point and half-extents.
177 * @param c Box centre.
178 * @param half Half-size along each axis.
179 */
180 [[nodiscard]] static AABB3D from_center(const glm::vec3& c, const glm::vec3& half) noexcept
181 {
182 return { .min = c - half, .max = c + half };
183 }
184
185 /**
186 * @brief Construct the tightest AABB enclosing a BoundingSphere.
187 * @param s Source sphere.
188 */
189 [[nodiscard]] static AABB3D from_sphere(const BoundingSphere& s) noexcept
190 {
191 return { .min = s.center - glm::vec3(s.radius),
192 .max = s.center + glm::vec3(s.radius) };
193 }
194};
195
196// =============================================================================
197// Containment callables
198// All functions return std::function<bool(glm::vec2)> suitable for
199// direct assignment to Portal::Forma::Element::contains.
200// =============================================================================
201
202/**
203 * @brief Containment test for a circle.
204 * @param center NDC center.
205 * @param radius Radius in NDC units.
206 */
207[[nodiscard]] inline std::function<bool(glm::vec2)>
208circular_bounds(glm::vec2 center, float radius) noexcept
209{
210 float r2 = radius * radius;
211 return [center, r2](glm::vec2 p) {
212 glm::vec2 d = p - center;
213 return d.x * d.x + d.y * d.y <= r2;
214 };
215}
216
217/**
218 * @brief Containment test for a convex or concave polygon.
219 *
220 * Uses the winding number algorithm, which handles both convex and
221 * non-convex polygons correctly. Vertices are in NDC, ordered either
222 * clockwise or counter-clockwise.
223 *
224 * @param vertices Polygon vertices. Copied into the closure.
225 */
226[[nodiscard]] inline std::function<bool(glm::vec2)>
227polygon_bounds(std::span<const glm::vec2> vertices)
228{
229 std::vector<glm::vec2> verts(vertices.begin(), vertices.end());
230 return [verts = std::move(verts)](glm::vec2 p) {
231 int winding = 0;
232 size_t n = verts.size();
233 for (size_t i = 0; i < n; ++i) {
234 glm::vec2 a = verts[i];
235 glm::vec2 b = verts[(i + 1) % n];
236 if (a.y <= p.y) {
237 if (b.y > p.y) {
238 float cross = (b.x - a.x) * (p.y - a.y)
239 - (b.y - a.y) * (p.x - a.x);
240 if (cross > 0.0F)
241 ++winding;
242 }
243 } else {
244 if (b.y <= p.y) {
245 float cross = (b.x - a.x) * (p.y - a.y)
246 - (b.y - a.y) * (p.x - a.x);
247 if (cross < 0.0F)
248 --winding;
249 }
250 }
251 }
252 return winding != 0;
253 };
254}
255
256/**
257 * @brief Containment test for a polyline with a uniform half-thickness.
258 *
259 * Covers open and closed curves equally. A point is inside if its
260 * perpendicular distance to any segment is within @p half_thickness.
261 * Suitable for stroke-based interactive regions and cable hit testing.
262 *
263 * @param points Ordered polyline vertices in NDC.
264 * @param half_thickness Distance threshold in NDC units.
265 */
266[[nodiscard]] inline std::function<bool(glm::vec2)>
267stroke_bounds(std::span<const glm::vec2> points, float half_thickness)
268{
269 std::vector<glm::vec2> pts(points.begin(), points.end());
270 float t2 = half_thickness * half_thickness;
271 return [pts = std::move(pts), t2](glm::vec2 p) {
272 for (size_t i = 0; i + 1 < pts.size(); ++i) {
273 glm::vec2 a = pts[i];
274 glm::vec2 b = pts[i + 1];
275 glm::vec2 ab = b - a;
276 float len2 = glm::dot(ab, ab);
277 float d2 = 0.0F;
278 if (len2 < 1e-12F) {
279 glm::vec2 ap = p - a;
280 d2 = glm::dot(ap, ap);
281 } else {
282 float t = glm::clamp(glm::dot(p - a, ab) / len2, 0.0F, 1.0F);
283 glm::vec2 proj = a + t * ab;
284 glm::vec2 diff = p - proj;
285 d2 = glm::dot(diff, diff);
286 }
287 if (d2 <= t2)
288 return true;
289 }
290 return false;
291 };
292}
293
294// =============================================================================
295// 3D containment callables
296// All functions return std::function<bool(glm::vec3)> suitable for
297// direct use as spatial predicates in Nexus and NavigationState constraints.
298// Pair with BoundingSphere for a cheap pre-filter where the predicate is
299// expensive.
300// =============================================================================
301
302/**
303 * @brief Containment test for a sphere.
304 * @param center World-space center.
305 * @param radius Radius in world units.
306 */
307[[nodiscard]] inline std::function<bool(glm::vec3)>
308circular_bounds(glm::vec3 center, float radius) noexcept
309{
310 float r2 = radius * radius;
311 return [center, r2](const glm::vec3& p) {
312 glm::vec3 d = p - center;
313 return glm::dot(d, d) <= r2;
314 };
315}
316
317/**
318 * @brief Containment test for a convex volume defined by inward-facing half-planes.
319 *
320 * A point is inside if it satisfies all half-planes: dot(normal, p) >= offset
321 * for every plane. Normals must point inward. Suitable for authored rooms,
322 * corridors, and any convex spatial zone. Non-convex volumes are composed
323 * via union_region3 / subtract_region3.
324 *
325 * @param planes Pairs of (inward normal, signed offset). Copied into closure.
326 */
327[[nodiscard]] inline std::function<bool(glm::vec3)>
328convex_region(std::span<const std::pair<glm::vec3, float>> planes)
329{
330 std::vector<std::pair<glm::vec3, float>> ps(planes.begin(), planes.end());
331 return [ps = std::move(ps)](const glm::vec3& p) {
332 return std::ranges::all_of(
333 ps,
334 [&p](const auto& plane) {
335 const auto& [n, d] = plane;
336 return glm::dot(n, p) >= d;
337 });
338 };
339}
340
341/**
342 * @brief Containment test for a vertically extruded 2D polygon.
343 *
344 * The footprint is tested with the winding number algorithm in the XZ plane.
345 * The Y axis is the vertical: the point must satisfy y_min <= p.y <= y_max.
346 * Covers authored rooms, columns, corridors where the cross-section is
347 * constant along Y.
348 *
349 * @param footprint Polygon vertices in XZ. Copied into closure.
350 * @param y_min Lower Y bound (inclusive).
351 * @param y_max Upper Y bound (inclusive).
352 */
353[[nodiscard]] inline std::function<bool(glm::vec3)>
355 std::span<const glm::vec2> footprint,
356 float y_min,
357 float y_max)
358{
359 std::vector<glm::vec2> verts(footprint.begin(), footprint.end());
360 return [verts = std::move(verts), y_min, y_max](const glm::vec3& p) {
361 if (p.y < y_min || p.y > y_max)
362 return false;
363 glm::vec2 q { p.x, p.z };
364 int winding = 0;
365 const size_t n = verts.size();
366 for (size_t i = 0; i < n; ++i) {
367 glm::vec2 a = verts[i];
368 glm::vec2 b = verts[(i + 1) % n];
369 if (a.y <= q.y) {
370 if (b.y > q.y) {
371 float cross = (b.x - a.x) * (q.y - a.y)
372 - (b.y - a.y) * (q.x - a.x);
373 if (cross > 0.0F)
374 ++winding;
375 }
376 } else {
377 if (b.y <= q.y) {
378 float cross = (b.x - a.x) * (q.y - a.y)
379 - (b.y - a.y) * (q.x - a.x);
380 if (cross < 0.0F)
381 --winding;
382 }
383 }
384 }
385 return winding != 0;
386 };
387}
388
389// =============================================================================
390// Combinators
391// =============================================================================
392
393/**
394 * @brief Union of two containment tests. Point is inside if either returns true.
395 */
396[[nodiscard]] inline std::function<bool(glm::vec2)>
398 std::function<bool(glm::vec2)> a,
399 std::function<bool(glm::vec2)> b)
400{
401 return [a = std::move(a), b = std::move(b)](glm::vec2 p) {
402 return a(p) || b(p);
403 };
404}
405
406/**
407 * @brief Intersection of two containment tests. Point is inside only if both return true.
408 */
409[[nodiscard]] inline std::function<bool(glm::vec2)>
411 std::function<bool(glm::vec2)> a,
412 std::function<bool(glm::vec2)> b)
413{
414 return [a = std::move(a), b = std::move(b)](glm::vec2 p) {
415 return a(p) && b(p);
416 };
417}
418
419/**
420 * @brief Difference of two containment tests. Point is inside if @p a is true and @p b is false.
421 * Useful for donuts, cutouts, and exclusion zones.
422 */
423[[nodiscard]] inline std::function<bool(glm::vec2)>
425 std::function<bool(glm::vec2)> a,
426 std::function<bool(glm::vec2)> b)
427{
428 return [a = std::move(a), b = std::move(b)](glm::vec2 p) {
429 return a(p) && !b(p);
430 };
431}
432
433/**
434 * @brief Convert an NDC AABB's extent to integer pixel dimensions.
435 *
436 * Equivalent to ndc_size_to_pixels(region.max - region.min, width, height).
437 *
438 * @param region NDC axis-aligned bounding box.
439 * @param width Window width in pixels.
440 * @param height Window height in pixels.
441 * @return Integer pixel dimensions of the region's extent.
442 */
443[[nodiscard]] inline glm::uvec2 ndc_size_to_pixels(
444 const AABB2D& region,
445 uint32_t width, uint32_t height)
446{
447 return ndc_size_to_pixels(region.max - region.min, width, height);
448}
449
450// =============================================================================
451// 3D combinators
452// =============================================================================
453
454/**
455 * @brief Union of two 3D containment tests.
456 */
457[[nodiscard]] inline std::function<bool(glm::vec3)>
459 std::function<bool(glm::vec3)> a,
460 std::function<bool(glm::vec3)> b)
461{
462 return [a = std::move(a), b = std::move(b)](const glm::vec3& p) {
463 return a(p) || b(p);
464 };
465}
466
467/**
468 * @brief Intersection of two 3D containment tests.
469 */
470[[nodiscard]] inline std::function<bool(glm::vec3)>
472 std::function<bool(glm::vec3)> a,
473 std::function<bool(glm::vec3)> b)
474{
475 return [a = std::move(a), b = std::move(b)](const glm::vec3& p) {
476 return a(p) && b(p);
477 };
478}
479
480/**
481 * @brief Difference of two 3D containment tests. Inside @p a but not @p b.
482 */
483[[nodiscard]] inline std::function<bool(glm::vec3)>
485 std::function<bool(glm::vec3)> a,
486 std::function<bool(glm::vec3)> b)
487{
488 return [a = std::move(a), b = std::move(b)](const glm::vec3& p) {
489 return a(p) && !b(p);
490 };
491}
492
493} // namespace MayaFlux::Kinesis
uint32_t width
Definition Decoder.cpp:59
std::vector< glm::vec2 > * points
uint32_t h
Definition InkPress.cpp:28
size_t a
size_t b
double q
std::function< bool(glm::vec3)> convex_region(std::span< const std::pair< glm::vec3, float > > planes)
Containment test for a convex volume defined by inward-facing half-planes.
Definition Bounds.hpp:328
std::function< bool(glm::vec2)> intersect_bounds(std::function< bool(glm::vec2)> a, std::function< bool(glm::vec2)> b)
Intersection of two containment tests.
Definition Bounds.hpp:410
std::function< bool(glm::vec2)> polygon_bounds(std::span< const glm::vec2 > vertices)
Containment test for a convex or concave polygon.
Definition Bounds.hpp:227
std::function< bool(glm::vec3)> extruded_polygon_region(std::span< const glm::vec2 > footprint, float y_min, float y_max)
Containment test for a vertically extruded 2D polygon.
Definition Bounds.hpp:354
std::function< bool(glm::vec2)> subtract_bounds(std::function< bool(glm::vec2)> a, std::function< bool(glm::vec2)> b)
Difference of two containment tests.
Definition Bounds.hpp:424
glm::uvec2 ndc_size_to_pixels(const AABB2D &region, uint32_t width, uint32_t height)
Convert an NDC AABB's extent to integer pixel dimensions.
Definition Bounds.hpp:443
std::function< bool(glm::vec2)> stroke_bounds(std::span< const glm::vec2 > points, float half_thickness)
Containment test for a polyline with a uniform half-thickness.
Definition Bounds.hpp:267
std::function< bool(glm::vec2)> circular_bounds(glm::vec2 center, float radius) noexcept
Containment test for a circle.
Definition Bounds.hpp:208
std::function< bool(glm::vec2)> union_bounds(std::function< bool(glm::vec2)> a, std::function< bool(glm::vec2)> b)
Union of two containment tests.
Definition Bounds.hpp:397
static AABB2D from_pixel(float x, float y, float w, float h, uint32_t win_w, uint32_t win_h) noexcept
Construct from pixel-space top-left rect, converted to NDC.
Definition Bounds.hpp:66
static AABB2D from_ndc(glm::vec2 center, glm::vec2 half) noexcept
Definition Bounds.hpp:52
float height() const noexcept
Definition Bounds.hpp:38
AABB2D expanded(float margin) const noexcept
Definition Bounds.hpp:47
bool overlaps(const AABB2D &other) const noexcept
Definition Bounds.hpp:31
bool contains(glm::vec2 p) const noexcept
Definition Bounds.hpp:25
AABB2D translated(glm::vec2 offset) const noexcept
Definition Bounds.hpp:42
glm::vec2 center() const noexcept
Definition Bounds.hpp:40
float width() const noexcept
Definition Bounds.hpp:37
static AABB2D from_buffer_transform(glm::vec2 ndc_position, glm::vec2 ndc_scale) noexcept
Construct from a TextureBuffer NDC position and scale.
Definition Bounds.hpp:84
Axis-aligned bounding rectangle in a 2D coordinate space.
Definition Bounds.hpp:21
glm::vec3 center() const noexcept
Definition Bounds.hpp:161
bool contains(const glm::vec3 &p) const noexcept
Definition Bounds.hpp:147
bool overlaps(const AABB3D &other) const noexcept
Definition Bounds.hpp:154
glm::vec3 half_extent() const noexcept
Definition Bounds.hpp:163
static AABB3D from_sphere(const BoundingSphere &s) noexcept
Construct the tightest AABB enclosing a BoundingSphere.
Definition Bounds.hpp:189
static AABB3D from_center(const glm::vec3 &c, const glm::vec3 &half) noexcept
Construct from a center point and half-extents.
Definition Bounds.hpp:180
AABB3D translated(const glm::vec3 &offset) const noexcept
Definition Bounds.hpp:165
AABB3D expanded(float margin) const noexcept
Definition Bounds.hpp:170
glm::vec3 extent() const noexcept
Definition Bounds.hpp:162
Axis-aligned bounding box in 3D world space.
Definition Bounds.hpp:143
bool overlaps_ray(const Ray &ray) const noexcept
Returns true if the ray passes through or originates inside the sphere.
Definition Bounds.hpp:119
bool contains(const glm::vec3 &p) const noexcept
Definition Bounds.hpp:109
Spherical fast-reject hint in world space.
Definition Bounds.hpp:105
Origin and normalized direction in world space.
Definition HitTest.hpp:17