Skip to main content

supreme_replay_analyzer/
plane_intersection.rs

1//! Plane intersection detection for checkpoint crossing analysis
2
3use crate::{
4    RowData,
5    models::{Level, LevelDataEntry, Position, Quaternion},
6};
7
8#[derive(Debug, Clone)]
9pub struct PlaneIntersectionInfo {
10    pub distance: f32,
11    pub frame1: usize,
12    pub frame2: usize,
13    pub exact_time_with_start_lag_ms: f32, // Exact crossing time using linear interpolation
14    pub object_name: String,
15}
16
17#[derive(Debug, Clone)]
18pub struct LevelPlaneIntersection {
19    pub name: String,
20    pub intersections: Vec<PlaneIntersectionInfo>,
21}
22
23#[must_use]
24pub fn get_plane_collisions(rows: &[RowData]) -> Vec<LevelPlaneIntersection> {
25    get_plane_intersections_with_level_data(rows, &[])
26}
27
28#[must_use]
29pub fn get_plane_intersections_with_level_data(
30    rows: &[RowData],
31    levels: &[Level],
32) -> Vec<LevelPlaneIntersection> {
33    let row_positions: Vec<Position> = rows
34        .iter()
35        .map(|r| Position {
36            x: r.x,
37            y: r.y,
38            z: r.z,
39        })
40        .collect();
41
42    let mut result = Vec::new();
43
44    // Iterate over actual levels
45    for level in levels {
46        let plane_entries: Vec<&LevelDataEntry> = level
47            .entries
48            .iter()
49            .filter(|e| {
50                [
51                    "Start_Point",
52                    "Check_Point_1",
53                    "Check_Point_2",
54                    "Finish_Point",
55                ]
56                .contains(&e.name.as_str())
57            })
58            .collect();
59
60        let mut level_plane_intersection = LevelPlaneIntersection {
61            name: level.name.clone(),
62            intersections: Vec::new(),
63        };
64
65        for i in 0..row_positions.len() - 1 {
66            let p1 = &row_positions[i];
67            let p2 = &row_positions[i + 1];
68
69            for plane_entry in &plane_entries {
70                let plane_normal = get_plane_normal(&plane_entry.quat);
71
72                // Calculate intersection point and exact crossing time
73                if let Some((intersection, t)) = does_line_segment_collide_with_plane_with_t(
74                    &plane_entry.position,
75                    &plane_normal,
76                    p1,
77                    p2,
78                ) {
79                    let distance = calculate_distance(&intersection, &plane_entry.position);
80                    // Calculate exact crossing time using linear interpolation
81                    // Each frame represents 10ms, so frame1 = i * 10ms, frame2 = (i+1) * 10ms
82                    #[allow(clippy::cast_precision_loss)]
83                    let exact_time_ms = (i as f32 * 10.0) + (t * 10.0);
84
85                    level_plane_intersection
86                        .intersections
87                        .push(PlaneIntersectionInfo {
88                            distance,
89                            frame1: i,
90                            frame2: i + 1,
91                            exact_time_with_start_lag_ms: exact_time_ms,
92                            object_name: plane_entry.name.clone(),
93                        });
94                }
95            }
96        }
97
98        result.push(level_plane_intersection);
99    }
100
101    result
102}
103
104fn get_plane_normal(quat: &Quaternion) -> Position {
105    Position {
106        x: 2.0 * (quat.x * quat.z + quat.w * quat.y),
107        y: 2.0 * (quat.y * quat.z - quat.w * quat.x),
108        z: 1.0 - 2.0 * (quat.x * quat.x + quat.y * quat.y),
109    }
110}
111
112fn does_line_segment_collide_with_plane_with_t(
113    plane_position: &Position,
114    plane_normal: &Position,
115    p1: &Position,
116    p2: &Position,
117) -> Option<(Position, f32)> {
118    let normal_length = (plane_normal.x * plane_normal.x
119        + plane_normal.y * plane_normal.y
120        + plane_normal.z * plane_normal.z)
121        .sqrt();
122
123    if normal_length < 1e-6 {
124        return None;
125    }
126
127    let normalized_normal = Position {
128        x: plane_normal.x / normal_length,
129        y: plane_normal.y / normal_length,
130        z: plane_normal.z / normal_length,
131    };
132
133    let direction = Position {
134        x: p2.x - p1.x,
135        y: p2.y - p1.y,
136        z: p2.z - p1.z,
137    };
138
139    let dot_normal_direction = normalized_normal.x * direction.x
140        + normalized_normal.y * direction.y
141        + normalized_normal.z * direction.z;
142
143    if dot_normal_direction.abs() < 1e-6 {
144        return None;
145    }
146
147    let to_start = Position {
148        x: p1.x - plane_position.x,
149        y: p1.y - plane_position.y,
150        z: p1.z - plane_position.z,
151    };
152
153    let dot_normal_to_start = normalized_normal.x * to_start.x
154        + normalized_normal.y * to_start.y
155        + normalized_normal.z * to_start.z;
156
157    let t = -dot_normal_to_start / dot_normal_direction;
158
159    if !(0.0..=1.0).contains(&t) {
160        return None;
161    }
162
163    Some((
164        Position {
165            x: p1.x + t * direction.x,
166            y: p1.y + t * direction.y,
167            z: p1.z + t * direction.z,
168        },
169        t,
170    ))
171}
172
173fn calculate_distance(p1: &Position, p2: &Position) -> f32 {
174    let dx = p2.x - p1.x;
175    let dy = p2.y - p1.y;
176    let dz = p2.z - p1.z;
177    (dx * dx + dy * dy + dz * dz).sqrt()
178}