Files
hcie-rust-v3.05/hcie-vector/src/lib.rs
T
2026-07-09 02:59:53 +03:00

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);
}
}