| 1 | use crate::{ |
| 2 | geometry::{Angle, Point, PointExt, Real, Trigonometry}, |
| 3 | primitives::{common::LineSide, Line}, |
| 4 | }; |
| 5 | |
| 6 | /// Scaling factor for unit length normal vectors. |
| 7 | pub const NORMAL_VECTOR_SCALE: i32 = 1 << 10; |
| 8 | |
| 9 | /// Linear equation. |
| 10 | /// |
| 11 | /// The equation is stored as a normal vector and the distance to the origin. |
| 12 | #[derive (Copy, Clone, PartialEq, PartialOrd, Debug)] |
| 13 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
| 14 | pub struct LinearEquation { |
| 15 | /// Normal vector, perpendicular to the line. |
| 16 | /// |
| 17 | /// The unit vector is scaled up to increase the resolution. |
| 18 | pub normal_vector: Point, |
| 19 | |
| 20 | /// Distance from the origin. |
| 21 | /// |
| 22 | /// The distance doesn't directly correlate to the distance in pixels, but is |
| 23 | /// scaled up by the length of the normal vector. |
| 24 | pub origin_distance: i32, |
| 25 | } |
| 26 | |
| 27 | impl LinearEquation { |
| 28 | /// Creates a new linear equation with the given angle and distance to the origin. |
| 29 | pub fn with_angle_and_distance(angle: Angle, origin_distance: i32) -> Self { |
| 30 | Self { |
| 31 | normal_vector: OriginLinearEquation::with_angle(angle).normal_vector, |
| 32 | origin_distance, |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | /// Creates a new linear equation from a line. |
| 37 | pub fn from_line(line: &Line) -> Self { |
| 38 | let normal_vector = line.delta().rotate_90(); |
| 39 | let origin_distance = line.start.dot_product(normal_vector); |
| 40 | |
| 41 | Self { |
| 42 | normal_vector, |
| 43 | origin_distance, |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /// Returns the distance between the line and a point. |
| 48 | /// |
| 49 | /// The scaling of the returned value depends on the length of the normal vector. |
| 50 | /// Positive values will be returned for points on the left side of the line and negative |
| 51 | /// values for points on the right. |
| 52 | pub fn distance(&self, point: Point) -> i32 { |
| 53 | point.dot_product(self.normal_vector) - self.origin_distance |
| 54 | } |
| 55 | |
| 56 | /// Checks if a point is on the given side of the line. |
| 57 | /// |
| 58 | /// Always returns `true` if the point is on the line. |
| 59 | pub fn check_side(&self, point: Point, side: LineSide) -> bool { |
| 60 | let distance = self.distance(point); |
| 61 | |
| 62 | match side { |
| 63 | LineSide::Left => distance <= 0, |
| 64 | LineSide::Right => distance >= 0, |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | /// Linear equation with zero distance to the origin. |
| 70 | #[derive (Copy, Clone, PartialEq, PartialOrd, Debug)] |
| 71 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
| 72 | pub struct OriginLinearEquation { |
| 73 | pub normal_vector: Point, |
| 74 | } |
| 75 | |
| 76 | impl OriginLinearEquation { |
| 77 | /// Creates a new linear equation with the given angle. |
| 78 | pub fn with_angle(angle: Angle) -> Self { |
| 79 | // FIXME: angle.tan() for 180.0 degrees isn't exactly 0 which causes problems when drawing |
| 80 | // a single quadrant. Is there a better solution to fix this? |
| 81 | let normal_vector = if angle == Angle::from_degrees(180.0) { |
| 82 | Point::new(0, -NORMAL_VECTOR_SCALE) |
| 83 | } else { |
| 84 | Point::new( |
| 85 | i32::from(angle.cos() * Real::from(NORMAL_VECTOR_SCALE)), |
| 86 | i32::from(angle.sin() * Real::from(NORMAL_VECTOR_SCALE)), |
| 87 | ) |
| 88 | .rotate_90() |
| 89 | }; |
| 90 | |
| 91 | Self { normal_vector } |
| 92 | } |
| 93 | |
| 94 | /// Creates a new horizontal linear equation. |
| 95 | pub const fn new_horizontal() -> Self { |
| 96 | Self { |
| 97 | normal_vector: Point::new(0, NORMAL_VECTOR_SCALE), |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | /// Returns the distance between the line and a point. |
| 102 | /// |
| 103 | /// The scaling of the returned value depends on the length of the normal vector. |
| 104 | /// Positive values will be returned for points on the right side of the line and negative |
| 105 | /// values for points on the left. |
| 106 | pub fn distance(&self, point: Point) -> i32 { |
| 107 | point.dot_product(self.normal_vector) |
| 108 | } |
| 109 | |
| 110 | /// Checks if a point is on the given side of the line. |
| 111 | /// |
| 112 | /// Always returns `true` if the point is on the line. |
| 113 | pub fn check_side(&self, point: Point, side: LineSide) -> bool { |
| 114 | let distance = self.distance(point); |
| 115 | |
| 116 | match side { |
| 117 | LineSide::Left => distance <= 0, |
| 118 | LineSide::Right => distance >= 0, |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | #[cfg (test)] |
| 124 | mod tests { |
| 125 | use super::*; |
| 126 | use crate::geometry::AngleUnit; |
| 127 | |
| 128 | #[test ] |
| 129 | fn from_line() { |
| 130 | assert_eq!( |
| 131 | LinearEquation::from_line(&Line::new(Point::zero(), Point::new(1, 0))), |
| 132 | LinearEquation { |
| 133 | normal_vector: Point::new(0, 1), |
| 134 | origin_distance: 0, // line goes through the origin |
| 135 | } |
| 136 | ); |
| 137 | |
| 138 | assert_eq!( |
| 139 | LinearEquation::from_line(&Line::new(Point::zero(), Point::new(0, 1))), |
| 140 | LinearEquation { |
| 141 | normal_vector: Point::new(-1, 0), |
| 142 | origin_distance: 0, // line goes through the origin |
| 143 | } |
| 144 | ); |
| 145 | |
| 146 | assert_eq!( |
| 147 | LinearEquation::from_line(&Line::new(Point::new(2, 3), Point::new(-2, 3))), |
| 148 | LinearEquation { |
| 149 | normal_vector: Point::new(0, -4), |
| 150 | // origin_distance = min. distance between line and origin * length of unit vector |
| 151 | // = 3 * 4 |
| 152 | origin_distance: -12, |
| 153 | } |
| 154 | ); |
| 155 | } |
| 156 | |
| 157 | #[test ] |
| 158 | fn with_angle() { |
| 159 | assert_eq!( |
| 160 | OriginLinearEquation::with_angle(0.0.deg()), |
| 161 | OriginLinearEquation { |
| 162 | normal_vector: Point::new(0, NORMAL_VECTOR_SCALE), |
| 163 | } |
| 164 | ); |
| 165 | |
| 166 | assert_eq!( |
| 167 | OriginLinearEquation::with_angle(90.0.deg()), |
| 168 | OriginLinearEquation { |
| 169 | normal_vector: Point::new(-NORMAL_VECTOR_SCALE, 0), |
| 170 | } |
| 171 | ); |
| 172 | } |
| 173 | |
| 174 | #[test ] |
| 175 | fn distance() { |
| 176 | let line = OriginLinearEquation::with_angle(90.0.deg()); |
| 177 | assert_eq!(line.distance(Point::new(-1, 0)), NORMAL_VECTOR_SCALE); |
| 178 | assert_eq!(line.distance(Point::new(1, 0)), -NORMAL_VECTOR_SCALE); |
| 179 | } |
| 180 | |
| 181 | #[test ] |
| 182 | fn check_side_horizontal_0deg() { |
| 183 | let eq1 = OriginLinearEquation::with_angle(0.0.deg()); |
| 184 | let eq2 = LinearEquation::from_line(&Line::with_delta(Point::zero(), Point::new(10, 0))); |
| 185 | |
| 186 | use LineSide::*; |
| 187 | for (point, side, expected) in [ |
| 188 | ((0, 0), Left, true), |
| 189 | ((1, 0), Right, true), |
| 190 | ((-2, 1), Left, false), |
| 191 | ((3, 1), Right, true), |
| 192 | ((-4, -1), Left, true), |
| 193 | ((5, -1), Right, false), |
| 194 | ] |
| 195 | .into_iter() |
| 196 | { |
| 197 | assert_eq!( |
| 198 | eq1.check_side(point.into(), side), |
| 199 | expected, |
| 200 | "{:?}, {:?}" , |
| 201 | point, |
| 202 | side |
| 203 | ); |
| 204 | |
| 205 | assert_eq!( |
| 206 | eq2.check_side(point.into(), side), |
| 207 | expected, |
| 208 | "{:?}, {:?}" , |
| 209 | point, |
| 210 | side |
| 211 | ); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | #[test ] |
| 216 | fn check_side_horizontal_180deg() { |
| 217 | let eq1 = OriginLinearEquation::with_angle(180.0.deg()); |
| 218 | let eq2 = LinearEquation::from_line(&Line::with_delta(Point::zero(), Point::new(-10, 0))); |
| 219 | |
| 220 | use LineSide::*; |
| 221 | for (point, side, expected) in [ |
| 222 | ((0, 0), Left, true), |
| 223 | ((1, 0), Right, true), |
| 224 | ((-2, 1), Left, true), |
| 225 | ((3, 1), Right, false), |
| 226 | ((-4, -1), Left, false), |
| 227 | ((5, -1), Right, true), |
| 228 | ] |
| 229 | .into_iter() |
| 230 | { |
| 231 | assert_eq!( |
| 232 | eq1.check_side(point.into(), side), |
| 233 | expected, |
| 234 | "{:?}, {:?}" , |
| 235 | point, |
| 236 | side |
| 237 | ); |
| 238 | |
| 239 | assert_eq!( |
| 240 | eq2.check_side(point.into(), side), |
| 241 | expected, |
| 242 | "{:?}, {:?}" , |
| 243 | point, |
| 244 | side |
| 245 | ); |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | #[test ] |
| 250 | fn check_side_vertical_90deg() { |
| 251 | let eq1 = OriginLinearEquation::with_angle(90.0.deg()); |
| 252 | let eq2 = LinearEquation::from_line(&Line::with_delta(Point::zero(), Point::new(0, 10))); |
| 253 | |
| 254 | use LineSide::*; |
| 255 | for (point, side, expected) in [ |
| 256 | ((0, 0), Left, true), |
| 257 | ((0, 1), Right, true), |
| 258 | ((-1, -2), Left, false), |
| 259 | ((-1, 3), Right, true), |
| 260 | ((1, -4), Left, true), |
| 261 | ((1, 5), Right, false), |
| 262 | ] |
| 263 | .into_iter() |
| 264 | { |
| 265 | assert_eq!( |
| 266 | eq1.check_side(point.into(), side), |
| 267 | expected, |
| 268 | "{:?}, {:?}" , |
| 269 | point, |
| 270 | side |
| 271 | ); |
| 272 | |
| 273 | assert_eq!( |
| 274 | eq2.check_side(point.into(), side), |
| 275 | expected, |
| 276 | "{:?}, {:?}" , |
| 277 | point, |
| 278 | side |
| 279 | ); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | #[test ] |
| 284 | fn check_side_vertical_270deg() { |
| 285 | let eq1 = OriginLinearEquation::with_angle(270.0.deg()); |
| 286 | let eq2 = LinearEquation::from_line(&Line::with_delta(Point::zero(), Point::new(0, -10))); |
| 287 | |
| 288 | use LineSide::*; |
| 289 | for (point, side, expected) in [ |
| 290 | ((0, 0), Left, true), |
| 291 | ((0, -1), Right, true), |
| 292 | ((-1, 2), Left, true), |
| 293 | ((-1, -3), Right, false), |
| 294 | ((1, 4), Left, false), |
| 295 | ((1, -5), Right, true), |
| 296 | ] |
| 297 | .into_iter() |
| 298 | { |
| 299 | assert_eq!( |
| 300 | eq1.check_side(point.into(), side), |
| 301 | expected, |
| 302 | "{:?}, {:?}" , |
| 303 | point, |
| 304 | side |
| 305 | ); |
| 306 | |
| 307 | assert_eq!( |
| 308 | eq2.check_side(point.into(), side), |
| 309 | expected, |
| 310 | "{:?}, {:?}" , |
| 311 | point, |
| 312 | side |
| 313 | ); |
| 314 | } |
| 315 | } |
| 316 | } |
| 317 | |