Files
ink-notes/src/geometry.rs
T

1738 lines
53 KiB
Rust

//! Geometrie- und Objekterkennung (Shape Recognition) für Freihandstriche.
//!
//! Erkennt Geraden, Rechtecke, Kreise und Koordinatensysteme (Achsen)
//! und erzeugt saubere Vektor-Primitiven und GPU-Triangle-Meshes.
use glam::Vec2;
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
use crate::canvas::InkVertex;
use crate::document::{Aabb, Stroke, StylusSample};
/// Arten der geometrischen Primitiven.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShapeKind {
Line,
Circle,
Rectangle,
CoordinateSystem,
Triangle,
Rhombus,
Polygon,
Octahedron,
}
/// Erkannte, mathematisch bereinigte Form.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RecognizedShape {
Line {
start: Vec2,
end: Vec2,
},
Circle {
center: Vec2,
radius: f32,
},
Rectangle {
/// 4 Eckpunkte in umlaufender Reihenfolge (strikt rechtwinklig).
corners: [Vec2; 4],
},
CoordinateSystem {
origin: Vec2,
x_axis_end: Vec2,
y_axis_end: Vec2,
with_arrows: bool,
},
Triangle {
/// 3 Eckpunkte in umlaufender Reihenfolge.
corners: [Vec2; 3],
},
Rhombus {
/// 4 Eckpunkte in umlaufender Reihenfolge (Raute/Karo mit gleich langen Kanten).
corners: [Vec2; 4],
},
Polygon {
/// N Eckpunkte in umlaufender Reihenfolge (z. B. 5-Eck, 6-Eck, 8-Eck / Oktagon).
vertices: Vec<Vec2>,
},
Octahedron {
/// 3D-Kantenmodell (Doppelpyramide) mit 6 Eckpunkten und 12 Kanten im Schrägbild.
center: Vec2,
radius_x: f32,
radius_y: f32,
depth: f32,
},
}
impl RecognizedShape {
pub fn kind(&self) -> ShapeKind {
match self {
Self::Line { .. } => ShapeKind::Line,
Self::Circle { .. } => ShapeKind::Circle,
Self::Rectangle { .. } => ShapeKind::Rectangle,
Self::CoordinateSystem { .. } => ShapeKind::CoordinateSystem,
Self::Triangle { .. } => ShapeKind::Triangle,
Self::Rhombus { .. } => ShapeKind::Rhombus,
Self::Polygon { .. } => ShapeKind::Polygon,
Self::Octahedron { .. } => ShapeKind::Octahedron,
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Line { .. } => "Gerade",
Self::Circle { .. } => "Kreis",
Self::Rectangle { .. } => "Rechteck",
Self::CoordinateSystem { .. } => "Koordinatensystem",
Self::Triangle { .. } => "Dreieck",
Self::Rhombus { .. } => "Raute (Rhombus)",
Self::Polygon { vertices } => match vertices.len() {
5 => "Fünfeck (Pentagon)",
6 => "Sechseck (Hexagon)",
8 => "Achteck (Oktagon)",
_ => "Polygon",
},
Self::Octahedron { .. } => "Oktaeder (3D-Kantenmodell)",
}
}
/// Verschiebt die geometrische Form um `delta`.
pub fn translate(&mut self, delta: Vec2) {
match self {
Self::Line { start, end } => {
*start += delta;
*end += delta;
}
Self::Circle { center, .. } => {
*center += delta;
}
Self::Rectangle { corners } | Self::Rhombus { corners } => {
for c in corners {
*c += delta;
}
}
Self::Triangle { corners } => {
for c in corners {
*c += delta;
}
}
Self::CoordinateSystem {
origin,
x_axis_end,
y_axis_end,
..
} => {
*origin += delta;
*x_axis_end += delta;
*y_axis_end += delta;
}
Self::Polygon { vertices } => {
for v in vertices {
*v += delta;
}
}
Self::Octahedron { center, .. } => {
*center += delta;
}
}
}
/// Dreht die geometrische Form um `origin` mit dem Winkel `angle_rad`.
pub fn rotate(&mut self, origin: Vec2, angle_rad: f32) {
let rot = |pt: Vec2| -> Vec2 {
let diff = pt - origin;
let (sin, cos) = angle_rad.sin_cos();
origin + Vec2::new(diff.x * cos - diff.y * sin, diff.x * sin + diff.y * cos)
};
match self {
Self::Line { start, end } => {
*start = rot(*start);
*end = rot(*end);
}
Self::Circle { center, .. } => {
*center = rot(*center);
}
Self::Rectangle { corners } | Self::Rhombus { corners } => {
for c in corners {
*c = rot(*c);
}
}
Self::Triangle { corners } => {
for c in corners {
*c = rot(*c);
}
}
Self::CoordinateSystem {
origin: o,
x_axis_end,
y_axis_end,
..
} => {
*o = rot(*o);
*x_axis_end = rot(*x_axis_end);
*y_axis_end = rot(*y_axis_end);
}
Self::Polygon { vertices } => {
for v in vertices {
*v = rot(*v);
}
}
Self::Octahedron { center, .. } => {
*center = rot(*center);
}
}
}
/// Skaliert die geometrische Form bezüglich `origin` um den Faktor `factors`.
pub fn scale(&mut self, origin: Vec2, factors: Vec2) {
let sc = |pt: Vec2| -> Vec2 { origin + (pt - origin) * factors };
match self {
Self::Line { start, end } => {
*start = sc(*start);
*end = sc(*end);
}
Self::Circle { center, radius } => {
*center = sc(*center);
*radius *= (factors.x.abs() + factors.y.abs()) * 0.5;
}
Self::Rectangle { corners } | Self::Rhombus { corners } => {
for c in corners {
*c = sc(*c);
}
}
Self::Triangle { corners } => {
for c in corners {
*c = sc(*c);
}
}
Self::CoordinateSystem {
origin: o,
x_axis_end,
y_axis_end,
..
} => {
*o = sc(*o);
*x_axis_end = sc(*x_axis_end);
*y_axis_end = sc(*y_axis_end);
}
Self::Polygon { vertices } => {
for v in vertices {
*v = sc(*v);
}
}
Self::Octahedron {
center,
radius_x,
radius_y,
depth,
} => {
*center = sc(*center);
*radius_x *= factors.x.abs();
*radius_y *= factors.y.abs();
*depth *= (factors.x.abs() + factors.y.abs()) * 0.5;
}
}
}
}
/// Vereinfacht einen Polygonzug mit dem Ramer-Douglas-Peucker (RDP) Algorithmus.
pub fn ramer_douglas_peucker(points: &[Vec2], epsilon: f32) -> Vec<Vec2> {
if points.len() <= 2 {
return points.to_vec();
}
rdp_recursive(points, epsilon)
}
fn rdp_recursive(points: &[Vec2], epsilon: f32) -> Vec<Vec2> {
if points.len() <= 2 {
return points.to_vec();
}
let first = points[0];
let last = *points.last().unwrap();
let mut max_dist = 0.0f32;
let mut max_idx = 0;
for (i, &pt) in points.iter().enumerate().skip(1).take(points.len() - 2) {
let dist = point_to_line_dist(pt, first, last);
if dist > max_dist {
max_dist = dist;
max_idx = i;
}
}
if max_dist > epsilon {
let mut left = rdp_recursive(&points[..=max_idx], epsilon);
let right = rdp_recursive(&points[max_idx..], epsilon);
left.pop();
left.extend(right);
left
} else {
vec![first, last]
}
}
/// Abstand eines Punktes `p` zur Geraden durch `a` und `b`.
#[inline]
pub fn point_to_line_dist(p: Vec2, a: Vec2, b: Vec2) -> f32 {
let ab = b - a;
let len = ab.length();
if len < 1e-6 {
return (p - a).length();
}
let cross = (p.x - a.x) * ab.y - (p.y - a.y) * ab.x;
cross.abs() / len
}
/// Berechnet die aufsummierte Pfadlänge einer Punktfolge.
pub fn path_length(points: &[Vec2]) -> f32 {
points.windows(2).map(|w| (w[1] - w[0]).length()).sum()
}
/// 1. Geradenerkennung: Verhältnis Sehnenlänge zu Pfadlänge > 0.92.
pub fn detect_line(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 2 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
let chord_len = (end - start).length();
let total_len = path_length(points);
if total_len < 10.0 {
return None;
}
let ratio = chord_len / total_len;
if ratio > 0.92 {
// Bei Winkeln nahe an horizontal/vertikal (innerhalb 5°) einrasten
let dir = end - start;
let angle_deg = dir.y.atan2(dir.x).to_degrees().abs();
let snapped_end = if angle_deg < 5.0 || (angle_deg - 180.0).abs() < 5.0 {
Vec2::new(end.x, start.y)
} else if (angle_deg - 90.0).abs() < 5.0 {
Vec2::new(start.x, end.y)
} else {
end
};
Some(RecognizedShape::Line {
start,
end: snapped_end,
})
} else {
None
}
}
/// 2. Kreis- & Ellipsenerkennung: Geschlossener Pfad und geringe Standardabweichung der Radien.
pub fn detect_circle(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 8 {
return None;
}
let total_len = path_length(points);
if total_len < 30.0 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
let chord_len = (end - start).length();
// Pfad muss annähernd geschlossen sein
if chord_len > 0.35 * total_len {
return None;
}
// Schwerpunkt als Startpunkt
let n = points.len() as f32;
let mean_x = points.iter().map(|p| p.x).sum::<f32>() / n;
let mean_y = points.iter().map(|p| p.y).sum::<f32>() / n;
// Algebraic Least-Squares Circle Fit (Kåsa-Verfahren)
let mut suu = 0.0;
let mut suv = 0.0;
let mut svv = 0.0;
let mut suuu = 0.0;
let mut svvv = 0.0;
let mut suvv = 0.0;
let mut svuu = 0.0;
for p in points {
let u = p.x - mean_x;
let v = p.y - mean_y;
let u2 = u * u;
let v2 = v * v;
suu += u2;
suv += u * v;
svv += v2;
suuu += u2 * u;
svvv += v2 * v;
suvv += u * v2;
svuu += v * u2;
}
let det = suu * svv - suv * suv;
let center = if det.abs() > 1e-5 {
let rhs_u = 0.5 * (suuu + suvv);
let rhs_v = 0.5 * (svvv + svuu);
let uc = (rhs_u * svv - rhs_v * suv) / det;
let vc = (suu * rhs_v - suv * rhs_u) / det;
Vec2::new(mean_x + uc, mean_y + vc)
} else {
Vec2::new(mean_x, mean_y)
};
// Radien vom berechneten Mittelpunkt ermitteln
let radii: Vec<f32> = points.iter().map(|p| (*p - center).length()).collect();
let mean_r = radii.iter().sum::<f32>() / n;
if mean_r < 10.0 {
return None;
}
let variance = radii.iter().map(|r| (r - mean_r).powi(2)).sum::<f32>() / n;
let std_dev = variance.sqrt();
let cv = std_dev / mean_r; // Variationskoeffizient
// Geringe Standardabweichung der Radien (für Freihandkreise/Ovale < 0.22)
if cv < 0.22 {
Some(RecognizedShape::Circle {
center,
radius: mean_r,
})
} else {
None
}
}
/// 3. Rechteckerkennung: Geschlossener Pfad mit 4 dominanten Ecken (~90° +/- 18°).
pub fn detect_rectangle(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 8 {
return None;
}
let total_len = path_length(points);
if total_len < 40.0 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
let chord_len = (end - start).length();
// Pfad muss annähernd geschlossen sein
if chord_len > 0.35 * total_len {
return None;
}
// RDP mit ca. 5% Pfadlänge als Toleranz
let epsilon = (total_len * 0.05).clamp(8.0, 45.0);
let simplified = ramer_douglas_peucker(points, epsilon);
let corners: Vec<Vec2> =
if simplified.len() == 5 && (simplified[0] - simplified[4]).length() < epsilon * 2.5 {
simplified[0..4].to_vec()
} else if simplified.len() == 4 {
simplified
} else if simplified.len() > 4 {
// Suche die 4 markantesten Ecken (stärkste Richtungsänderung)
extract_k_corners(&simplified, 4)?
} else {
return None;
};
if corners.len() != 4 {
return None;
}
// Innenwinkel prüfen: 4 Kanten müssen jeweils ~90° (+/- 18°) zueinander stehen
let c = [corners[0], corners[1], corners[2], corners[3]];
for i in 0..4 {
let prev = c[(i + 3) % 4];
let curr = c[i];
let next = c[(i + 1) % 4];
let v1 = (prev - curr).normalize_or_zero();
let v2 = (next - curr).normalize_or_zero();
let dot = v1.dot(v2).clamp(-1.0, 1.0);
let angle_deg = dot.acos().to_degrees();
if !(72.0..=108.0).contains(&angle_deg) {
return None;
}
}
// Saubere rechtwinklige Ausrichtung
let aabb = Aabb::from_points(&c);
let center = (c[0] + c[1] + c[2] + c[3]) * 0.25;
let e0 = c[1] - c[0];
let angle_deg = e0.y.atan2(e0.x).to_degrees().abs();
// Nahe an Achsenparallelität (< 12°)?
let snapped_corners = if angle_deg < 12.0
|| (angle_deg - 90.0).abs() < 12.0
|| (angle_deg - 180.0).abs() < 12.0
{
[
Vec2::new(aabb.min.x, aabb.min.y),
Vec2::new(aabb.max.x, aabb.min.y),
Vec2::new(aabb.max.x, aabb.max.y),
Vec2::new(aabb.min.x, aabb.max.y),
]
} else {
// Rotiertes Rechteck mit exakt 90° Kanten
let dir = e0.normalize_or_zero();
let norm = Vec2::new(-dir.y, dir.x);
let w = 0.5 * ((c[1] - c[0]).length() + (c[2] - c[3]).length());
let h = 0.5 * ((c[2] - c[1]).length() + (c[3] - c[0]).length());
let hw = w * 0.5;
let hh = h * 0.5;
[
center - dir * hw - norm * hh,
center + dir * hw - norm * hh,
center + dir * hw + norm * hh,
center - dir * hw + norm * hh,
]
};
Some(RecognizedShape::Rectangle {
corners: snapped_corners,
})
}
/// Rastet einen Winkel in Radiant an horizontalen, vertikalen oder diagonalen Achsen ein.
pub fn snap_angle(angle_rad: f32, tolerance_deg: f32) -> f32 {
let tol = tolerance_deg.to_radians();
let cardinal = [
0.0,
PI * 0.5,
PI,
-PI,
-PI * 0.5,
PI * 0.25,
PI * 0.75,
-PI * 0.25,
-PI * 0.75,
];
for &target in &cardinal {
let diff = (angle_rad - target).abs();
let diff_wrapped = diff
.min((angle_rad - (target + 2.0 * PI)).abs())
.min((angle_rad - (target - 2.0 * PI)).abs());
if diff_wrapped < tol {
return target;
}
}
angle_rad
}
/// Extrahiert die k markantesten Ecken (stärkste Richtungsänderung) aus einem vereinfachten Polygonzug.
pub fn extract_k_corners(points: &[Vec2], k: usize) -> Option<Vec<Vec2>> {
let n = points.len();
if n < k {
return None;
}
let mut scored: Vec<(usize, f32)> = Vec::with_capacity(n);
for i in 0..n {
let prev = points[(i + n - 1) % n];
let curr = points[i];
let next = points[(i + 1) % n];
let v1 = (prev - curr).normalize_or_zero();
let v2 = (next - curr).normalize_or_zero();
// Dot product: je kleiner, desto spitzer/schärfer die Richtungsänderung
let dot = v1.dot(v2).clamp(-1.0, 1.0);
scored.push((i, dot));
}
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut selected_indices: Vec<usize> = scored.iter().take(k).map(|&(idx, _)| idx).collect();
selected_indices.sort_unstable();
Some(
selected_indices
.into_iter()
.map(|idx| points[idx])
.collect(),
)
}
/// 4. Dreieck-Erkennung: Geschlossener Pfad mit 3 dominanten Ecken.
pub fn detect_triangle(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 4 {
return None;
}
let total_len = path_length(points);
if total_len < 35.0 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
let chord_len = (end - start).length();
// Pfad muss annähernd geschlossen sein
if chord_len > 0.40 * total_len {
return None;
}
let epsilon = (total_len * 0.07).clamp(8.0, 50.0);
let simplified = ramer_douglas_peucker(points, epsilon);
let corners: Vec<Vec2> = if (simplified.len() == 4
&& (simplified[0] - simplified[3]).length() < epsilon * 2.5)
|| simplified.len() == 3
{
simplified[0..3].to_vec()
} else if simplified.len() > 4 {
extract_k_corners(&simplified, 3)?
} else {
return None;
};
if corners.len() != 3 {
return None;
}
let a = corners[0];
let mut b = corners[1];
let mut c = corners[2];
let l_ab = (b - a).length();
let l_bc = (c - b).length();
let l_ca = (a - c).length();
if l_ab < 15.0 || l_bc < 15.0 || l_ca < 15.0 {
return None;
}
// Kreuzprodukt für Fläche
let cross = ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs();
if cross < 80.0 {
return None;
}
let angle_at = |prev: Vec2, curr: Vec2, next: Vec2| -> f32 {
let v1 = (prev - curr).normalize_or_zero();
let v2 = (next - curr).normalize_or_zero();
v1.dot(v2).clamp(-1.0, 1.0).acos().to_degrees()
};
let ang_a = angle_at(c, a, b);
let ang_b = angle_at(a, b, c);
let ang_c = angle_at(b, c, a);
if ang_a < 18.0 || ang_b < 18.0 || ang_c < 18.0 {
return None;
}
// Geometrisches Einrasten (Snapping):
// 1. Gleichseitig? (Alle Winkel nahe 60°)
if (ang_a - 60.0).abs() < 14.0 && (ang_b - 60.0).abs() < 14.0 && (ang_c - 60.0).abs() < 14.0 {
let center = (a + b + c) / 3.0;
let r = ((a - center).length() + (b - center).length() + (c - center).length()) / 3.0;
let base_dir = (a - center).normalize_or_zero();
let base_angle = base_dir.y.atan2(base_dir.x);
let snapped_angle = snap_angle(base_angle, 15.0);
let c0 = center + Vec2::new(snapped_angle.cos(), snapped_angle.sin()) * r;
let c1 = center
+ Vec2::new(
(snapped_angle + 2.0 * PI / 3.0).cos(),
(snapped_angle + 2.0 * PI / 3.0).sin(),
) * r;
let c2 = center
+ Vec2::new(
(snapped_angle + 4.0 * PI / 3.0).cos(),
(snapped_angle + 4.0 * PI / 3.0).sin(),
) * r;
return Some(RecognizedShape::Triangle {
corners: [c0, c1, c2],
});
}
// 2. Rechtwinklig? (Ein Winkel nahe 90°)
let angles = [(0, ang_a), (1, ang_b), (2, ang_c)];
if let Some(&(idx, _)) = angles.iter().find(|(_, ang)| (*ang - 90.0).abs() < 14.0) {
let (corner_v, p1, p2) = match idx {
0 => (a, b, c),
1 => (b, c, a),
_ => (c, a, b),
};
let mut v1 = p1 - corner_v;
let mut v2 = p2 - corner_v;
let a1 = v1.y.atan2(v1.x);
let a1_snapped = snap_angle(a1, 15.0);
let len1 = v1.length();
let len2 = v2.length();
v1 = Vec2::new(a1_snapped.cos(), a1_snapped.sin()) * len1;
let sign = if (v1.x * v2.y - v1.y * v2.x) >= 0.0 {
1.0
} else {
-1.0
};
v2 = Vec2::new(-v1.y * sign, v1.x * sign).normalize_or_zero() * len2;
let new_p1 = corner_v + v1;
let new_p2 = corner_v + v2;
let snapped = match idx {
0 => [corner_v, new_p1, new_p2],
1 => [new_p2, corner_v, new_p1],
_ => [new_p1, new_p2, corner_v],
};
return Some(RecognizedShape::Triangle { corners: snapped });
}
// 3. Gleichschenklig?
if (l_ab - l_ca).abs() / l_ab.max(l_ca) < 0.12 {
let mid_bc = (b + c) * 0.5;
let axis = (a - mid_bc).normalize_or_zero();
let perp = Vec2::new(-axis.y, axis.x);
let half_base = ((b - mid_bc).length() + (c - mid_bc).length()) * 0.5;
b = mid_bc + perp * half_base;
c = mid_bc - perp * half_base;
} else if (l_ab - l_bc).abs() / l_ab.max(l_bc) < 0.12 {
let mid_ac = (a + c) * 0.5;
let axis = (b - mid_ac).normalize_or_zero();
let perp = Vec2::new(-axis.y, axis.x);
let half_base = ((a - mid_ac).length() + (c - mid_ac).length()) * 0.5;
let new_a = mid_ac + perp * half_base;
c = mid_ac - perp * half_base;
return Some(RecognizedShape::Triangle {
corners: [new_a, b, c],
});
} else if (l_bc - l_ca).abs() / l_bc.max(l_ca) < 0.12 {
let mid_ab = (a + b) * 0.5;
let axis = (c - mid_ab).normalize_or_zero();
let perp = Vec2::new(-axis.y, axis.x);
let half_base = ((a - mid_ab).length() + (b - mid_ab).length()) * 0.5;
let new_a = mid_ab + perp * half_base;
b = mid_ab - perp * half_base;
return Some(RecognizedShape::Triangle {
corners: [new_a, b, c],
});
}
Some(RecognizedShape::Triangle { corners: [a, b, c] })
}
/// 5. Rauten-Erkennung (Rhombus / Karo): Geschlossener Pfad mit 4 gleich langen Kanten und orthogonalen Diagonalen.
pub fn detect_rhombus(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 4 {
return None;
}
let total_len = path_length(points);
if total_len < 40.0 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
if (end - start).length() > 0.35 * total_len {
return None;
}
let epsilon = (total_len * 0.05).clamp(8.0, 45.0);
let simplified = ramer_douglas_peucker(points, epsilon);
let corners: Vec<Vec2> =
if simplified.len() == 5 && (simplified[0] - simplified[4]).length() < epsilon * 2.5 {
simplified[0..4].to_vec()
} else if simplified.len() == 4 {
simplified
} else if simplified.len() > 4 {
extract_k_corners(&simplified, 4)?
} else {
return None;
};
if corners.len() != 4 {
return None;
}
let c = [corners[0], corners[1], corners[2], corners[3]];
let l0 = (c[1] - c[0]).length();
let l1 = (c[2] - c[1]).length();
let l2 = (c[3] - c[2]).length();
let l3 = (c[0] - c[3]).length();
let mean_l = (l0 + l1 + l2 + l3) * 0.25;
if mean_l < 15.0 {
return None;
}
let max_dev = ((l0 - mean_l).abs())
.max((l1 - mean_l).abs())
.max((l2 - mean_l).abs())
.max((l3 - mean_l).abs());
if max_dev / mean_l > 0.28 {
return None;
}
let center = (c[0] + c[1] + c[2] + c[3]) * 0.25;
let d1 = c[2] - c[0];
let d2 = c[3] - c[1];
let len1 = d1.length();
let len2 = d2.length();
if len1 < 10.0 || len2 < 10.0 {
return None;
}
let dot = (d1.normalize_or_zero().dot(d2.normalize_or_zero())).clamp(-1.0, 1.0);
let diag_angle_deg = dot.acos().to_degrees();
if !(65.0..=115.0).contains(&diag_angle_deg) {
return None;
}
let mut dir1 = d1.normalize_or_zero();
let angle1 = dir1.y.atan2(dir1.x);
let snapped_a1 = snap_angle(angle1, 15.0);
dir1 = Vec2::new(snapped_a1.cos(), snapped_a1.sin());
let sign = if (dir1.x * d2.y - dir1.y * d2.x) >= 0.0 {
1.0
} else {
-1.0
};
let dir2 = Vec2::new(-dir1.y * sign, dir1.x * sign);
let h1 = len1 * 0.5;
let h2 = len2 * 0.5;
let c0 = center - dir1 * h1;
let c1 = center - dir2 * h2;
let c2 = center + dir1 * h1;
let c3 = center + dir2 * h2;
Some(RecognizedShape::Rhombus {
corners: [c0, c1, c2, c3],
})
}
/// 6. Polygon-Erkennung (z. B. 5-Eck, 6-Eck, 8-Eck / Oktagon).
pub fn detect_polygon(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 6 {
return None;
}
let total_len = path_length(points);
if total_len < 50.0 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
if (end - start).length() > 0.35 * total_len {
return None;
}
let epsilon = (total_len * 0.04).clamp(6.0, 30.0);
let simplified = ramer_douucker_closed(points, epsilon);
let n = simplified.len();
if n != 5 && n != 6 && n != 8 {
return None;
}
let corners = simplified;
let center = corners.iter().copied().sum::<Vec2>() / (n as f32);
let radii: Vec<f32> = corners.iter().map(|&pt| (pt - center).length()).collect();
let mean_r = radii.iter().sum::<f32>() / (n as f32);
if mean_r < 15.0 {
return None;
}
let max_r_diff = radii
.iter()
.map(|r| (r - mean_r).abs())
.fold(0.0f32, f32::max);
let snapped_vertices = if max_r_diff / mean_r < 0.25 {
let base_dir = (corners[0] - center).normalize_or_zero();
let base_angle = snap_angle(base_dir.y.atan2(base_dir.x), 15.0);
let step = 2.0 * PI / (n as f32);
(0..n)
.map(|i| {
let ang = base_angle + i as f32 * step;
center + Vec2::new(ang.cos(), ang.sin()) * mean_r
})
.collect()
} else {
corners
};
Some(RecognizedShape::Polygon {
vertices: snapped_vertices,
})
}
fn ramer_douucker_closed(points: &[Vec2], epsilon: f32) -> Vec<Vec2> {
let simplified = ramer_douglas_peucker(points, epsilon);
if simplified.len() >= 2
&& (simplified[0] - *simplified.last().unwrap()).length() < epsilon * 2.0
{
simplified[0..simplified.len() - 1].to_vec()
} else {
simplified
}
}
/// 7. Oktaeder-Erkennung (3D-Kantenmodell / Doppelpyramide).
pub fn detect_octahedron(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 8 {
return None;
}
let total_len = path_length(points);
if total_len < 70.0 {
return None;
}
let aabb = Aabb::from_points(points);
let w = (aabb.max.x - aabb.min.x) * 0.5;
let h = (aabb.max.y - aabb.min.y) * 0.5;
if w < 20.0 || h < 20.0 {
return None;
}
let aspect = w / h;
if !(0.3..=3.0).contains(&aspect) {
return None;
}
let center = (aabb.min + aabb.max) * 0.5;
let mut has_top = false;
let mut has_bottom = false;
let mut has_left = false;
let mut has_right = false;
let mut has_center_equator = false;
for &pt in points {
if pt.y <= aabb.min.y + 0.18 * h && (pt.x - center.x).abs() < 0.40 * w {
has_top = true;
}
if pt.y >= aabb.max.y - 0.18 * h && (pt.x - center.x).abs() < 0.40 * w {
has_bottom = true;
}
if pt.x <= aabb.min.x + 0.18 * w && (pt.y - center.y).abs() < 0.40 * h {
has_left = true;
}
if pt.x >= aabb.max.x - 0.18 * w && (pt.y - center.y).abs() < 0.40 * h {
has_right = true;
}
if (pt.x - center.x).abs() < 0.45 * w && (pt.y - center.y).abs() < 0.22 * h {
has_center_equator = true;
}
}
if has_top && has_bottom && has_left && has_right && has_center_equator {
let depth = (w * 0.35).clamp(10.0, 50.0);
return Some(RecognizedShape::Octahedron {
center,
radius_x: w,
radius_y: h,
depth,
});
}
None
}
/// 4. Koordinatensystem-Erkennung: Zwei aufeinander stehende Linien (80° bis 100°), gesnappt auf exakt 90°.
pub fn detect_coordinate_system(points: &[Vec2]) -> Option<RecognizedShape> {
if points.len() < 5 {
return None;
}
let total_len = path_length(points);
if total_len < 30.0 {
return None;
}
let start = points[0];
let end = *points.last().unwrap();
let chord_len = (end - start).length();
// Darf NICHT geschlossen sein
if chord_len < 0.3 * total_len {
return None;
}
// RDP zur Findung des Eckpunktes (Ursprungs)
let epsilon = (total_len * 0.08).clamp(12.0, 50.0);
let simplified = ramer_douglas_peucker(points, epsilon);
if simplified.len() != 3 {
return None;
}
let p0 = simplified[0];
let origin = simplified[1];
let p2 = simplified[2];
let v0 = p0 - origin;
let v2 = p2 - origin;
let len0 = v0.length();
let len2 = v2.length();
if len0 < 15.0 || len2 < 15.0 {
return None;
}
let dot = (v0.normalize().dot(v2.normalize())).clamp(-1.0, 1.0);
let angle_deg = dot.acos().to_degrees();
// Orthogonalitäts-Bedingung: 80° bis 100°
if !(80.0..=100.0).contains(&angle_deg) {
return None;
}
// Orthogonalitäts-Snap: Auf exakt 90° ausrichten
// Bestimme, welche Achse näher an der Vertikalen (Y) und welche an der Horizontalen (X) liegt
let (x_end, y_end) = if v0.x.abs() > v0.y.abs() {
// v0 ist eher horizontal, v2 eher vertikal
let sign_x = if v0.x >= 0.0 { 1.0 } else { -1.0 };
let sign_y = if v2.y >= 0.0 { 1.0 } else { -1.0 };
(
origin + Vec2::new(sign_x * len0, 0.0),
origin + Vec2::new(0.0, sign_y * len2),
)
} else {
// v2 ist eher horizontal, v0 eher vertikal
let sign_y = if v0.y >= 0.0 { 1.0 } else { -1.0 };
let sign_x = if v2.x >= 0.0 { 1.0 } else { -1.0 };
(
origin + Vec2::new(sign_x * len2, 0.0),
origin + Vec2::new(0.0, sign_y * len0),
)
};
Some(RecognizedShape::CoordinateSystem {
origin,
x_axis_end: x_end,
y_axis_end: y_end,
with_arrows: true,
})
}
/// Führt die automatische Erkennung für einen Strich aus.
pub fn detect_shape(points: &[StylusSample]) -> Option<RecognizedShape> {
if points.len() < 2 {
return None;
}
let pts: Vec<Vec2> = points.iter().map(|s| s.position).collect();
// 1. Zuerst 3D-Kantenmodelle prüfen (Oktaeder / Doppelpyramide)
if let Some(octa) = detect_octahedron(&pts) {
return Some(octa);
}
// 2. Geschlossene Formen prüfen (Kreis, Dreieck, Rechteck, Raute, Polygon/Oktagon)
if let Some(circle) = detect_circle(&pts) {
return Some(circle);
}
if let Some(triangle) = detect_triangle(&pts) {
return Some(triangle);
}
if let Some(rect) = detect_rectangle(&pts) {
return Some(rect);
}
if let Some(rhombus) = detect_rhombus(&pts) {
return Some(rhombus);
}
if let Some(polygon) = detect_polygon(&pts) {
return Some(polygon);
}
// 3. Offene Formen: Koordinatensystem oder Gerade
if let Some(coord) = detect_coordinate_system(&pts) {
return Some(coord);
}
if let Some(line) = detect_line(&pts) {
return Some(line);
}
None
}
/// Erzeugt ein gleichmäßiges Triangle-Strip-Mesh für ein geschlossenes Polygon (Dreieck, Rechteck, Raute, N-Eck).
pub fn closed_polygon_to_mesh(
corners: &[Vec2],
half_w: f32,
color: [f32; 4],
) -> (Vec<StylusSample>, Vec<InkVertex>) {
let n = corners.len();
if n < 3 {
return (Vec::new(), Vec::new());
}
let mut vertices = Vec::with_capacity((n + 1) * 2);
let mut samples = Vec::with_capacity(n * 8);
for i in 0..n {
let prev = corners[(i + n - 1) % n];
let curr = corners[i];
let next = corners[(i + 1) % n];
let d_prev = (curr - prev).normalize_or_zero();
let d_next = (next - curr).normalize_or_zero();
let n_prev = Vec2::new(-d_prev.y, d_prev.x);
let n_next = Vec2::new(-d_next.y, d_next.x);
let corner_norm = (n_prev + n_next).normalize_or_zero() * half_w;
vertices.push(InkVertex::new(
curr.x + corner_norm.x,
curr.y + corner_norm.y,
color,
));
vertices.push(InkVertex::new(
curr.x - corner_norm.x,
curr.y - corner_norm.y,
color,
));
let edge_len = (next - curr).length();
let steps = ((edge_len / 10.0).ceil() as usize).max(2);
for s in 0..steps {
let t = s as f32 / steps as f32;
samples.push(StylusSample {
position: curr.lerp(next, t),
pressure: 0.5,
timestamp: 0.0,
});
}
}
if vertices.len() >= 2 {
let v0 = vertices[0];
let v1 = vertices[1];
vertices.push(v0);
vertices.push(v1);
}
(samples, vertices)
}
/// Berechnet die 12 Kanten der 3D-Schrägbild-Projektion eines Oktaeders (Doppelpyramide).
pub fn octahedron_edges(
center: Vec2,
radius_x: f32,
radius_y: f32,
depth: f32,
) -> Vec<(Vec2, Vec2)> {
let top = center + Vec2::new(0.0, -radius_y);
let bottom = center + Vec2::new(0.0, radius_y);
let left = center + Vec2::new(-radius_x, 0.0);
let right = center + Vec2::new(radius_x, 0.0);
let front = center + Vec2::new(-0.2 * radius_x, depth);
let back = center + Vec2::new(0.2 * radius_x, -depth);
vec![
// Äquator-Ring (4 Kanten)
(left, front),
(front, right),
(right, back),
(back, left),
// Obere Pyramide (4 Kanten)
(top, left),
(top, front),
(top, right),
(top, back),
// Untere Pyramide (4 Kanten)
(bottom, left),
(bottom, front),
(bottom, right),
(bottom, back),
]
}
/// Erzeugt dichte Abtastpunkte und ein GPU-kompatibles Triangle-Strip-Mesh für eine erkannte Form.
pub fn shape_to_mesh(
shape: &RecognizedShape,
width_units: f32,
color: [f32; 4],
) -> (Vec<StylusSample>, Vec<InkVertex>) {
let half_w = (width_units * 0.5).max(1.0);
match shape {
RecognizedShape::Line { start, end } => {
let dir = (*end - *start).normalize_or_zero();
let norm = Vec2::new(-dir.y, dir.x) * half_w;
let vertices = vec![
InkVertex::new(start.x + norm.x, start.y + norm.y, color),
InkVertex::new(start.x - norm.x, start.y - norm.y, color),
InkVertex::new(end.x + norm.x, end.y + norm.y, color),
InkVertex::new(end.x - norm.x, end.y - norm.y, color),
];
let len = (*end - *start).length();
let steps = ((len / 5.0).ceil() as usize).max(2);
let mut samples = Vec::with_capacity(steps);
for i in 0..steps {
let t = i as f32 / (steps - 1) as f32;
samples.push(StylusSample {
position: start.lerp(*end, t),
pressure: 0.5,
timestamp: 0.0,
});
}
(samples, vertices)
}
RecognizedShape::Circle { center, radius } => {
let segments = 64;
let r_in = (radius - half_w).max(0.1);
let r_out = radius + half_w;
let mut vertices = Vec::with_capacity((segments + 1) * 2);
let mut samples = Vec::with_capacity(segments);
for i in 0..=segments {
let angle = (i as f32 / segments as f32) * 2.0 * PI;
let (sin, cos) = angle.sin_cos();
vertices.push(InkVertex::new(
center.x + cos * r_out,
center.y + sin * r_out,
color,
));
vertices.push(InkVertex::new(
center.x + cos * r_in,
center.y + sin * r_in,
color,
));
if i < segments {
samples.push(StylusSample {
position: *center + Vec2::new(cos * radius, sin * radius),
pressure: 0.5,
timestamp: 0.0,
});
}
}
(samples, vertices)
}
RecognizedShape::Rectangle { corners } => closed_polygon_to_mesh(corners, half_w, color),
RecognizedShape::Triangle { corners } => closed_polygon_to_mesh(corners, half_w, color),
RecognizedShape::Rhombus { corners } => closed_polygon_to_mesh(corners, half_w, color),
RecognizedShape::Polygon { vertices } => closed_polygon_to_mesh(vertices, half_w, color),
RecognizedShape::Octahedron {
center,
radius_x,
radius_y,
depth,
} => {
let edges = octahedron_edges(*center, *radius_x, *radius_y, *depth);
let mut vertices = Vec::new();
let mut samples = Vec::new();
for (i, &(p1, p2)) in edges.iter().enumerate() {
let dir = (p2 - p1).normalize_or_zero();
let norm = Vec2::new(-dir.y, dir.x) * half_w;
let v0 = InkVertex::new(p1.x + norm.x, p1.y + norm.y, color);
let v1 = InkVertex::new(p1.x - norm.x, p1.y - norm.y, color);
let v2 = InkVertex::new(p2.x + norm.x, p2.y + norm.y, color);
let v3 = InkVertex::new(p2.x - norm.x, p2.y - norm.y, color);
if i > 0 {
let last = *vertices.last().unwrap();
vertices.push(last);
vertices.push(v0);
}
vertices.push(v0);
vertices.push(v1);
vertices.push(v2);
vertices.push(v3);
let len = (p2 - p1).length();
let steps = ((len / 15.0).ceil() as usize).max(2);
for s in 0..steps {
let t = s as f32 / steps as f32;
samples.push(StylusSample {
position: p1.lerp(p2, t),
pressure: 0.5,
timestamp: 0.0,
});
}
}
(samples, vertices)
}
RecognizedShape::CoordinateSystem {
origin,
x_axis_end,
y_axis_end,
with_arrows,
} => {
let mut vertices = Vec::new();
let mut samples = Vec::new();
// 1. Y-Achse (von Pfeilspitze oben zum Ursprung)
let y_dir = (*y_axis_end - *origin).normalize_or_zero();
let y_norm = Vec2::new(-y_dir.y, y_dir.x) * half_w;
// Y-Linienband
vertices.push(InkVertex::new(
origin.x + y_norm.x,
origin.y + y_norm.y,
color,
));
vertices.push(InkVertex::new(
origin.x - y_norm.x,
origin.y - y_norm.y,
color,
));
vertices.push(InkVertex::new(
y_axis_end.x + y_norm.x,
y_axis_end.y + y_norm.y,
color,
));
vertices.push(InkVertex::new(
y_axis_end.x - y_norm.x,
y_axis_end.y - y_norm.y,
color,
));
// Degenerate Dreiecke für Übergang zur X-Achse
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
let x_norm = Vec2::new(-x_dir.y, x_dir.x) * half_w;
let last_v = *vertices.last().unwrap();
let next_v = InkVertex::new(origin.x + x_norm.x, origin.y + x_norm.y, color);
vertices.push(last_v);
vertices.push(next_v);
// 2. X-Linienband
vertices.push(next_v);
vertices.push(InkVertex::new(
origin.x - x_norm.x,
origin.y - x_norm.y,
color,
));
vertices.push(InkVertex::new(
x_axis_end.x + x_norm.x,
x_axis_end.y + x_norm.y,
color,
));
vertices.push(InkVertex::new(
x_axis_end.x - x_norm.x,
x_axis_end.y - x_norm.y,
color,
));
// 3. Optionale Pfeilspitzen
if *with_arrows {
let arrow_len = (half_w * 5.0).max(18.0);
let arrow_w = (half_w * 3.5).max(12.0);
// Pfeilspitze Y
let y_base = *y_axis_end - y_dir * arrow_len;
let y_left = y_base + Vec2::new(-y_dir.y, y_dir.x) * arrow_w;
let y_right = y_base - Vec2::new(-y_dir.y, y_dir.x) * arrow_w;
let last_v = *vertices.last().unwrap();
let tip_v = InkVertex::new(y_axis_end.x, y_axis_end.y, color);
vertices.push(last_v);
vertices.push(tip_v);
vertices.push(tip_v);
vertices.push(InkVertex::new(y_left.x, y_left.y, color));
vertices.push(InkVertex::new(y_right.x, y_right.y, color));
// Pfeilspitze X
let x_base = *x_axis_end - x_dir * arrow_len;
let x_left = x_base + Vec2::new(-x_dir.y, x_dir.x) * arrow_w;
let x_right = x_base - Vec2::new(-x_dir.y, x_dir.x) * arrow_w;
let last_v = *vertices.last().unwrap();
let tip_x = InkVertex::new(x_axis_end.x, x_axis_end.y, color);
vertices.push(last_v);
vertices.push(tip_x);
vertices.push(tip_x);
vertices.push(InkVertex::new(x_left.x, x_left.y, color));
vertices.push(InkVertex::new(x_right.x, x_right.y, color));
}
// Abtastpunkte
samples.push(StylusSample {
position: *y_axis_end,
pressure: 0.5,
timestamp: 0.0,
});
samples.push(StylusSample {
position: *origin,
pressure: 0.5,
timestamp: 0.0,
});
samples.push(StylusSample {
position: *x_axis_end,
pressure: 0.5,
timestamp: 0.0,
});
(samples, vertices)
}
}
}
/// Erzeugt einen neuen `Stroke` aus einer erkannten Form.
pub fn shape_to_stroke(
shape: RecognizedShape,
stroke_id: u64,
color: [f32; 4],
width_units: f32,
) -> Stroke {
let (samples, vertices) = shape_to_mesh(&shape, width_units, color);
let mut stroke = Stroke::new(stroke_id, samples, vertices, color);
stroke.shape = Some(shape);
stroke
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_line() {
// Leicht zittrige diagonale Punktfolge
let mut points = Vec::new();
for i in 0..=50 {
let t = i as f32 / 50.0;
let jitter = (i as f32 * 0.7).sin() * 1.5;
points.push(Vec2::new(
100.0 + t * 400.0 + jitter,
100.0 + t * 300.0 - jitter,
));
}
let detected = detect_line(&points);
assert!(detected.is_some(), "Gerade sollte erkannt werden");
if let Some(RecognizedShape::Line { start, end }) = detected {
assert!((start - points[0]).length() < 5.0);
assert!((end - *points.last().unwrap()).length() < 10.0);
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_rectangle() {
// Handgezeichnete 4-Punkt-Schleife mit leichtem Jitter
let mut points = Vec::new();
// Kante 1: Oben links nach oben rechts
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(Vec2::new(100.0 + t * 200.0, 100.0 + (i as f32).sin() * 1.2));
}
// Kante 2: Oben rechts nach unten rechts
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(Vec2::new(300.0 + (i as f32).cos() * 1.2, 100.0 + t * 150.0));
}
// Kante 3: Unten rechts nach unten links
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(Vec2::new(300.0 - t * 200.0, 250.0 - (i as f32).sin() * 1.2));
}
// Kante 4: Unten links nach oben links (Schließen)
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(Vec2::new(100.0 - (i as f32).cos() * 1.2, 250.0 - t * 150.0));
}
points.push(points[0]);
let detected = detect_rectangle(&points);
assert!(detected.is_some(), "Rechteck sollte erkannt werden");
if let Some(RecognizedShape::Rectangle { corners }) = detected {
// Alle 4 Winkel müssen exakt 90° sein
for i in 0..4 {
let prev = corners[(i + 3) % 4];
let curr = corners[i];
let next = corners[(i + 1) % 4];
let v1 = (prev - curr).normalize();
let v2 = (next - curr).normalize();
let dot = v1.dot(v2).abs();
assert!(
dot < 1e-4,
"Winkel an Ecke {} ist nicht rechtwinklig: dot={}",
i,
dot
);
}
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_circle() {
// Unrundes Oval / Kreis mit leicht variierendem Radius (50 bis 58)
let mut points = Vec::new();
let center = Vec2::new(200.0, 200.0);
let n = 40;
for i in 0..=n {
let angle = (i as f32 / n as f32) * 2.0 * PI;
let r = 54.0 + (angle * 3.0).sin() * 3.5;
points.push(center + Vec2::new(angle.cos() * r, angle.sin() * r));
}
let detected = detect_circle(&points);
assert!(detected.is_some(), "Kreis sollte erkannt werden");
if let Some(RecognizedShape::Circle {
center: c,
radius: r,
}) = detected
{
assert!((c - center).length() < 5.0, "Zentrum weicht ab: {:?}", c);
assert!((r - 54.0).abs() < 5.0, "Radius weicht ab: {}", r);
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_coordinate_system() {
// Zwei zusammenhängende Linien im 85°-Winkel
let mut points = Vec::new();
let origin = Vec2::new(100.0, 300.0);
// Vertikaler Schenkel (Y-Achse von oben (100, 100) hinunter zum Ursprung (100, 300))
for i in 0..=20 {
let t = i as f32 / 20.0;
points.push(Vec2::new(100.0, 100.0 + t * 200.0));
}
// Schenkel im 85°-Winkel zur Y-Achse (5° nach oben geneigt statt rein horizontal)
let angle_rad = 5.0f32.to_radians();
for i in 1..=20 {
let t = i as f32 / 20.0;
let len = t * 200.0;
points.push(origin + Vec2::new(len * angle_rad.cos(), -len * angle_rad.sin()));
}
let detected = detect_coordinate_system(&points);
assert!(
detected.is_some(),
"Koordinatensystem sollte erkannt werden"
);
if let Some(RecognizedShape::CoordinateSystem {
origin: o,
x_axis_end,
y_axis_end,
..
}) = detected
{
assert!((o - origin).length() < 2.0, "Ursprung weicht ab");
let v_x = (x_axis_end - o).normalize();
let v_y = (y_axis_end - o).normalize();
let dot = v_x.dot(v_y).abs();
assert!(
dot < 1e-4,
"Achsen sind nach Orthogonalitäts-Snap nicht exakt 90°: dot={}",
dot
);
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_triangle() {
let mut points = Vec::new();
let a = Vec2::new(100.0, 300.0);
let b = Vec2::new(300.0, 300.0);
let c = Vec2::new(200.0, 126.8); // ~Gleichseitiges Dreieck
// Kante A -> B
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(a.lerp(b, t) + Vec2::new(0.0, (i as f32).sin() * 1.0));
}
// Kante B -> C
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(b.lerp(c, t) + Vec2::new((i as f32).cos() * 1.0, 0.0));
}
// Kante C -> A
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(c.lerp(a, t));
}
points.push(a);
let detected = detect_triangle(&points);
assert!(detected.is_some(), "Dreieck sollte erkannt werden");
if let Some(RecognizedShape::Triangle { corners }) = detected {
assert_eq!(corners.len(), 3);
let ab = (corners[1] - corners[0]).length();
let bc = (corners[2] - corners[1]).length();
let ca = (corners[0] - corners[2]).length();
assert!(
(ab - bc).abs() < 5.0,
"Gleichseitiges Dreieck sollte gesnappt sein"
);
assert!((bc - ca).abs() < 5.0);
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_rhombus() {
let mut points = Vec::new();
// Raute mit Kantenlängen ~200, nicht rechtwinklig (Winkel 60° und 120°)
let c0 = Vec2::new(200.0, 100.0); // Oben
let c1 = Vec2::new(320.0, 250.0); // Rechts
let c2 = Vec2::new(200.0, 400.0); // Unten
let c3 = Vec2::new(80.0, 250.0); // Links
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(c0.lerp(c1, t) + Vec2::new((i as f32).sin() * 1.0, 0.0));
}
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(c1.lerp(c2, t));
}
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(c2.lerp(c3, t) - Vec2::new((i as f32).cos() * 1.0, 0.0));
}
for i in 0..20 {
let t = i as f32 / 20.0;
points.push(c3.lerp(c0, t));
}
points.push(c0);
let detected = detect_rhombus(&points);
assert!(detected.is_some(), "Raute sollte erkannt werden");
if let Some(RecognizedShape::Rhombus { corners }) = detected {
assert_eq!(corners.len(), 4);
// Gegenüberliegende Kanten müssen parallel und gleich lang sein
let l0 = (corners[1] - corners[0]).length();
let l1 = (corners[2] - corners[1]).length();
let l2 = (corners[3] - corners[2]).length();
let l3 = (corners[0] - corners[3]).length();
assert!(
(l0 - l1).abs() < 1e-3,
"Alle 4 Seiten einer Raute müssen exakt gleich lang sein"
);
assert!((l1 - l2).abs() < 1e-3);
assert!((l2 - l3).abs() < 1e-3);
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_polygon_octagon() {
let mut points = Vec::new();
let center = Vec2::new(200.0, 200.0);
let r = 80.0;
let n = 8;
for i in 0..n {
let a1 = i as f32 * (2.0 * PI / n as f32);
let a2 = (i + 1) as f32 * (2.0 * PI / n as f32);
let p1 = center + Vec2::new(a1.cos() * r, a1.sin() * r);
let p2 = center + Vec2::new(a2.cos() * r, a2.sin() * r);
for step in 0..10 {
let t = step as f32 / 10.0;
points.push(p1.lerp(p2, t) + Vec2::new((step as f32 * 0.5).sin() * 0.8, 0.0));
}
}
points.push(points[0]);
let detected = detect_polygon(&points);
assert!(
detected.is_some(),
"Oktagon / Achteck sollte erkannt werden"
);
if let Some(RecognizedShape::Polygon { vertices }) = detected {
assert_eq!(vertices.len(), 8, "Achteck muss 8 Vertizes haben");
} else {
panic!("Falscher Formtyp erkannt");
}
}
#[test]
fn test_detect_octahedron() {
let mut points = Vec::new();
let top = Vec2::new(200.0, 50.0);
let right = Vec2::new(350.0, 200.0);
let bottom = Vec2::new(200.0, 350.0);
let left = Vec2::new(50.0, 200.0);
// Äußere Raute
for i in 0..15 {
points.push(top.lerp(right, i as f32 / 15.0));
}
for i in 0..15 {
points.push(right.lerp(bottom, i as f32 / 15.0));
}
for i in 0..15 {
points.push(bottom.lerp(left, i as f32 / 15.0));
}
for i in 0..15 {
points.push(left.lerp(top, i as f32 / 15.0));
}
// Interne Äquatorlinie von Links nach Rechts durch das Zentrum
for i in 0..20 {
points.push(left.lerp(right, i as f32 / 20.0));
}
let detected = detect_octahedron(&points);
assert!(detected.is_some(), "Oktaeder sollte erkannt werden");
if let Some(RecognizedShape::Octahedron {
center,
radius_x,
radius_y,
depth,
}) = detected
{
assert!((center.x - 200.0).abs() < 5.0);
assert!((center.y - 200.0).abs() < 5.0);
assert!(radius_x > 100.0);
assert!(radius_y > 100.0);
assert!(depth > 10.0);
} else {
panic!("Falscher Formtyp erkannt");
}
}
}