1045 lines
43 KiB
Rust
1045 lines
43 KiB
Rust
//! Vector shape rasterization.
|
|
|
|
pub mod path_boolean;
|
|
|
|
use hcie_protocol::{Layer, LayerData, VectorShape};
|
|
use hcie_blend::blend_pixels;
|
|
use std::cell::Cell;
|
|
|
|
thread_local! {
|
|
static ACTIVE_HARDNESS: Cell<f32> = Cell::new(1.0);
|
|
}
|
|
|
|
fn get_shape_hardness(shape: &VectorShape) -> f32 {
|
|
use VectorShape::*;
|
|
match shape {
|
|
Line { hardness, .. } => *hardness,
|
|
Rect { hardness, .. } => *hardness,
|
|
Circle { hardness, .. } => *hardness,
|
|
Arrow { hardness, .. } => *hardness,
|
|
Star { hardness, .. } => *hardness,
|
|
Polygon { hardness, .. } => *hardness,
|
|
Rhombus { hardness, .. } => *hardness,
|
|
Cylinder { hardness, .. } => *hardness,
|
|
Heart { hardness, .. } => *hardness,
|
|
Bubble { hardness, .. } => *hardness,
|
|
Gear { hardness, .. } => *hardness,
|
|
Cross { hardness, .. } => *hardness,
|
|
Crescent { hardness, .. } => *hardness,
|
|
Bolt { hardness, .. } => *hardness,
|
|
Arrow4 { hardness, .. } => *hardness,
|
|
FreePath { hardness, .. } => *hardness,
|
|
}
|
|
}
|
|
|
|
fn draw_filled_circle_smooth(
|
|
layer: &mut Layer,
|
|
cx: f32,
|
|
cy: f32,
|
|
r: f32,
|
|
color: [u8; 4],
|
|
hardness: f32,
|
|
mask: Option<&[u8]>,
|
|
) {
|
|
let r_i = r.ceil() as i32;
|
|
let h_clamped = hardness.clamp(0.0, 0.99);
|
|
let transition_start = r * h_clamped;
|
|
let transition_width = r - transition_start;
|
|
let transition_width = if transition_width < 2.0 { 2.0 } else { transition_width };
|
|
|
|
for dy in -r_i..=r_i {
|
|
let py = (cy + dy as f32).round() as i32;
|
|
if py < 0 || py >= layer.height as i32 { continue; }
|
|
|
|
let dx_max = (r * r - dy as f32 * dy as f32).sqrt();
|
|
let dx_i = dx_max.ceil() as i32;
|
|
|
|
for dx in -dx_i..=dx_i {
|
|
let px = (cx + dx as f32).round() as i32;
|
|
if px < 0 || px >= layer.width as i32 { continue; }
|
|
|
|
if let Some(m) = mask {
|
|
let idx = (py as u32 * layer.width + px as u32) as usize;
|
|
if m.get(idx).copied().unwrap_or(0) == 0 { continue; }
|
|
}
|
|
|
|
// Calculate precise subpixel Euclidean distance to floating-point center
|
|
let dist = ((px as f32 - cx).powi(2) + (py as f32 - cy).powi(2)).sqrt();
|
|
|
|
// Hardness-based smoothing:
|
|
let alpha_t = if dist <= transition_start {
|
|
1.0
|
|
} else if dist >= r {
|
|
0.0
|
|
} else {
|
|
1.0 - (dist - transition_start) / transition_width
|
|
};
|
|
|
|
if alpha_t <= 0.0 { continue; }
|
|
|
|
let src = [color[0], color[1], color[2], (color[3] as f32 * alpha_t).round() as u8];
|
|
let dst = layer.get_pixel(px as u32, py as u32);
|
|
let out = hcie_blend::blend_pixels(dst, src, hcie_blend::BlendMode::Normal, 1.0);
|
|
layer.set_pixel(px as u32, py as u32, out);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_line(
|
|
layer: &mut Layer,
|
|
x0: f32, y0: f32, x1: f32, y1: f32,
|
|
color: [u8; 4],
|
|
width: f32,
|
|
mask: Option<&[u8]>,
|
|
) {
|
|
let dx = x1 - x0;
|
|
let dy = y1 - y0;
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|
if dist == 0.0 { return; }
|
|
|
|
let hardness = ACTIVE_HARDNESS.with(|cell| cell.get());
|
|
let steps = (dist * 2.0).max(1.0).ceil() as i32;
|
|
for i in 0..=steps {
|
|
let t = i as f32 / steps as f32;
|
|
let px = x0 + dx * t;
|
|
let py = y0 + dy * t;
|
|
draw_filled_circle_smooth(layer, px, py, width / 2.0, color, hardness, mask);
|
|
}
|
|
}
|
|
|
|
/// Render all vector shapes in a layer to its pixel buffer.
|
|
/// NOTE: The caller MUST clear the pixel buffer before calling this
|
|
/// on pure-vector layers (e.g., by calling `render_vector_clear_bg`).
|
|
pub fn render_vector_shapes(layer: &mut Layer) {
|
|
let shapes = match &layer.data {
|
|
LayerData::Vector { shapes } => shapes.clone(),
|
|
_ => return,
|
|
};
|
|
|
|
for shape in shapes {
|
|
render_shape(layer, &shape);
|
|
}
|
|
}
|
|
|
|
/// Clear pixel buffer for vector-only re-render.
|
|
pub fn render_vector_clear_bg(layer: &mut Layer) {
|
|
layer.pixels.fill(0);
|
|
}
|
|
|
|
/// Rotates a coordinate around a pivot point by a given angle.
|
|
///
|
|
/// **Purpose:**
|
|
/// Applies a 2D rotation matrix transformation to a point `(x, y)` around a central pivot `(cx, cy)`.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. If the angle is exactly 0.0, returns the original coordinates immediately.
|
|
/// 2. Calculates sine and cosine of the angle.
|
|
/// 3. Shifts the point to the origin relative to the pivot.
|
|
/// 4. Performs matrix multiplication:
|
|
/// - `new_x = cx + dx * cos - dy * sin`
|
|
/// - `new_y = cy + dx * sin + dy * cos`
|
|
///
|
|
/// **Arguments:**
|
|
/// - `x`: Original X coordinate.
|
|
/// - `y`: Original Y coordinate.
|
|
/// - `cx`: Center X coordinate of rotation.
|
|
/// - `cy`: Center Y coordinate of rotation.
|
|
/// - `angle`: Rotation angle in radians.
|
|
///
|
|
/// **Returns:**
|
|
/// A `(f32, f32)` containing the rotated coordinates.
|
|
///
|
|
/// **Side Effects / Dependencies:**
|
|
/// None. Pure mathematics.
|
|
fn rotate_point(x: f32, y: f32, cx: f32, cy: f32, angle: f32) -> (f32, f32) {
|
|
if angle == 0.0 {
|
|
return (x, y);
|
|
}
|
|
let (sin_a, cos_a) = angle.sin_cos();
|
|
let dx = x - cx;
|
|
let dy = y - cy;
|
|
(
|
|
cx + dx * cos_a - dy * sin_a,
|
|
cy + dx * sin_a + dy * cos_a,
|
|
)
|
|
}
|
|
|
|
/// Generate the vertices of an ellipse.
|
|
///
|
|
/// **Purpose:**
|
|
/// Generates a set of 2D coordinates representing the outline of an ellipse.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. Samples the ellipse equation using standard trigonometric step increments.
|
|
/// 2. Scales horizontal and vertical radii from center coordinate.
|
|
///
|
|
/// **Arguments:**
|
|
/// - `cx`: Center X-coordinate.
|
|
/// - `cy`: Center Y-coordinate.
|
|
/// - `rx`: Horizontal radius.
|
|
/// - `ry`: Vertical radius.
|
|
///
|
|
/// **Returns:**
|
|
/// A `Vec<(f32, f32)>` containing the sampled vertices.
|
|
fn get_ellipse_pts(cx: f32, cy: f32, rx: f32, ry: f32) -> Vec<(f32, f32)> {
|
|
let steps = ((rx + ry).max(1.0) * 6.0).max(32.0).ceil() as i32;
|
|
let mut pts = Vec::with_capacity(steps as usize);
|
|
for i in 0..steps {
|
|
let angle = std::f32::consts::PI * 2.0 * i as f32 / steps as f32;
|
|
let px = cx + rx * angle.cos();
|
|
let py = cy + ry * angle.sin();
|
|
pts.push((px, py));
|
|
}
|
|
pts
|
|
}
|
|
|
|
fn render_shape(layer: &mut Layer, shape: &VectorShape) {
|
|
let h = get_shape_hardness(shape);
|
|
ACTIVE_HARDNESS.with(|cell| cell.set(h));
|
|
use VectorShape::*;
|
|
match shape {
|
|
Line { x1, y1, x2, y2, stroke, color, opacity, angle, .. } => {
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
let cx = (*x1 + *x2) / 2.0;
|
|
let cy = (*y1 + *y2) / 2.0;
|
|
let (rx1, ry1) = rotate_point(*x1, *y1, cx, cy, *angle);
|
|
let (rx2, ry2) = rotate_point(*x2, *y2, cx, cy, *angle);
|
|
draw_line(layer, rx1, ry1, rx2, ry2, px_color, *stroke, None);
|
|
}
|
|
Rect { x1, y1, x2, y2, stroke, color, fill_color, fill, radius, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
|
|
let r = radius.min((right - left) * 0.5).min((bottom - top) * 0.5);
|
|
|
|
let mut pts = if r <= 0.0 {
|
|
vec![(left, top), (right, top), (right, bottom), (left, bottom)]
|
|
} else {
|
|
get_rounded_rect_pts(left, top, right, bottom, r)
|
|
};
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
|
|
Circle { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let rx = (right - left) / 2.0;
|
|
let ry = (bottom - top) / 2.0;
|
|
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts = get_ellipse_pts(cx, cy, rx, ry);
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Arrow { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, thick, angle, .. } => {
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
let arrow_thickness = if *thick { stroke * 2.5 } else { stroke * 1.8 };
|
|
let arrow_head_len = arrow_thickness * 3.0;
|
|
|
|
let cx = (*x1 + *x2) / 2.0;
|
|
let cy = (*y1 + *y2) / 2.0;
|
|
|
|
let rx1 = *x1;
|
|
let ry1 = *y1;
|
|
let rx2 = *x2;
|
|
let ry2 = *y2;
|
|
|
|
let head_angle = ((ry2 - ry1)).atan2(rx2 - rx1);
|
|
let arrow_angle = std::f32::consts::PI / 6.0;
|
|
let rx3 = rx2 - arrow_head_len * (head_angle + arrow_angle).cos();
|
|
let ry3 = ry2 - arrow_head_len * (head_angle + arrow_angle).sin();
|
|
let rx4 = rx2 - arrow_head_len * (head_angle - arrow_angle).cos();
|
|
let ry4 = ry2 - arrow_head_len * (head_angle - arrow_angle).sin();
|
|
|
|
let (rot_x1, rot_y1) = rotate_point(rx1, ry1, cx, cy, *angle);
|
|
let (rot_x2, rot_y2) = rotate_point(rx2, ry2, cx, cy, *angle);
|
|
let (rot_x3, rot_y3) = rotate_point(rx3, ry3, cx, cy, *angle);
|
|
let (rot_x4, rot_y4) = rotate_point(rx4, ry4, cx, cy, *angle);
|
|
|
|
draw_line(layer, rot_x1, rot_y1, rot_x2, rot_y2, px_color, *stroke, None);
|
|
draw_line(layer, rot_x2, rot_y2, rot_x3, rot_y3, px_color, *stroke, None);
|
|
draw_line(layer, rot_x2, rot_y2, rot_x4, rot_y4, px_color, *stroke, None);
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
draw_line(layer, rot_x2, rot_y2, rot_x3, rot_y3, px_fill, arrow_head_len, None);
|
|
}
|
|
}
|
|
Star { x1, y1, x2, y2, stroke, color, fill_color, fill, points, inner_radius, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let outer_r = (right - left).min(bottom - top) / 2.0;
|
|
let inner_r = inner_radius.max(0.1);
|
|
let n_points = *points as i32;
|
|
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts: Vec<(f32, f32)> = Vec::with_capacity(n_points as usize * 2);
|
|
for i in 0..(n_points * 2) {
|
|
let a = std::f32::consts::PI * 2.0 * i as f32 / (n_points * 2) as f32 - std::f32::consts::PI / 2.0;
|
|
let r = if i % 2 == 0 { outer_r } else { outer_r * inner_r };
|
|
let px = cx + r * a.cos();
|
|
let py = cy + r * a.sin();
|
|
pts.push((px, py));
|
|
}
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Polygon { x1, y1, x2, y2, stroke, color, fill_color, fill, sides, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let r = (right - left).min(bottom - top) / 2.0;
|
|
let n_sides = (*sides).max(3);
|
|
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts: Vec<(f32, f32)> = Vec::with_capacity(n_sides as usize);
|
|
for i in 0..n_sides {
|
|
let a = std::f32::consts::PI * 2.0 * i as f32 / n_sides as f32 - std::f32::consts::PI / 2.0;
|
|
let px = cx + r * a.cos();
|
|
let py = cy + r * a.sin();
|
|
pts.push((px, py));
|
|
}
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Rhombus { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let pts = vec![
|
|
(cx, top), (right, cy), (cx, bottom), (left, cy),
|
|
];
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts = pts;
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
for i in 0..4 {
|
|
let j = (i + 1) % 4;
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Cylinder { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let rx = (right - left) / 2.0;
|
|
let ry = (bottom - top) * 0.1; // Cylinder depth effect
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts_top = get_ellipse_pts(cx, top + ry, rx, ry);
|
|
let mut pts_bottom = get_ellipse_pts(cx, bottom - ry, rx, ry);
|
|
|
|
for p in &mut pts_top {
|
|
let rot = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rot.0; p.1 = rot.1;
|
|
}
|
|
for p in &mut pts_bottom {
|
|
let rot = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rot.0; p.1 = rot.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
let mut fill_pts = Vec::new();
|
|
fill_pts.extend(pts_top.clone());
|
|
fill_pts.extend(pts_bottom.clone());
|
|
fill_polygon(layer, &fill_pts, px_fill);
|
|
}
|
|
|
|
for i in 0..pts_top.len() {
|
|
let j = (i + 1) % pts_top.len();
|
|
draw_line(layer, pts_top[i].0, pts_top[i].1, pts_top[j].0, pts_top[j].1, px_color, *stroke, None);
|
|
}
|
|
for i in 0..pts_bottom.len() {
|
|
let j = (i + 1) % pts_bottom.len();
|
|
draw_line(layer, pts_bottom[i].0, pts_bottom[i].1, pts_bottom[j].0, pts_bottom[j].1, px_color, *stroke, None);
|
|
}
|
|
|
|
let (rot_left_top_x, rot_left_top_y) = rotate_point(left, top + ry, cx, cy, *angle);
|
|
let (rot_left_bot_x, rot_left_bot_y) = rotate_point(left, bottom - ry, cx, cy, *angle);
|
|
let (rot_right_top_x, rot_right_top_y) = rotate_point(right, top + ry, cx, cy, *angle);
|
|
let (rot_right_bot_x, rot_right_bot_y) = rotate_point(right, bottom - ry, cx, cy, *angle);
|
|
|
|
draw_line(layer, rot_left_top_x, rot_left_top_y, rot_left_bot_x, rot_left_bot_y, px_color, *stroke, None);
|
|
draw_line(layer, rot_right_top_x, rot_right_top_y, rot_right_bot_x, rot_right_bot_y, px_color, *stroke, None);
|
|
}
|
|
|
|
Heart { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let hw = (right - left) * 0.5;
|
|
let hh = (bottom - top) * 0.5;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
let steps = 32;
|
|
let mut pts: Vec<(f32, f32)> = Vec::with_capacity(steps);
|
|
for i in 0..steps {
|
|
let t = std::f32::consts::PI * 2.0 * i as f32 / steps as f32;
|
|
let x = cx + hw * (16.0 * t.sin().powi(3)) / 16.0;
|
|
let y = cy - hh * (13.0 * t.cos() - 5.0 * (2.0 * t).cos() - 2.0 * (3.0 * t).cos() - (4.0 * t).cos()) / 16.0;
|
|
pts.push((x, y));
|
|
}
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
for i in 0..steps {
|
|
let j = (i + 1) % steps;
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Bubble { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let rx = (right - left) / 2.0;
|
|
let ry = (bottom - top) / 2.0;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts = get_bubble_pts(cx, cy, rx, ry);
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
|
|
Gear { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let r_base = (right - left).min(bottom - top) / 2.0;
|
|
let outer_r = r_base * 0.9;
|
|
let inner_r = r_base * 0.6;
|
|
let hole_r = r_base * 0.25;
|
|
let teeth = 8;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
let mut pts: Vec<(f32, f32)> = Vec::with_capacity(teeth as usize * 2);
|
|
for i in 0..(teeth * 2) {
|
|
let a = std::f32::consts::PI * 2.0 * i as f32 / (teeth * 2) as f32 - std::f32::consts::PI / 2.0;
|
|
let r = if i % 2 == 0 { outer_r } else { inner_r };
|
|
pts.push((cx + r * a.cos(), cy + r * a.sin()));
|
|
}
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
|
|
// Draw inner hole circle (matches TS preview exactly)
|
|
let mut hole_pts = get_ellipse_pts(cx, cy, hole_r, hole_r);
|
|
for p in &mut hole_pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &hole_pts, px_fill);
|
|
}
|
|
for i in 0..hole_pts.len() {
|
|
let j = (i + 1) % hole_pts.len();
|
|
draw_line(layer, hole_pts[i].0, hole_pts[i].1, hole_pts[j].0, hole_pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Cross { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let hw = (right - left) * 0.3;
|
|
let hh = (bottom - top) * 0.3;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts = vec![
|
|
(cx - hw, top),
|
|
(cx + hw, top),
|
|
(cx + hw, cy - hh),
|
|
(right, cy - hh),
|
|
(right, cy + hh),
|
|
(cx + hw, cy + hh),
|
|
(cx + hw, bottom),
|
|
(cx - hw, bottom),
|
|
(cx - hw, cy + hh),
|
|
(left, cy + hh),
|
|
(left, cy - hh),
|
|
(cx - hw, cy - hh),
|
|
];
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
Crescent { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let rx = (right - left) / 2.0;
|
|
let ry = (bottom - top) / 2.0;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts = get_crescent_pts(cx, cy, rx, ry);
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None);
|
|
}
|
|
}
|
|
|
|
Bolt { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
let cx = (*x1 + *x2) / 2.0;
|
|
let cy = (*y1 + *y2) / 2.0;
|
|
let mid_x = (*x1 + *x2) / 2.0;
|
|
|
|
let mut pts = vec![
|
|
(*x1, *y1), (mid_x, *y2 * 0.7 + *y1 * 0.3), (*x2, *y1 * 0.3 + *y2 * 0.7), (*x2, *y2),
|
|
];
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
draw_line(layer, pts[0].0, pts[0].1, pts[1].0, pts[1].1, px_color, *stroke, None);
|
|
draw_line(layer, pts[1].0, pts[1].1, pts[2].0, pts[2].1, px_color, *stroke, None);
|
|
draw_line(layer, pts[2].0, pts[2].1, pts[3].0, pts[3].1, px_color, *stroke, None);
|
|
}
|
|
Arrow4 { x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, angle, .. } => {
|
|
let (left, top, right, bottom) = normalized_rect(*x1, *y1, *x2, *y2);
|
|
let cx = (left + right) / 2.0;
|
|
let cy = (top + bottom) / 2.0;
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
|
|
let mut pts = vec![
|
|
(cx, top), (right, cy), (cx, bottom), (left, cy),
|
|
];
|
|
|
|
for p in &mut pts {
|
|
let rotated = rotate_point(p.0, p.1, cx, cy, *angle);
|
|
p.0 = rotated.0;
|
|
p.1 = rotated.1;
|
|
}
|
|
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
fill_polygon(layer, &pts, px_fill);
|
|
}
|
|
draw_line(layer, pts[0].0, pts[0].1, pts[1].0, pts[1].1, px_color, *stroke, None);
|
|
draw_line(layer, pts[1].0, pts[1].1, pts[2].0, pts[2].1, px_color, *stroke, None);
|
|
draw_line(layer, pts[2].0, pts[2].1, pts[3].0, pts[3].1, px_color, *stroke, None);
|
|
draw_line(layer, pts[3].0, pts[3].1, pts[0].0, pts[0].1, px_color, *stroke, None);
|
|
}
|
|
FreePath { pts, stroke, color, fill, fill_color, opacity, .. } => {
|
|
let alpha = (color[3] as f32 * opacity).round() as u8;
|
|
let px_color = [color[0], color[1], color[2], alpha];
|
|
if *fill {
|
|
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
|
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
|
let tup_pts: Vec<(f32, f32)> = pts.iter().map(|p| (p[0], p[1])).collect();
|
|
fill_polygon(layer, &tup_pts, px_fill);
|
|
}
|
|
for i in 1..pts.len() {
|
|
draw_line(layer, pts[i-1][0], pts[i-1][1], pts[i][0], pts[i][1], px_color, *stroke, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn normalized_rect(x1: f32, y1: f32, x2: f32, y2: f32) -> (f32, f32, f32, f32) {
|
|
(x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2))
|
|
}
|
|
|
|
fn fill_polygon(layer: &mut Layer, pts: &[(f32, f32)], color: [u8; 4]) {
|
|
if pts.len() < 3 { return; }
|
|
|
|
let min_y = pts.iter().map(|p| p.1).fold(f32::INFINITY, f32::min).floor() as i32;
|
|
let max_y = pts.iter().map(|p| p.1).fold(f32::NEG_INFINITY, f32::max).ceil() as i32;
|
|
let _min_x = pts.iter().map(|p| p.0).fold(f32::INFINITY, f32::min).floor() as i32;
|
|
let _max_x = pts.iter().map(|p| p.0).fold(f32::NEG_INFINITY, f32::max).ceil() as i32;
|
|
|
|
let w = layer.width as i32;
|
|
let h = layer.height as i32;
|
|
|
|
for y in min_y..=max_y {
|
|
if y < 0 || y >= h { continue; }
|
|
let mut intersections = Vec::new();
|
|
|
|
for i in 0..pts.len() {
|
|
let j = (i + 1) % pts.len();
|
|
let p1 = pts[i];
|
|
let p2 = pts[j];
|
|
|
|
if (p1.1 > y as f32 && p2.1 <= y as f32) || (p2.1 > y as f32 && p1.1 <= y as f32) {
|
|
if (p2.1 - p1.1).abs() > 1e-6 {
|
|
let t = (y as f32 - p1.1) / (p2.1 - p1.1);
|
|
let ix = p1.0 + t * (p2.0 - p1.0);
|
|
intersections.push(ix);
|
|
}
|
|
}
|
|
}
|
|
|
|
intersections.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
|
|
for pair in intersections.chunks_exact(2) {
|
|
let x_start_float = pair[0].max(0.0);
|
|
let x_end_float = pair[1].min((w - 1) as f32);
|
|
let x_start = x_start_float.ceil() as i32;
|
|
let x_end = x_end_float.floor() as i32;
|
|
|
|
// Fill interior pixels at full opacity
|
|
for x in x_start..=x_end {
|
|
let dst = layer.get_pixel(x as u32, y as u32);
|
|
let out = blend_pixels(dst, color, hcie_blend::BlendMode::Normal, 1.0);
|
|
layer.set_pixel(x as u32, y as u32, out);
|
|
}
|
|
|
|
// Left boundary pixel: subpixel coverage
|
|
let left_px = x_start_float.floor() as i32;
|
|
if left_px >= 0 && left_px < w && left_px != x_start {
|
|
let coverage = (left_px as f32 + 1.0 - x_start_float).clamp(0.0, 1.0);
|
|
let aa = (color[3] as f32 * coverage).round() as u8;
|
|
let mut aa_color = color;
|
|
aa_color[3] = aa;
|
|
let dst = layer.get_pixel(left_px as u32, y as u32);
|
|
let out = blend_pixels(dst, aa_color, hcie_blend::BlendMode::Normal, 1.0);
|
|
layer.set_pixel(left_px as u32, y as u32, out);
|
|
}
|
|
|
|
// Right boundary pixel: subpixel coverage
|
|
let right_px = x_end_float.ceil() as i32;
|
|
if right_px >= 0 && right_px < w && right_px != x_end {
|
|
let coverage = (x_end_float - (right_px as f32 - 1.0)).clamp(0.0, 1.0);
|
|
let aa = (color[3] as f32 * coverage).round() as u8;
|
|
let mut aa_color = color;
|
|
aa_color[3] = aa;
|
|
let dst = layer.get_pixel(right_px as u32, y as u32);
|
|
let out = blend_pixels(dst, aa_color, hcie_blend::BlendMode::Normal, 1.0);
|
|
layer.set_pixel(right_px as u32, y as u32, out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pub fn rotate_shape(shape: &mut VectorShape, delta_degrees: f32) {
|
|
use VectorShape::*;
|
|
match shape {
|
|
Line { angle, .. } | Rect { angle, .. } | Circle { angle, .. }
|
|
| Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. }
|
|
| Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. }
|
|
| Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. }
|
|
| Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } => {
|
|
*angle += delta_degrees * std::f32::consts::PI / 180.0;
|
|
}
|
|
FreePath { .. } => {}
|
|
}
|
|
}
|
|
|
|
pub fn set_shape_angle(shape: &mut VectorShape, new_angle_degrees: f32) {
|
|
use VectorShape::*;
|
|
let rad = new_angle_degrees * std::f32::consts::PI / 180.0;
|
|
match shape {
|
|
Line { angle, .. } | Rect { angle, .. } | Circle { angle, .. }
|
|
| Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. }
|
|
| Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. }
|
|
| Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. }
|
|
| Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } => {
|
|
*angle = rad;
|
|
}
|
|
FreePath { .. } => {}
|
|
}
|
|
}
|
|
|
|
fn get_rounded_rect_pts(x1: f32, y1: f32, x2: f32, y2: f32, r: f32) -> Vec<(f32, f32)> {
|
|
let mut pts = Vec::new();
|
|
let steps = 8;
|
|
|
|
// Top Right
|
|
for i in 0..=steps {
|
|
let a = -std::f32::consts::PI / 2.0 + (std::f32::consts::PI / 2.0) * i as f32 / steps as f32;
|
|
pts.push((x2 - r + r * a.cos(), y1 + r + r * a.sin()));
|
|
}
|
|
// Bottom Right
|
|
for i in 0..=steps {
|
|
let a = 0.0 + (std::f32::consts::PI / 2.0) * i as f32 / steps as f32;
|
|
pts.push((x2 - r + r * a.cos(), y2 - r + r * a.sin()));
|
|
}
|
|
// Bottom Left
|
|
for i in 0..=steps {
|
|
let a = std::f32::consts::PI / 2.0 + (std::f32::consts::PI / 2.0) * i as f32 / steps as f32;
|
|
pts.push((x1 + r + r * a.cos(), y2 - r + r * a.sin()));
|
|
}
|
|
// Top Left
|
|
for i in 0..=steps {
|
|
let a = std::f32::consts::PI + (std::f32::consts::PI / 2.0) * i as f32 / steps as f32;
|
|
pts.push((x1 + r + r * a.cos(), y1 + r + r * a.sin()));
|
|
}
|
|
pts
|
|
}
|
|
|
|
/// Generate the vertices of a speech bubble shape.
|
|
///
|
|
/// **Purpose:**
|
|
/// Creates a closed polygon representing a speech bubble: a main elliptical body
|
|
/// with a triangular tail at the bottom-left.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. Generates 64 steps around a circle to outline the ellipse.
|
|
/// 2. Skips generating normal ellipse points between 78 degrees and 127 degrees (bottom-left region).
|
|
/// 3. In place of the skipped region, inserts three points:
|
|
/// - An inner connection point at the right end of the tail: (cx + rx * 0.2, cy + ry * 0.8)
|
|
/// - The tip of the bubble pointer tail extending outwards: (cx - rx * 0.6, cy + ry * 1.4)
|
|
/// - An outer connection point at the left end of the tail: (cx - rx * 0.6, cy + ry * 0.8)
|
|
/// This ensures a non-overlapping, correctly oriented, clean path.
|
|
///
|
|
/// **Arguments:**
|
|
/// - `cx`: Center X-coordinate.
|
|
/// - `cy`: Center Y-coordinate.
|
|
/// - `rx`: Horizontal radius of the elliptical body.
|
|
/// - `ry`: Vertical radius of the elliptical body.
|
|
///
|
|
/// **Returns:**
|
|
/// A `Vec<(f32, f32)>` representing the ordered vertices of the speech bubble polygon.
|
|
///
|
|
/// **Side Effects / Dependencies:**
|
|
/// None. Pure geometry calculation.
|
|
fn get_bubble_pts(cx: f32, cy: f32, rx: f32, ry: f32) -> Vec<(f32, f32)> {
|
|
let mut pts = Vec::new();
|
|
let steps = 64;
|
|
|
|
for i in 0..=steps {
|
|
let a = std::f32::consts::PI * 2.0 * i as f32 / steps as f32;
|
|
let deg = a.to_degrees();
|
|
|
|
if deg > 78.0 && deg < 127.0 {
|
|
// Skip the ellipse arc that is replaced by the bubble tail
|
|
continue;
|
|
}
|
|
|
|
let px = cx + rx * a.cos();
|
|
let py = cy + ry * a.sin();
|
|
pts.push((px, py));
|
|
|
|
// At the boundary where we transition to the tail
|
|
if i == 13 {
|
|
pts.push((cx + rx * 0.2, cy + ry * 0.8));
|
|
pts.push((cx - rx * 0.6, cy + ry * 1.4));
|
|
pts.push((cx - rx * 0.6, cy + ry * 0.8));
|
|
}
|
|
}
|
|
pts
|
|
}
|
|
|
|
/// Generate the vertices of a crescent shape.
|
|
///
|
|
/// **Purpose:**
|
|
/// Creates a closed polygon representing a crescent moon.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. Generates the outer arc by sampling the ellipse from `-0.2 * PI` (-36°) to `1.2 * PI` (216°).
|
|
/// 2. Defines a smaller inner ellipse that is shifted to the right (`inner_cx = cx + rx * 0.4`).
|
|
/// 3. Generates the inner arc in reverse from `1.2 * PI` back to `-0.2 * PI`.
|
|
/// 4. To prevent gaps and ensure a sharp tip at both ends of the crescent, the difference between
|
|
/// the outer and inner ellipses at the start/end points is calculated. A linear morphing function
|
|
/// based on parameter `t` is applied to shift the inner points so they align perfectly at the tips.
|
|
///
|
|
/// **Arguments:**
|
|
/// - `cx`: Center X-coordinate.
|
|
/// - `cy`: Center Y-coordinate.
|
|
/// - `rx`: Horizontal radius of the outer ellipse.
|
|
/// - `ry`: Vertical radius of the outer ellipse.
|
|
///
|
|
/// **Returns:**
|
|
/// A `Vec<(f32, f32)>` representing the ordered vertices of the crescent polygon.
|
|
///
|
|
/// **Side Effects / Dependencies:**
|
|
/// None. Pure geometry calculation.
|
|
fn get_crescent_pts(cx: f32, cy: f32, rx: f32, ry: f32) -> Vec<(f32, f32)> {
|
|
let mut pts = Vec::new();
|
|
let steps = 64;
|
|
|
|
let start_angle = -std::f32::consts::PI * 0.2;
|
|
let end_angle = std::f32::consts::PI * 1.2;
|
|
|
|
// 1. Generate outer arc points (from start_angle to end_angle)
|
|
for i in 0..=steps {
|
|
let a = start_angle + (end_angle - start_angle) * i as f32 / steps as f32;
|
|
pts.push((cx + rx * a.cos(), cy + ry * a.sin()));
|
|
}
|
|
|
|
// 2. Define the inner ellipse parameters
|
|
let inner_cx = cx + rx * 0.4;
|
|
let inner_cy = cy;
|
|
let inner_rx = rx * 0.7;
|
|
let inner_ry = ry * 0.7;
|
|
|
|
// Calculate the difference at the tips (start and end)
|
|
// Outer start (i = 0):
|
|
let p_start = pts[0];
|
|
// Inner start (i = 0):
|
|
let a_start = start_angle;
|
|
let inner_p_start = (inner_cx + inner_rx * a_start.cos(), inner_cy + inner_ry * a_start.sin());
|
|
let diff_start = (p_start.0 - inner_p_start.0, p_start.1 - inner_p_start.1);
|
|
|
|
// Outer end (i = steps):
|
|
let p_end = pts[steps];
|
|
// Inner end (i = steps):
|
|
let a_end = end_angle;
|
|
let inner_p_end = (inner_cx + inner_rx * a_end.cos(), inner_cy + inner_ry * a_end.sin());
|
|
let diff_end = (p_end.0 - inner_p_end.0, p_end.1 - inner_p_end.1);
|
|
|
|
// 3. Generate inner arc points in reverse, with morphing/correction to meet exactly at the tips
|
|
for i in (0..=steps).rev() {
|
|
let t = i as f32 / steps as f32;
|
|
let a = start_angle + (end_angle - start_angle) * t;
|
|
|
|
let raw_inner_x = inner_cx + inner_rx * a.cos();
|
|
let raw_inner_y = inner_cy + inner_ry * a.sin();
|
|
|
|
// Linearly interpolate the difference/offset
|
|
let offset_x = (1.0 - t) * diff_start.0 + t * diff_end.0;
|
|
let offset_y = (1.0 - t) * diff_start.1 + t * diff_end.1;
|
|
|
|
pts.push((raw_inner_x + offset_x, raw_inner_y + offset_y));
|
|
}
|
|
|
|
pts
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Test that the crescent shape has closed and sharp tips.
|
|
///
|
|
/// **Purpose:**
|
|
/// Verifies that the crescent outer and inner ellipse arcs meet exactly at the two tips
|
|
/// with no gaps, ensuring a pixel-perfect, clean vector crescent.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. Calls `get_crescent_pts` with a center of (100.0, 100.0) and radii of (50.0, 50.0).
|
|
/// 2. Asserts that the point where the outer arc ends (`pts[steps]`) matches the point
|
|
/// where the inner arc starts (`pts[steps + 1]`) within 1e-4 precision.
|
|
/// 3. Asserts that the point where the outer arc starts (`pts[0]`) matches the point
|
|
/// where the inner arc ends (`pts.last()`) within 1e-4 precision.
|
|
#[test]
|
|
fn test_crescent_sharp_tips() {
|
|
let pts = get_crescent_pts(100.0, 100.0, 50.0, 50.0);
|
|
let steps = 64;
|
|
|
|
// Check that the first inner point matches the last outer point
|
|
let outer_end = pts[steps];
|
|
let inner_start = pts[steps + 1];
|
|
assert!((outer_end.0 - inner_start.0).abs() < 1e-4);
|
|
assert!((outer_end.1 - inner_start.1).abs() < 1e-4);
|
|
|
|
// Check that the last inner point matches the first outer point
|
|
let outer_start = pts[0];
|
|
let inner_end = *pts.last().unwrap();
|
|
assert!((outer_start.0 - inner_end.0).abs() < 1e-4);
|
|
assert!((outer_start.1 - inner_end.1).abs() < 1e-4);
|
|
}
|
|
|
|
/// Test that the speech bubble shape generates correct number of points and contains the tail.
|
|
///
|
|
/// **Purpose:**
|
|
/// Verifies that `get_bubble_pts` successfully replaces the bottom-left arc with the
|
|
/// triangular bubble pointer tail without self-intersection or weird index offsets.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. Calls `get_bubble_pts` with a center of (100.0, 100.0) and radii of (50.0, 50.0).
|
|
/// 2. Checks that the returned point list has a valid length.
|
|
/// 3. Verifies that the tail tip coordinate `(cx - rx * 0.6, cy + ry * 1.4)` = `(70.0, 170.0)` is present in the vertices.
|
|
#[test]
|
|
fn test_bubble_tail_points() {
|
|
let pts = get_bubble_pts(100.0, 100.0, 50.0, 50.0);
|
|
|
|
// Tip coordinate should be (100 - 30.0, 100 + 70.0) = (70.0, 170.0)
|
|
let tail_tip = (70.0, 170.0);
|
|
let has_tail_tip = pts.iter().any(|p| (p.0 - tail_tip.0).abs() < 1e-4 && (p.1 - tail_tip.1).abs() < 1e-4);
|
|
assert!(has_tail_tip, "Bubble points should contain the tail tip at (70, 170)");
|
|
|
|
// Inner connection point should be (100 + 10.0, 100 + 40.0) = (110.0, 140.0)
|
|
let tail_inner = (110.0, 140.0);
|
|
let has_tail_inner = pts.iter().any(|p| (p.0 - tail_inner.0).abs() < 1e-4 && (p.1 - tail_inner.1).abs() < 1e-4);
|
|
assert!(has_tail_inner, "Bubble points should contain the tail inner connection at (110, 140)");
|
|
}
|
|
|
|
/// Test that the rotate_point helper works correctly.
|
|
///
|
|
/// **Purpose:**
|
|
/// Verifies that rotating a point (150.0, 100.0) around a center (100.0, 100.0) by 90 degrees
|
|
/// clockwise (pi/2 radians) results in (100.0, 150.0) within 1e-4 precision.
|
|
///
|
|
/// **Logic & Workflow:**
|
|
/// 1. Calls `rotate_point` with point=(150.0, 100.0), center=(100.0, 100.0), angle=std::f32::consts::FRAC_PI_2.
|
|
/// 2. Asserts that output matches (100.0, 150.0) within 1e-4.
|
|
#[test]
|
|
fn test_rotate_point() {
|
|
let (rx, ry) = rotate_point(150.0, 100.0, 100.0, 100.0, std::f32::consts::FRAC_PI_2);
|
|
assert!((rx - 100.0).abs() < 1e-4);
|
|
assert!((ry - 150.0).abs() < 1e-4);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|