fix(geometry): spitze Pfeilspitzen beim Koordinatensystem
Die Achsenlinien liefen in voller Strichbreite bis zur Spitze und überdeckten sie; zudem war die Spitze klein und breit (~67° Öffnung). - Gemeinsame Pfeilspitzen-Geometrie (arrow_head) für Bildschirm und PDF: ~4 mm lang (dicke Linien länger, höchstens 1/5 der Achse), ~40° Öffnung - Achsen enden an der Basis der Spitze - PDF-Export nutzt dieselben Maße (vorher eigene, abweichende Werte) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+47
-65
@@ -412,76 +412,58 @@ fn render_recognized_shape(
|
||||
y_axis_end,
|
||||
with_arrows,
|
||||
} => {
|
||||
// Gleiche Pfeilspitzen wie auf dem Bildschirm; Achsen enden an der Basis der Spitze
|
||||
// (Linienstärke 1,5 pt ≈ 5,3 Seiteneinheiten, halbe Breite ≈ 2,65)
|
||||
const PDF_SHAPE_HALF_WIDTH: f32 = 2.65;
|
||||
let y_dir = (*y_axis_end - *origin).normalize_or_zero();
|
||||
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
|
||||
let y_arrow = crate::geometry::arrow_head(
|
||||
*y_axis_end,
|
||||
y_dir,
|
||||
PDF_SHAPE_HALF_WIDTH,
|
||||
(*y_axis_end - *origin).length(),
|
||||
);
|
||||
let x_arrow = crate::geometry::arrow_head(
|
||||
*x_axis_end,
|
||||
x_dir,
|
||||
PDF_SHAPE_HALF_WIDTH,
|
||||
(*x_axis_end - *origin).length(),
|
||||
);
|
||||
let (y_line_end, x_line_end) = if *with_arrows {
|
||||
(y_arrow.0, x_arrow.0)
|
||||
} else {
|
||||
(*y_axis_end, *x_axis_end)
|
||||
};
|
||||
|
||||
let (ox, oy) = page_to_pdf_mm(origin.x, origin.y);
|
||||
let (xx, xy) = page_to_pdf_mm(x_axis_end.x, x_axis_end.y);
|
||||
let (yx, yy) = page_to_pdf_mm(y_axis_end.x, y_axis_end.y);
|
||||
|
||||
// Achse 1 (Y)
|
||||
let line_y = Line {
|
||||
points: vec![
|
||||
(Point::new(Mm(ox), Mm(oy)), false),
|
||||
(Point::new(Mm(yx), Mm(yy)), false),
|
||||
],
|
||||
is_closed: false,
|
||||
};
|
||||
layer.add_line(line_y);
|
||||
|
||||
// Achse 2 (X)
|
||||
let line_x = Line {
|
||||
points: vec![
|
||||
(Point::new(Mm(ox), Mm(oy)), false),
|
||||
(Point::new(Mm(xx), Mm(xy)), false),
|
||||
],
|
||||
is_closed: false,
|
||||
};
|
||||
layer.add_line(line_x);
|
||||
for end in [y_line_end, x_line_end] {
|
||||
let (ex, ey) = page_to_pdf_mm(end.x, end.y);
|
||||
layer.add_line(Line {
|
||||
points: vec![
|
||||
(Point::new(Mm(ox), Mm(oy)), false),
|
||||
(Point::new(Mm(ex), Mm(ey)), false),
|
||||
],
|
||||
is_closed: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Pfeilspitzen als gefüllte Dreiecke
|
||||
if *with_arrows {
|
||||
layer.set_fill_color(stroke_color);
|
||||
|
||||
let y_dir = (*y_axis_end - *origin).normalize_or_zero();
|
||||
let arrow_len_page = 25.0;
|
||||
let arrow_w_page = 16.0;
|
||||
|
||||
let base_y = *y_axis_end - y_dir * arrow_len_page;
|
||||
let left_y = base_y + glam::Vec2::new(-y_dir.y, y_dir.x) * (arrow_w_page * 0.5);
|
||||
let right_y = base_y - glam::Vec2::new(-y_dir.y, y_dir.x) * (arrow_w_page * 0.5);
|
||||
|
||||
let (tx, ty) = page_to_pdf_mm(y_axis_end.x, y_axis_end.y);
|
||||
let (lx, ly) = page_to_pdf_mm(left_y.x, left_y.y);
|
||||
let (rx, ry) = page_to_pdf_mm(right_y.x, right_y.y);
|
||||
|
||||
let arrow_poly_y = Polygon {
|
||||
rings: vec![vec![
|
||||
(Point::new(Mm(tx), Mm(ty)), false),
|
||||
(Point::new(Mm(lx), Mm(ly)), false),
|
||||
(Point::new(Mm(rx), Mm(ry)), false),
|
||||
]],
|
||||
mode: PaintMode::Fill,
|
||||
winding_order: WindingOrder::NonZero,
|
||||
};
|
||||
layer.add_polygon(arrow_poly_y);
|
||||
|
||||
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
|
||||
let base_x = *x_axis_end - x_dir * arrow_len_page;
|
||||
let left_x = base_x + glam::Vec2::new(-x_dir.y, x_dir.x) * (arrow_w_page * 0.5);
|
||||
let right_x = base_x - glam::Vec2::new(-x_dir.y, x_dir.x) * (arrow_w_page * 0.5);
|
||||
|
||||
let (tx_x, ty_x) = page_to_pdf_mm(x_axis_end.x, x_axis_end.y);
|
||||
let (lx_x, ly_x) = page_to_pdf_mm(left_x.x, left_x.y);
|
||||
let (rx_x, ry_x) = page_to_pdf_mm(right_x.x, right_x.y);
|
||||
|
||||
let arrow_poly_x = Polygon {
|
||||
rings: vec![vec![
|
||||
(Point::new(Mm(tx_x), Mm(ty_x)), false),
|
||||
(Point::new(Mm(lx_x), Mm(ly_x)), false),
|
||||
(Point::new(Mm(rx_x), Mm(ry_x)), false),
|
||||
]],
|
||||
mode: PaintMode::Fill,
|
||||
winding_order: WindingOrder::NonZero,
|
||||
};
|
||||
layer.add_polygon(arrow_poly_x);
|
||||
for (tip, (_, left, right)) in [(*y_axis_end, y_arrow), (*x_axis_end, x_arrow)] {
|
||||
let ring = [tip, left, right]
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (px, py) = page_to_pdf_mm(p.x, p.y);
|
||||
(Point::new(Mm(px), Mm(py)), false)
|
||||
})
|
||||
.collect();
|
||||
layer.add_polygon(Polygon {
|
||||
rings: vec![ring],
|
||||
mode: PaintMode::Fill,
|
||||
winding_order: WindingOrder::NonZero,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+97
-46
@@ -1269,6 +1269,23 @@ fn best_closed_shape(pts: &[Vec2]) -> Option<(f32, RecognizedShape)> {
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||||
}
|
||||
|
||||
/// Halber Öffnungswinkel einer Pfeilspitze als Verhältnis halbe Breite zu Länge (tan 20° ≈ 0,36,
|
||||
/// also ~40° Öffnung – schlank genug, dass die Spitze spitz wirkt).
|
||||
const ARROW_HALF_WIDTH_RATIO: f32 = 0.36;
|
||||
|
||||
/// Geometrie einer Pfeilspitze an `tip`, Richtung `dir` (Einheitsvektor), für eine Linie der
|
||||
/// halben Breite `half_w` (Seiteneinheiten) und der Länge `line_len`. Liefert (Basismitte,
|
||||
/// linke Ecke, rechte Ecke). Die Spitze ist ~4 mm lang (dicke Linien länger), höchstens ein
|
||||
/// Fünftel der Linie. Die Linie soll an der Basismitte enden – reicht sie bis zur Spitze,
|
||||
/// wirkt diese stumpf.
|
||||
pub fn arrow_head(tip: Vec2, dir: Vec2, half_w: f32, line_len: f32) -> (Vec2, Vec2, Vec2) {
|
||||
let len = (half_w * 10.0).max(40.0).min(line_len * 0.2);
|
||||
let half_width = len * ARROW_HALF_WIDTH_RATIO;
|
||||
let base = tip - dir * len;
|
||||
let normal = Vec2::new(-dir.y, dir.x);
|
||||
(base, base + normal * half_width, base - normal * half_width)
|
||||
}
|
||||
|
||||
/// Abstand eines Punktes zur Strecke `a`–`b`.
|
||||
fn segment_distance(p: Vec2, a: Vec2, b: Vec2) -> f32 {
|
||||
let ab = b - a;
|
||||
@@ -1539,11 +1556,20 @@ pub fn shape_to_mesh(
|
||||
let mut vertices = Vec::new();
|
||||
let mut samples = Vec::new();
|
||||
|
||||
// 1. Y-Achse (von Pfeilspitze oben zum Ursprung)
|
||||
// Achsen enden bei Pfeilen an der Basis der Spitze, sonst überdeckt das Linienband
|
||||
// die Spitze und sie wirkt stumpf
|
||||
let y_dir = (*y_axis_end - *origin).normalize_or_zero();
|
||||
let y_norm = Vec2::new(-y_dir.y, y_dir.x) * half_w;
|
||||
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
|
||||
let y_arrow = arrow_head(*y_axis_end, y_dir, half_w, (*y_axis_end - *origin).length());
|
||||
let x_arrow = arrow_head(*x_axis_end, x_dir, half_w, (*x_axis_end - *origin).length());
|
||||
let (y_line_end, x_line_end) = if *with_arrows {
|
||||
(y_arrow.0, x_arrow.0)
|
||||
} else {
|
||||
(*y_axis_end, *x_axis_end)
|
||||
};
|
||||
|
||||
// Y-Linienband
|
||||
// 1. Y-Achse (vom Ursprung zur Spitze)
|
||||
let y_norm = Vec2::new(-y_dir.y, y_dir.x) * half_w;
|
||||
vertices.push(InkVertex::new(
|
||||
origin.x + y_norm.x,
|
||||
origin.y + y_norm.y,
|
||||
@@ -1555,26 +1581,24 @@ pub fn shape_to_mesh(
|
||||
color,
|
||||
));
|
||||
vertices.push(InkVertex::new(
|
||||
y_axis_end.x + y_norm.x,
|
||||
y_axis_end.y + y_norm.y,
|
||||
y_line_end.x + y_norm.x,
|
||||
y_line_end.y + y_norm.y,
|
||||
color,
|
||||
));
|
||||
vertices.push(InkVertex::new(
|
||||
y_axis_end.x - y_norm.x,
|
||||
y_axis_end.y - y_norm.y,
|
||||
y_line_end.x - y_norm.x,
|
||||
y_line_end.y - y_norm.y,
|
||||
color,
|
||||
));
|
||||
|
||||
// Degenerate Dreiecke für Übergang zur X-Achse
|
||||
let x_dir = (*x_axis_end - *origin).normalize_or_zero();
|
||||
// Degenerierte Dreiecke für den Übergang zur X-Achse
|
||||
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
|
||||
// 2. X-Achse
|
||||
vertices.push(next_v);
|
||||
vertices.push(InkVertex::new(
|
||||
origin.x - x_norm.x,
|
||||
@@ -1582,48 +1606,27 @@ pub fn shape_to_mesh(
|
||||
color,
|
||||
));
|
||||
vertices.push(InkVertex::new(
|
||||
x_axis_end.x + x_norm.x,
|
||||
x_axis_end.y + x_norm.y,
|
||||
x_line_end.x + x_norm.x,
|
||||
x_line_end.y + x_norm.y,
|
||||
color,
|
||||
));
|
||||
vertices.push(InkVertex::new(
|
||||
x_axis_end.x - x_norm.x,
|
||||
x_axis_end.y - x_norm.y,
|
||||
x_line_end.x - x_norm.x,
|
||||
x_line_end.y - x_norm.y,
|
||||
color,
|
||||
));
|
||||
|
||||
// 3. Optionale Pfeilspitzen
|
||||
// 3. Optionale Pfeilspitzen (je ein Dreieck, über degenerierte Dreiecke angehängt)
|
||||
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));
|
||||
for (tip, (_, left, right)) in [(*y_axis_end, y_arrow), (*x_axis_end, x_arrow)] {
|
||||
let last_v = *vertices.last().unwrap();
|
||||
let tip_v = InkVertex::new(tip.x, tip.y, color);
|
||||
vertices.push(last_v);
|
||||
vertices.push(tip_v);
|
||||
vertices.push(tip_v);
|
||||
vertices.push(InkVertex::new(left.x, left.y, color));
|
||||
vertices.push(InkVertex::new(right.x, right.y, color));
|
||||
}
|
||||
}
|
||||
|
||||
// Abtastpunkte
|
||||
@@ -2400,4 +2403,52 @@ mod tests {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arrow_head_is_slender_and_axes_end_at_its_base() {
|
||||
// Spitze schlank (~40° Öffnung) und breiter als die Linie
|
||||
let (base, left, right) = arrow_head(Vec2::new(0.0, 0.0), Vec2::new(0.0, -1.0), 2.5, 500.0);
|
||||
let len = base.length();
|
||||
let half_width = (left - base).length();
|
||||
let opening = 2.0 * (half_width / len).atan().to_degrees();
|
||||
assert!(
|
||||
(35.0..=45.0).contains(&opening),
|
||||
"Öffnungswinkel {opening}°"
|
||||
);
|
||||
assert!(
|
||||
half_width > 2.5 * 2.0,
|
||||
"Spitze muss breiter als die Linie sein"
|
||||
);
|
||||
assert!(((left + right) * 0.5 - base).length() < 1e-4);
|
||||
assert!(len >= 40.0, "Spitze mindestens 4 mm lang: {len}");
|
||||
// Bei kurzen Linien höchstens ein Fünftel der Linie
|
||||
let (short_base, ..) = arrow_head(Vec2::ZERO, Vec2::new(1.0, 0.0), 2.5, 100.0);
|
||||
assert!((short_base.length() - 20.0).abs() < 1e-3);
|
||||
|
||||
// Kein Vertex des Linienbands ragt über die Basis hinaus in die Spitze hinein
|
||||
let origin = Vec2::new(100.0, 300.0);
|
||||
let y_end = Vec2::new(100.0, 100.0);
|
||||
let x_end = Vec2::new(300.0, 300.0);
|
||||
let shape = RecognizedShape::CoordinateSystem {
|
||||
origin,
|
||||
x_axis_end: x_end,
|
||||
y_axis_end: y_end,
|
||||
with_arrows: true,
|
||||
};
|
||||
let (_, vertices) = shape_to_mesh(&shape, 5.0, [0.0, 0.0, 0.0, 1.0]);
|
||||
let (y_base, ..) = arrow_head(y_end, Vec2::new(0.0, -1.0), 2.5, 200.0);
|
||||
let (x_base, ..) = arrow_head(x_end, Vec2::new(1.0, 0.0), 2.5, 200.0);
|
||||
for v in &vertices {
|
||||
let p = Vec2::new(v.position[0], v.position[1]);
|
||||
let is_tip = (p - y_end).length() < 1e-3 || (p - x_end).length() < 1e-3;
|
||||
assert!(
|
||||
is_tip || p.y >= y_base.y - 1e-3,
|
||||
"Y-Achse ragt in die Spitze: {p:?}"
|
||||
);
|
||||
assert!(
|
||||
is_tip || p.x <= x_base.x + 1e-3,
|
||||
"X-Achse ragt in die Spitze: {p:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user