/*
* InfiniPaint
* Copyright (C) 2025-2026 Yousef Khadadeh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#pragma once
#include "Helpers/MathExtras.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Eigen;
namespace SCollision {
template class AABB {
public:
typedef T UnderlyingType;
AABB() = default; // NOTE: Should not use the min and max numeric limits to initialize, since that wont work for WorldScalar
AABB(const Vector& initMin, const Vector& initMax):
min(initMin),
max(initMax)
{}
AABB(const SkRect& r):
min(r.TL().x(), r.TL().y()),
max(r.BR().x(), r.BR().y())
{}
Vector min;
Vector max;
Vector center() const { return (min + max) / T(2); }
Vector bottom_left() const { return Vector{min.x(), max.y()}; }
Vector top_right() const { return Vector{max.x(), min.y()}; }
Vector dim() const { return max - min; }
T width() const { return max.x() - min.x(); }
T height() const { return max.y() - min.y(); }
// Do not try to get intersection between AABBs unless you know they collide
AABB get_intersection_between_aabbs(const AABB& other) const {
return {
cwise_vec_max(other.min, min),
cwise_vec_min(other.max, max)
};
}
void include_aabb_in_bounds(const AABB& other) {
min = cwise_vec_min(other.min, min);
max = cwise_vec_max(other.max, max);
}
void include_point_in_bounds(const Vector& p) {
min = cwise_vec_min(p, min);
max = cwise_vec_max(p, max);
}
bool operator==(const AABB& other) const {
return min == other.min && max == other.max;
}
bool fully_contains_aabb(const AABB& other) const {
return other.min.x() >= min.x() && other.min.y() >= min.y() && other.max.x() <= max.x() && other.max.y() <= max.y();
}
Vector clamp_point_to_aabb(const Vector& p) const {
return {
std::clamp(p.x(), min.x(), max.x()),
std::clamp(p.y(), min.y(), max.y())
};
}
template AABB transform(const std::function(const Vector&)>& transformFunc, const std::function& transformLinearFunc) const {
Vector aabb1 = transformFunc(min);
Vector aabb2 = transformFunc(max);
Vector aabb3 = transformFunc(top_right());
Vector aabb4 = transformFunc(bottom_left());
AABB toRet{aabb1, aabb1};
toRet.include_point_in_bounds(aabb1);
toRet.include_point_in_bounds(aabb2);
toRet.include_point_in_bounds(aabb3);
toRet.include_point_in_bounds(aabb4);
return toRet;
}
template AABB cast() {
AABB toRet{min.template cast(), max.template cast()};
return toRet;
}
template void serialize(Archive& a) {
a(min, max);
}
SDL_Rect get_sdl_rect() const {
return SDL_Rect{static_cast(min.x()), static_cast(min.y()), static_cast(width()), static_cast(height())};
}
SkRect get_sk_rect() const {
return SkRect::MakeLTRB(static_cast(min.x()), static_cast(min.y()), static_cast(max.x()), static_cast(max.y()));
}
SkIRect get_sk_irect() const {
return SkIRect::MakeLTRB(static_cast(min.x()), static_cast(min.y()), static_cast(max.x()), static_cast(max.y()));
}
};
template class Circle {
public:
typedef T UnderlyingType;
Circle() = default;
Circle(const Vector& initPos, T initRadius):
pos(initPos),
radius(initRadius),
bounds(pos - Vector{radius, radius}, pos + Vector{radius, radius})
{}
Vector pos;
T radius;
AABB bounds;
template Circle transform(const std::function(const Vector&)>& transformFunc, const std::function& transformLinearFunc) const {
Circle newCirc = Circle();
newCirc.pos = transformFunc(pos);
newCirc.radius = transformLinearFunc(radius);
newCirc.bounds = AABB(newCirc.pos - Vector{newCirc.radius, newCirc.radius}, newCirc.pos + Vector{newCirc.radius, newCirc.radius});
return newCirc;
}
template Circle cast() {
Circle toRet;
toRet.pos = pos.template cast();
toRet.radius = static_cast(radius);
toRet.bounds = bounds.template cast();
return toRet;
}
};
template class Triangle {
public:
typedef T UnderlyingType;
Triangle() = default;
Triangle(const std::array, 3>& initP):
p(initP),
bounds(cwise_vec_min(cwise_vec_min(p[0], p[1]), p[2]), cwise_vec_max(cwise_vec_max(p[0], p[1]), p[2]))
{}
Triangle(const Vector& p1, const Vector& p2, const Vector& p3):
p({p1, p2, p3}),
bounds(cwise_vec_min(cwise_vec_min(p[0], p[1]), p[2]), cwise_vec_max(cwise_vec_max(p[0], p[1]), p[2]))
{}
template Triangle transform(const std::function(const Vector&)>& transformFunc, const std::function& transformLinearFunc) const {
Triangle toRet = Triangle();
toRet.p[0] = transformFunc(p[0]);
toRet.p[1] = transformFunc(p[1]);
toRet.p[2] = transformFunc(p[2]);
toRet.bounds = AABB(cwise_vec_min(cwise_vec_min(toRet.p[0], toRet.p[1]), toRet.p[2]), cwise_vec_max(cwise_vec_max(toRet.p[0], toRet.p[1]), toRet.p[2]));
return toRet;
}
template Triangle cast() {
Triangle toRet;
toRet.p[0] = p[0].template cast();
toRet.p[1] = p[1].template cast();
toRet.p[2] = p[2].template cast();
toRet.bounds = bounds.template cast();
return toRet;
}
std::array, 3> p;
AABB bounds;
};
template struct ColliderCollection {
typedef T UnderlyingType;
AABB bounds;
std::vector> aabb;
std::vector> circle;
std::vector> triangle;
void clear() {
aabb.clear();
circle.clear();
triangle.clear();
bounds = AABB({0, 0}, {0, 0});
}
size_t size() const { return aabb.size() + circle.size() + triangle.size(); }
bool empty() const { return aabb.empty() && circle.empty() && triangle.empty(); }
template void bound_limit(const ColliderList& list) {
for(auto& o : list)
bounds.include_aabb_in_bounds(get_bounds(o));
}
void recalculate_bounds() {
bounds = AABB();
if(!aabb.empty())
bounds = get_bounds(aabb.front());
else if(!circle.empty())
bounds = get_bounds(circle.front());
else if(!triangle.empty())
bounds = get_bounds(triangle.front());
bound_limit(aabb);
bound_limit(circle);
bound_limit(triangle);
}
template ColliderCollection transform(const std::function(const Vector&)>& transformFunc, const std::function& transformLinearFunc) const {
ColliderCollection toRet;
for(auto& o : aabb)
toRet.aabb.emplace_back(o.transform(transformFunc, transformLinearFunc));
for(auto& o : circle)
toRet.circle.emplace_back(o.transform(transformFunc, transformLinearFunc));
for(auto& o : triangle)
toRet.triangle.emplace_back(o.transform(transformFunc, transformLinearFunc));
toRet.recalculate_bounds();
return toRet;
}
};
template std::ostream& operator<<(std::ostream& os, const AABB& a) {
return os << "min: [" << a.min.x() << " " << a.min.y() << "]\n"
<< "max: [" << a.max.x() << " " << a.max.y() << "]\n";
}
template const AABB& get_bounds(const Circle& c) { return c.bounds; }
template const AABB& get_bounds(const Triangle& c) { return c.bounds; }
template const AABB& get_bounds(const AABB& a) { return a; }
template const AABB& get_bounds(const ColliderCollection& c) { return c.bounds; }
template void generate_wide_line(ColliderCollection& collection, const Vector& p1, const Vector& p2, T width, bool circleCaps) {
if(p1 != p2) {
Vector dir = p1 - p2;
Vector perpVec = perpendicular_vec2(dir);
perpVec.normalize();
Vector perpWidth = (perpVec * width * T(0.5)).eval();
collection.triangle.emplace_back(Triangle{p1 + perpWidth, p1 - perpWidth, p2 + perpWidth});
collection.triangle.emplace_back(Triangle{p2 + perpWidth, p2 - perpWidth, p1 - perpWidth});
}
if(circleCaps) {
collection.circle.emplace_back(Circle{p1, width * T(0.5)});
if(p1 != p2)
collection.circle.emplace_back(Circle{p2, width * T(0.5)});
}
collection.recalculate_bounds();
}
template Vector two_dim_vec_slerp(const Vector& v1, const Vector& v2) {
Vector v1Norm = v1.normalized();
Vector v2Norm = v2.normalized();
Vector avg = (v1Norm + v2Norm) * 0.5;
if(std::abs(avg.x()) < 0.00001 && std::abs(avg.y()) < 0.00001)
return perpendicular_vec2(v1Norm).normalized();
else
return avg.normalized();
}
template void generate_polyline(ColliderCollection& collection, const std::vector>& points, T width, bool closePath) {
if(points.size() < 2) {
return;
}
Vector perpPrev = perpendicular_vec2((points[1] - points.front()).eval());
perpPrev.normalize();
std::array, 3> vertArray;
unsigned newIndex = 0;
if(!closePath || points.size() == 2) {
vertArray[0] = points.front() + perpPrev * width * 0.5;
vertArray[1] = points.front() - perpPrev * width * 0.5;
newIndex = 2;
}
if(points.size() == 2) {
vertArray[newIndex] = points.back() + perpPrev * width * 0.5;
collection.triangle.emplace_back(Triangle{vertArray});
newIndex = (newIndex + 1) % 3;
vertArray[newIndex] = points.back() - perpPrev * width * 0.5;
collection.triangle.emplace_back(Triangle{vertArray});
return;
}
for(size_t x = 0; x <= points.size(); x++) {
size_t i = x;
size_t iP1 = x + 1;
size_t iM1 = x - 1;
if(x == 0) {
if(!closePath)
continue;
else
iM1 = points.size() - 1;
}
else if(x == points.size() - 1) {
if(!closePath)
continue;
else {
iP1 = 0;
}
}
else if(x == points.size()) {
if(!closePath)
continue;
else {
i = 0;
iM1 = points.size() - 1;
iP1 = 1;
}
}
Vector v1Norm = (points[iM1] - points[i]).normalized();
Vector v2Norm = (points[iP1] - points[i]).normalized();
Vector perp = two_dim_vec_slerp(v1Norm, v2Norm);
if(perp.dot(perpPrev) <= 0.0)
perp = -perp;
T angle1 = std::acos((perp.dot(v1Norm) < 0 ? -perp : perp).dot(v1Norm));
T angle2 = std::acos((perp.dot(v2Norm) < 0 ? -perp : perp).dot(v2Norm));
T sinAngle1 = std::sin(angle1 + angle2);
T alteredWidth = std::lerp(0.5, 0.707, sinAngle1);
vertArray[newIndex] = points[i] + perp * alteredWidth * width;
newIndex = (newIndex + 1) % 3;
if(x != 0)
collection.triangle.emplace_back(Triangle{vertArray});
vertArray[newIndex] = points[i] - perp * alteredWidth * width;
newIndex = (newIndex + 1) % 3;
if(x != 0)
collection.triangle.emplace_back(Triangle{vertArray});
perpPrev = perp;
}
if(!closePath) {
Vector perp = perpendicular_vec2((points[points.size() - 2] - points.back()).eval());
perp.normalize();
if(perp.dot(perpPrev) <= 0.0)
perp = -perp;
vertArray[newIndex] = points.back() + perp * width * 0.5;
newIndex = (newIndex + 1) % 3;
collection.triangle.emplace_back(Triangle{vertArray});
vertArray[newIndex] = points.back() - perp * width * 0.5;
newIndex = (newIndex + 1) % 3;
collection.triangle.emplace_back(Triangle{vertArray});
}
}
// https://blackpawn.com/texts/pointinpoly/default.html
template bool collide(const Vector& p, const Triangle& t) {
Vector v0 = t.p[2] - t.p[0];
Vector v1 = t.p[1] - t.p[0];
Vector v2 = p - t.p[0];
T dot00 = v0.dot(v0);
T dot01 = v0.dot(v1);
T dot02 = v0.dot(v2);
T dot11 = v1.dot(v1);
T dot12 = v1.dot(v2);
T invDenom = T(1) / (dot00 * dot11 - dot01 * dot01);
T u = (dot11 * dot02 - dot01 * dot12) * invDenom;
T v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return (u >= T(0)) && (v >= T(0)) && (u + v < T(1));
}
template bool collide(const Triangle& t, const Vector& p) {
return collide(p, t);
}
template bool collide(const Vector& p, const AABB& a) {
return (p.x() > a.min.x() && p.x() < a.max.x() && p.y() > a.min.y() && p.y() < a.max.y());
}
template bool collide(const AABB& a, const Vector& p) {
return collide(p, a);
}
template bool collide(const Vector& p, const Circle& a) {
return vec_distance(p, a.pos) <= a.radius;
}
template bool collide(const Circle& a, const Vector& p) {
return collide(p, a);
}
template bool collide(const Circle& c1, const Circle& c2) {
return vec_distance(c1.pos, c2.pos) <= (c1.radius + c2.radius);
}
// https://gamedev.stackexchange.com/a/178154
template bool collide(const Circle& c, const AABB& a) {
if(!collide(get_bounds(c), a))
return false;
Vector aabbCenter = a.center();
Vector aabbBounds = aabbCenter - a.min;
Vector distance = c.pos - aabbCenter;
Vector clampDst = cwise_vec_clamp(distance, (-aabbBounds).eval(), aabbBounds);
Vector closestPoint = aabbCenter + clampDst;
return vec_distance(closestPoint, c.pos) <= c.radius;
}
template bool collide(const AABB& a, const Circle& c) {
return collide(c, a);
}
template bool collide(const AABB& a1, const AABB& a2) {
return a1.min.x() < a2.max.x() && a1.max.x() > a2.min.x() &&
a1.min.y() < a2.max.y() && a1.max.y() > a2.min.y();
}
template bool collide(const Circle& c, const Triangle& t) {
if(!collide(get_bounds(c), get_bounds(t)))
return false;
return collide(t.p[0], c) ||
collide(t.p[1], c) ||
collide(t.p[2], c) ||
collide(c.pos, t) ||
collision_circle_line_segment(c.pos, c.radius, t.p[0], t.p[1]) ||
collision_circle_line_segment(c.pos, c.radius, t.p[1], t.p[2]) ||
collision_circle_line_segment(c.pos, c.radius, t.p[2], t.p[0]);
}
template bool collide(const Triangle& t, const Circle& c) {
return collide(c, t);
}
template bool collide(const AABB& a, const Triangle& t) {
if(!collide(a, get_bounds(t)))
return false;
return collide(t.p[0], a) ||
collide(t.p[1], a) ||
collide(t.p[2], a) ||
collide(a.min, t) ||
collide(a.max, t) ||
collide(a.bottom_left(), t) ||
collide(a.top_right(), t) ||
is_collision_line_segment_line_segment(t.p[0], t.p[1], a.min, a.top_right()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[1], a.min, a.bottom_left()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[1], a.max, a.top_right()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[1], a.max, a.bottom_left()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[2], a.min, a.top_right()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[2], a.min, a.bottom_left()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[2], a.max, a.top_right()) ||
is_collision_line_segment_line_segment(t.p[0], t.p[2], a.max, a.bottom_left()) ||
is_collision_line_segment_line_segment(t.p[2], t.p[1], a.min, a.top_right()) ||
is_collision_line_segment_line_segment(t.p[2], t.p[1], a.min, a.bottom_left()) ||
is_collision_line_segment_line_segment(t.p[2], t.p[1], a.max, a.top_right()) ||
is_collision_line_segment_line_segment(t.p[2], t.p[1], a.max, a.bottom_left());
}
template bool collide(const Triangle& t, const AABB& a) {
return collide(a, t);
}
template bool collide(const Triangle& t1, const Triangle& t2) {
//return collide(get_bounds(t1), get_bounds(t2));
if(!collide(get_bounds(t1), get_bounds(t2)))
return false;
return collide(t1.p[0], t2) ||
collide(t1.p[1], t2) ||
collide(t1.p[2], t2) ||
collide(t2.p[0], t1) ||
collide(t2.p[1], t1) ||
collide(t2.p[2], t1) ||
is_collision_line_segment_line_segment(t1.p[0], t1.p[1], t2.p[0], t2.p[1]) ||
is_collision_line_segment_line_segment(t1.p[0], t1.p[1], t2.p[1], t2.p[2]) ||
is_collision_line_segment_line_segment(t1.p[0], t1.p[1], t2.p[2], t2.p[0]) ||
is_collision_line_segment_line_segment(t1.p[0], t1.p[2], t2.p[0], t2.p[1]) ||
is_collision_line_segment_line_segment(t1.p[0], t1.p[2], t2.p[1], t2.p[2]) ||
is_collision_line_segment_line_segment(t1.p[0], t1.p[2], t2.p[2], t2.p[0]) ||
is_collision_line_segment_line_segment(t1.p[2], t1.p[1], t2.p[0], t2.p[1]) ||
is_collision_line_segment_line_segment(t1.p[2], t1.p[1], t2.p[1], t2.p[2]) ||
is_collision_line_segment_line_segment(t1.p[2], t1.p[1], t2.p[2], t2.p[0]);
}
template bool collide(const Collection& collection, const Collider& c) {
for(auto& o : collection.circle)
if(collide(c, o))
return true;
for(auto& o : collection.aabb)
if(collide(c, o))
return true;
for(auto& o : collection.triangle)
if(collide(c, o))
return true;
return false;
}
template class BVHContainer {
public:
typedef T UnderlyingType;
std::vector> children;
ColliderCollection objects;
// NOTE: BVH TRANSFORMATION LEADS TO A BROKEN BVH
//void transform(const std::function(const Vector&)>& transformFunc, const std::function& transformLinearFunc) {
// objects.transform(transformFunc, transformLinearFunc);
// for(auto& a : children)
// a.transform(transformFunc, transformLinearFunc);
//}
void clear() {
children.clear();
objects.clear();
}
bool empty() const {
return children.empty() && objects.empty();
}
unsigned assign_quad_to_point(const Vector& point, const Vector& origin) const {
bool negativeX = point.x() <= origin.x();
bool negativeY = point.y() <= origin.y();
if(negativeX && negativeY)
return 0;
else if(negativeX && !negativeY)
return 1;
else if(!negativeX && !negativeY)
return 2;
else
return 3;
}
void calculate_bvh_recursive(ColliderCollection& bvhObjects, int maxDepth = -1) {
bvhObjects.recalculate_bounds();
objects.bounds = bvhObjects.bounds;
if(bvhObjects.empty()) {
return;
}
if(bvhObjects.size() <= 2 || maxDepth == 0) {
objects = bvhObjects;
return;
}
ColliderCollection parts[4];
Vector halfwayPoint = bvhObjects.bounds.center();
for(auto& o : bvhObjects.aabb)
parts[assign_quad_to_point(get_bounds(o).center(), halfwayPoint)].aabb.emplace_back(o);
for(auto& o : bvhObjects.circle)
parts[assign_quad_to_point(get_bounds(o).center(), halfwayPoint)].circle.emplace_back(o);
for(auto& o : bvhObjects.triangle)
parts[assign_quad_to_point(get_bounds(o).center(), halfwayPoint)].triangle.emplace_back(o);
for(size_t i = 0; i < 4; i++) {
if(parts[i].empty())
continue;
else if(parts[i].size() == bvhObjects.size()) {
objects = bvhObjects;
break;
}
else {
children.emplace_back();
children.back().calculate_bvh_recursive(parts[i], maxDepth - 1);
}
}
}
template void is_collide_triangle_bounds_func(const Collider& collider, const std::function&)> triangleFunc) const {
if(!collide(get_bounds(collider), get_bounds(objects)))
return;
for(auto& t : objects.triangle) {
if(collide(t.bounds, collider))
triangleFunc(t);
}
for(const auto& c : children)
c.is_collide_triangle_bounds_func(collider, triangleFunc);
}
template bool is_collide(const Collider& collider) const {
if(!collide(get_bounds(collider), get_bounds(objects)))
return false;
if(collide(objects, collider))
return true;
for(const auto& c : children)
if(c.is_collide(collider))
return true;
return false;
}
template bool is_collide_other_bvh(const BVH& bvh) const {
if(!collide(get_bounds(objects), get_bounds(bvh.objects)))
return false;
if(is_collide(bvh.objects))
return true;
if(bvh.is_collide(objects))
return true;
for(const auto& c : children) {
for(const auto& d : bvh.children) {
if(c.is_collide_other_bvh(d))
return true;
}
}
return false;
}
};
};