#![allow(clippy::many_single_char_names)]
use crate::data::{Attachment, AttachmentRef};
use crate::skeleton::Skeleton;
#[derive(Debug, Clone, Default)]
pub struct BoundsPolygon {
pub vertices: Vec<f32>,
pub count: usize,
}
impl BoundsPolygon {
pub fn iter_vertices(&self) -> impl Iterator<Item = (f32, f32)> + '_ {
self.vertices[..self.count]
.as_chunks::<2>()
.0
.iter()
.map(|xy| (xy[0], xy[1]))
}
}
#[derive(Debug, Default)]
pub struct SkeletonBounds {
bounding_boxes: Vec<AttachmentRef>,
polygons: Vec<BoundsPolygon>,
min_x: f32,
min_y: f32,
max_x: f32,
max_y: f32,
}
impl SkeletonBounds {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, skeleton: &Skeleton, update_aabb: bool) {
self.bounding_boxes.clear();
let reused = self.polygons.len();
let mut slot_count_seen = 0;
for slot_idx in 0..skeleton.slots.len() {
let slot = &skeleton.slots[slot_idx];
let bone_id = skeleton.data.slots[slot_idx].bone;
if !skeleton.bones[bone_id.index()].active {
continue;
}
let Some(attachment_id) = slot.applied().attachment else {
continue;
};
let Attachment::BoundingBox(bbox) = skeleton.attachment(attachment_id) else {
continue;
};
let count = bbox.vertex_data.world_vertices_length as usize;
let polygon = if slot_count_seen < reused {
&mut self.polygons[slot_count_seen]
} else {
self.polygons.push(BoundsPolygon::default());
&mut self.polygons[slot_count_seen]
};
polygon.count = count;
if polygon.vertices.len() < count {
polygon.vertices.resize(count, 0.0);
}
skeleton.compute_world_vertices(
&bbox.vertex_data,
crate::data::SlotId(slot_idx as u16),
0,
count,
&mut polygon.vertices,
0,
2,
);
self.bounding_boxes.push(attachment_id);
slot_count_seen += 1;
}
self.polygons.truncate(slot_count_seen);
if update_aabb {
self.compute_aabb();
} else {
self.min_x = f32::MIN;
self.min_y = f32::MIN;
self.max_x = f32::MAX;
self.max_y = f32::MAX;
}
}
fn compute_aabb(&mut self) {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for polygon in &self.polygons {
for (x, y) in polygon.iter_vertices() {
if x < min_x {
min_x = x;
}
if y < min_y {
min_y = y;
}
if x > max_x {
max_x = x;
}
if y > max_y {
max_y = y;
}
}
}
self.min_x = min_x;
self.min_y = min_y;
self.max_x = max_x;
self.max_y = max_y;
}
#[must_use]
pub fn aabb_contains_point(&self, x: f32, y: f32) -> bool {
x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
}
#[must_use]
pub fn aabb_intersects_segment(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
let (min_x, min_y, max_x, max_y) = (self.min_x, self.min_y, self.max_x, self.max_y);
if (x1 <= min_x && x2 <= min_x)
|| (y1 <= min_y && y2 <= min_y)
|| (x1 >= max_x && x2 >= max_x)
|| (y1 >= max_y && y2 >= max_y)
{
return false;
}
let m = (y2 - y1) / (x2 - x1);
let y = m * (min_x - x1) + y1;
if y > min_y && y < max_y {
return true;
}
let y = m * (max_x - x1) + y1;
if y > min_y && y < max_y {
return true;
}
let x = (min_y - y1) / m + x1;
if x > min_x && x < max_x {
return true;
}
let x = (max_y - y1) / m + x1;
if x > min_x && x < max_x {
return true;
}
false
}
#[must_use]
pub fn aabb_intersects_skeleton(&self, other: &SkeletonBounds) -> bool {
self.min_x < other.max_x
&& self.max_x > other.min_x
&& self.min_y < other.max_y
&& self.max_y > other.min_y
}
#[must_use]
pub fn polygon_contains_point(polygon: &BoundsPolygon, x: f32, y: f32) -> bool {
let vertices = &polygon.vertices;
let nn = polygon.count;
if nn < 6 {
return false;
}
let mut prev_index = nn - 2;
let mut inside = false;
let mut ii = 0;
while ii < nn {
let vertex_y = vertices[ii + 1];
let prev_y = vertices[prev_index + 1];
if (vertex_y < y && prev_y >= y) || (prev_y < y && vertex_y >= y) {
let vertex_x = vertices[ii];
if vertex_x
+ (y - vertex_y) / (prev_y - vertex_y) * (vertices[prev_index] - vertex_x)
< x
{
inside = !inside;
}
}
prev_index = ii;
ii += 2;
}
inside
}
#[must_use]
pub fn contains_point(&self, x: f32, y: f32) -> Option<AttachmentRef> {
for (i, polygon) in self.polygons.iter().enumerate() {
if Self::polygon_contains_point(polygon, x, y) {
return Some(self.bounding_boxes[i]);
}
}
None
}
#[must_use]
pub fn intersects_segment(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> Option<AttachmentRef> {
for (i, polygon) in self.polygons.iter().enumerate() {
if Self::polygon_intersects_segment(polygon, x1, y1, x2, y2) {
return Some(self.bounding_boxes[i]);
}
}
None
}
#[must_use]
pub fn polygon_intersects_segment(
polygon: &BoundsPolygon,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
) -> bool {
let vertices = &polygon.vertices;
let nn = polygon.count;
if nn < 6 {
return false;
}
let width12 = x1 - x2;
let height12 = y1 - y2;
let det1 = x1 * y2 - y1 * x2;
let mut x3 = vertices[nn - 2];
let mut y3 = vertices[nn - 1];
let mut ii = 0;
while ii < nn {
let x4 = vertices[ii];
let y4 = vertices[ii + 1];
let det2 = x3 * y4 - y3 * x4;
let width34 = x3 - x4;
let height34 = y3 - y4;
let det3 = width12 * height34 - height12 * width34;
let x = (det1 * width34 - width12 * det2) / det3;
if ((x >= x3 && x <= x4) || (x >= x4 && x <= x3))
&& ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))
{
let y = (det1 * height34 - height12 * det2) / det3;
if ((y >= y3 && y <= y4) || (y >= y4 && y <= y3))
&& ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))
{
return true;
}
}
x3 = x4;
y3 = y4;
ii += 2;
}
false
}
#[must_use]
pub fn polygon_for(&self, attachment_id: AttachmentRef) -> Option<&BoundsPolygon> {
self.bounding_boxes
.iter()
.position(|&a| a == attachment_id)
.map(|i| &self.polygons[i])
}
#[must_use]
pub fn polygons(&self) -> &[BoundsPolygon] {
&self.polygons
}
#[must_use]
pub fn bounding_boxes(&self) -> &[AttachmentRef] {
&self.bounding_boxes
}
#[must_use]
pub fn width(&self) -> f32 {
self.max_x - self.min_x
}
#[must_use]
pub fn height(&self) -> f32 {
self.max_y - self.min_y
}
#[must_use]
pub fn aabb(&self) -> (f32, f32, f32, f32) {
(self.min_x, self.min_y, self.max_x, self.max_y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_is_empty() {
let b = SkeletonBounds::new();
assert!(b.polygons().is_empty());
assert_eq!(b.bounding_boxes(), []);
}
#[test]
fn polygon_contains_point_square() {
let polygon = BoundsPolygon {
vertices: vec![0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0],
count: 8,
};
assert!(SkeletonBounds::polygon_contains_point(&polygon, 5.0, 5.0));
assert!(!SkeletonBounds::polygon_contains_point(&polygon, -1.0, 5.0));
assert!(!SkeletonBounds::polygon_contains_point(&polygon, 5.0, 20.0));
}
#[test]
fn polygon_intersects_segment_crosses_square_edge() {
let polygon = BoundsPolygon {
vertices: vec![0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0],
count: 8,
};
assert!(SkeletonBounds::polygon_intersects_segment(
&polygon, -5.0, 5.0, 5.0, 5.0
));
assert!(!SkeletonBounds::polygon_intersects_segment(
&polygon, -5.0, -5.0, -1.0, -5.0
));
}
#[test]
fn update_on_example_rig() {
use crate::atlas::Atlas;
use crate::load::{AtlasAttachmentLoader, SkeletonBinary};
use crate::skeleton::{Physics, Skeleton};
use std::sync::Arc;
let Ok(atlas_src) =
std::fs::read_to_string("../spine-runtimes/examples/goblins/export/goblins-pma.atlas")
else {
return;
};
let atlas = Atlas::parse(&atlas_src).unwrap();
let mut loader = AtlasAttachmentLoader::new(&atlas);
let bytes =
std::fs::read("../spine-runtimes/examples/goblins/export/goblins-pro.skel").unwrap();
let data = Arc::new(
SkeletonBinary::with_loader(&mut loader)
.read(&bytes)
.unwrap(),
);
let mut sk = Skeleton::new(Arc::clone(&data));
sk.setup_pose();
sk.update_world_transform(Physics::None);
let mut bounds = SkeletonBounds::new();
bounds.update(&sk, true);
for polygon in bounds.polygons() {
assert!(polygon.count > 0);
assert!(polygon.count.is_multiple_of(2));
for (x, y) in polygon.iter_vertices() {
assert!(x.is_finite() && y.is_finite());
}
}
}
}