diff --git a/Cargo.lock b/Cargo.lock index b125506..2b8b2cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,7 +6,7 @@ version = 3 name = "MFEKglif" version = "2.0.1-beta1" dependencies = [ - "MFEKmath", + "MFEKmath 0.1.2", "arboard", "atty", "backtrace", @@ -62,6 +62,20 @@ dependencies = [ "xmltree", ] +[[package]] +name = "MFEKmath" +version = "0.1.2" +dependencies = [ + "flo_curves", + "glifparser", + "kurbo 0.9.5", + "log", + "plist", + "skia-safe", + "spline 0.3.0", + "xmltree", +] + [[package]] name = "MFEKmath" version = "0.1.2" @@ -1342,7 +1356,7 @@ name = "glifrenderer" version = "0.1.2" source = "git+https://github.com/MFEK/glifrenderer.rlib#044324a0fa7cc90c8cd4b25cac40ffa5494a85ee" dependencies = [ - "MFEKmath", + "MFEKmath 0.1.2 (git+https://github.com/MFEK/math.rlib?branch=main)", "derive_more", "enum-iterator", "enum-unitary", diff --git a/Cargo.toml b/Cargo.toml index 1a8fc42..0455524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,8 +91,8 @@ ctrlc = "3.2" glifparser = { git = "https://github.com/MFEK/glifparser.rlib", branch = "master", features=["skia", "mfek"] } #glifparser = { path = "../glifparser.rlib", features=["skia", "mfek"] } # for development -MFEKmath = { git = "https://github.com/MFEK/math.rlib", branch = "main" } -#MFEKmath = { path = "../math.rlib", features=["skia"]} # for development +#MFEKmath = { git = "https://github.com/MFEK/math.rlib", branch = "main" } +MFEKmath = { path = "../math.rlib", features=["skia"]} # for development pub-mod = { git = "https://github.com/MFEK/pub_mod.rlib" } diff --git a/src/contour_operations/mod.rs b/src/contour_operations/mod.rs index a90f2f1..68b7f30 100644 --- a/src/contour_operations/mod.rs +++ b/src/contour_operations/mod.rs @@ -14,7 +14,7 @@ impl ContourOperationBuild for Option> { fn build(&self, contour: &MFEKContour) -> MFEKOutline { if contour.operation().is_none() { let mut ret: MFEKOutline = MFEKOutline::new(); - ret.push(MFEKContour::new(contour.inner().clone(), None)); + ret.push(contour.clone()); return ret; } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index ee02296..79db061 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -31,6 +31,7 @@ pub mod operations; pub mod selection; pub mod tools; pub mod util; +pub mod tunni; #[macro_use] pub mod macros; diff --git a/src/editor/operations.rs b/src/editor/operations.rs index 6a3259e..5c5269b 100644 --- a/src/editor/operations.rs +++ b/src/editor/operations.rs @@ -43,7 +43,7 @@ impl Editor { continue; } - let build_result = glif_contour.operation().build(glif_contour); + let build_result: Vec> = glif_contour.operation().build(glif_contour); for new_contour in build_result { preview_outline.push(new_contour.to_cubic()); diff --git a/src/editor/tunni.rs b/src/editor/tunni.rs new file mode 100644 index 0000000..612345b --- /dev/null +++ b/src/editor/tunni.rs @@ -0,0 +1,176 @@ +use MFEKmath::{Vector, vec2}; +use flo_curves::Line; +use glifparser::{Point, MFEKPointData, glif::contour::MFEKContourCommon}; + +use crate::{get_point, user_interface::Interface}; + +use super::Editor; + + +// This struct holds the indices of the tunni line in the form of (ci, pi) +#[derive(Clone, Debug)] +pub struct TunniLineInfo { + pub a: (usize, usize), + pub b: (usize, usize), +} + +pub fn get_tunni_point(first: &Point, second: &Point) -> Option { + let a = first.a; + let b = second.b; + + // both of our handles need to be located + match (a, b) { + (glifparser::Handle::At(ax, ay), glifparser::Handle::At(bx, by)) => { + // Construct a line between the handles + let av = vec2![ax, ay]; + let bv = vec2![bx, by]; + + let fv = vec2![first.x, first.y]; + let sv = vec2![second.x, second.y]; + + let pint = flo_curves::line::ray_intersects_ray(&(fv, av), &(sv, bv)); + if let Some(intersection) = pint { + return Some(((av * 2. - fv) + (bv * 2. - sv)) - intersection); + } + }, + _ => { } + } + + None +} + +pub fn construct_tunni_line(first: &Point, second: &Point) -> Option<(Vector, Vector)> { + let a = first.a; + let b = second.b; + + if get_tunni_point(first, second).is_none() { + return None; + } + + // both of our handles need to be located + match (a, b) { + (glifparser::Handle::At(ax, ay), glifparser::Handle::At(bx, by)) => { + // Construct a line between the handles + let av = vec2![ax, ay]; + let bv = vec2![bx, by]; + + let fv = vec2![first.x, first.y]; + let sv = vec2![second.x, second.y]; + + // Calculate vectors from points to handles + let va = av - fv; + let vb = bv - sv; + + // Calculate the line vector and the normal of the line + let lv = sv - fv; + + // Calculate cross products + let cross_product_a = va.cross(lv); + let cross_product_b = vb.cross(lv); + + // If the signs of the cross products are the same, handles lie on the same side of the line + if (cross_product_a >= 0.0 && cross_product_b >= 0.0) || (cross_product_a < 0.0 && cross_product_b < 0.0) { + return Some((av, bv)) + } + }, + _ => { } + } + + None +} + +pub fn get_tunni_line_from_info(v: &Editor, info: &TunniLineInfo) -> Option<(Vector, Vector)> { + // we need to get both of the ci, pi pairs in tunnilineinfo + let a = get_point!(v.get_active_layer_ref(), info.a.0, info.a.1); + let b = get_point!(v.get_active_layer_ref(), info.b.0, info.b.1); + + return construct_tunni_line(a.unwrap().cubic().unwrap(), b.unwrap().cubic().unwrap()); +} + +pub fn get_tunni_point_from_info(v: &Editor, info: &TunniLineInfo) -> Option { + // we need to get both of the ci, pi pairs in tunnilineinfo + let a = get_point!(v.get_active_layer_ref(), info.a.0, info.a.1); + let b = get_point!(v.get_active_layer_ref(), info.b.0, info.b.1); + + return get_tunni_point(a.unwrap().cubic().unwrap(), b.unwrap().cubic().unwrap()); +} + +fn get_distance_from_tunni_line(pos: Vector, line: (Vector, Vector)) -> f64 { + let flo_line = <(Vector, Vector) as Line>::from_points(line.0, line.1); + + let pos_to_line = flo_line.pos_for_point(&pos); + let clamped_t = pos_to_line.clamp(0., 1.); + let clamped_position = flo_line.point_at_pos(clamped_t); + + return clamped_position.distance(pos); +} + +pub fn get_closest_tunni_line(v: &Editor, i: &Interface) -> Option { + let mut closest_distance = f64::INFINITY; + let mut closest_tunni: Option = None; + + let mp = vec2![i.mouse_info.position.0, i.mouse_info.position.1]; + + for (contour_idx, contour) in v.get_active_layer_ref().outline.iter().enumerate() { + if let Some(cubic) = contour.cubic() { + for (point_idx, pair) in cubic.windows(2).enumerate() { + if let Some(line) = construct_tunni_line(&pair[0], &pair[1]) { + let tunni_point = get_tunni_point(&pair[0], &pair[1]).unwrap(); + let point_distance = tunni_point.distance(mp); + let distance = get_distance_from_tunni_line(mp, line); + + if distance < closest_distance || point_distance < closest_distance { + closest_distance = f64::min(closest_distance, distance); + closest_tunni = Some(TunniLineInfo { a: (contour_idx, point_idx), b: (contour_idx, point_idx + 1) }); + } + } + } + + if cubic.is_closed() && cubic.len() > 1 { + if let Some(line) = construct_tunni_line( &cubic.last().unwrap(), &cubic.first().unwrap()) { + let distance = get_distance_from_tunni_line(mp, line); + + let point_idx = cubic.len() - 1; + if distance < closest_distance { + closest_distance = distance; + closest_tunni = Some(TunniLineInfo { a: (contour_idx, point_idx), b: (contour_idx, 0) }); + } + } + } + } + } + + return closest_tunni; +} + +pub enum Tunni { + Point, + Line +} + +const MIN_DISTANCE_FOR_CLICK: f64 = 5.; + +pub fn clicked_tunni_point_or_line(v: &Editor, i: &Interface) -> Option<(TunniLineInfo, Tunni)> { + let closest_point_or_line = get_closest_tunni_line(v, i); + + if let Some(tunni_info) = closest_point_or_line { + let tunni_point = get_tunni_point_from_info(v, &tunni_info).expect("get_closest_tunni should always return a valid tunni line."); + let tunni_line = get_tunni_line_from_info(v, &tunni_info).expect("get_closest_tunni should always return a valid tunni line."); + + let point_distance = Vector::distance(tunni_point, i.mouse_info.position.into()); + let line_distance = get_distance_from_tunni_line(i.mouse_info.position.into(), tunni_line); + + let closest = f64::min(point_distance, line_distance); + + println!("FOUND CLOSEST {0}", closest); + if closest < MIN_DISTANCE_FOR_CLICK * (1. / i.viewport.factor) as f64 { + if closest == point_distance { + return Some((tunni_info, Tunni::Point)) + } else { + return Some((tunni_info, Tunni::Line)) + } + } + } + + None +} \ No newline at end of file diff --git a/src/editor/util.rs b/src/editor/util.rs index 153056a..8beeaa5 100644 --- a/src/editor/util.rs +++ b/src/editor/util.rs @@ -1,18 +1,23 @@ // This file is mainly utilities that are common use cases for the editor, but don't necessarily need to be // in Editor. -use crate::user_interface::Interface; +use std::collections::btree_set::Intersection; + +use crate::{user_interface::{Interface, MouseInfo}, get_point}; +use MFEKmath::{vec2, subdivide::Subdivide}; use crate::{get_contour_len, get_point_mut}; +use egui::vec2; use flo_curves::{ bezier::{solve_curve_for_t_along_axis, Curve as FloCurve}, - geo::Coord2, + geo::Coord2, line::Line, }; -use glifparser::WhichHandle; +use glifparser::{WhichHandle, MFEKPointData, Point}; use glifrenderer::constants::{POINT_RADIUS, POINT_STROKE_THICKNESS}; use skia_safe::Contains; use skia_safe::Point as SkPoint; use skia_safe::Rect as SkRect; -use MFEKmath::{Bezier, Piecewise, Primitive as MathPrimitive}; +use MFEKmath::{Bezier, Piecewise, Vector}; +use flo_curves::line::Line2D; use super::Editor; use glifparser::glif::mfek::contour::MFEKContourCommon; @@ -198,7 +203,7 @@ pub fn nearest_point_on_curve( contour_idx = Some(cx); seg_idx = Some(bx); - let subdivisions = MathPrimitive::subdivide(mbezier, ct); + let subdivisions = Subdivide::split(mbezier, ct); if let Some(subdivisions) = subdivisions { h1 = Some(subdivisions.0.to_control_points()[2]); h2 = Some(subdivisions.1.to_control_points()[1]); diff --git a/src/render/measure.rs b/src/render/measure.rs new file mode 100644 index 0000000..cfe8223 --- /dev/null +++ b/src/render/measure.rs @@ -0,0 +1,214 @@ +use MFEKmath::{Vector, vec2}; +use glifrenderer::constants::OUTLINE_STROKE_THICKNESS; +use skia_safe::{Canvas, Paint, Path, Point, Rect, Font, Matrix}; + +use crate::{editor::Editor, user_interface::{Interface, MouseInfo}, tools::cut::{Cut, Intersection}, constants}; +use glifrenderer::constants::MEASURE_STROKE; + +pub struct Measure { + pub start_point: Option<(f32, f32)>, + pub end_point: Option<(f32, f32)>, + pub enabled: bool, +} + +impl Measure { + pub fn draw_line(&self, i: &Interface, v: &Editor, canvas: &mut Canvas, factor: f32) { + if !self.enabled { return } + if self.start_point.is_none() || self.end_point.is_none() { return } + + let start_point: Vector = self.start_point.unwrap().into(); + let end_point: Vector = self.end_point.unwrap().into(); + + // Calculate distance and angle + let delta = end_point - start_point; + let distance = delta.distance(vec2![0., 0.]); + let angle_radians = delta.y.atan2(delta.x); + let angle_degrees = angle_radians.to_degrees(); + + // Set up paint properties + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(skia_safe::PaintStyle::Stroke); + paint.set_stroke_width(OUTLINE_STROKE_THICKNESS * (1. / factor)); + + // Line from start_point to mouse_position + Cut::draw_line(i, v, canvas, &self.start_point, &self.end_point); + + // Horizontal line (unit vector guided) + let mut path = Path::new(); + let end_point_horizontal = vec2!(start_point.x + distance, start_point.y); + path.rewind(); // Clear the path to reuse it + path.move_to(start_point.to_skia_point()); + path.line_to(end_point_horizontal.to_skia_point()); + canvas.draw_path(&path, &paint); + + // Arc illustrating the angle + let radius = distance / 2.0; // This can be adjusted based on desired arc size. + path.rewind(); // Clear the path to reuse it + path.move_to(end_point_horizontal.to_skia_point()); + + // Define the bounding box for the circle, centered on start_point + let bounding_box = Rect::from_xywh( + (start_point.x - distance) as f32, + (start_point.y - distance) as f32, + 2.0 * distance as f32, + 2.0 * distance as f32 + ); + + // Now draw the arc + path.arc_to( + &bounding_box, + 0.0, + angle_degrees as f32, + false + ); + canvas.draw_path(&path, &paint); + + // Display the overall distance: + // Choose a position to display the distance at the end of the line, slightly offset from the endpoint. + let offset = 20.0; + let adjusted_offset = if delta.x >= 0.0 { offset } else { -offset }; // Offset to the right if dx is positive, else to the left + let distance_text_position = ( + end_point.x as f32 + adjusted_offset / factor, + end_point.y as f32 + ); + + // Use draw_text function to display the distance at the chosen position + self.draw_text(canvas, &format!("{:.2}", distance), distance_text_position, 0.0, 16., factor); + + // Display the angle beneath the horizontal line + let text_position = vec2!(start_point.x + distance/2.0, start_point.y + 10.0); + self.draw_text(canvas, &format!("{:.2}°", angle_degrees), text_position.into(), 0.0, 16., factor); + + let intersections = Cut::find_intersections(&self.start_point, &self.end_point, v, true); + self.draw_intersection_distances(canvas, intersections.as_slice(), factor); + + if intersections.len() > 1 { + let first_intersection = intersections.first().unwrap(); + let last_intersection = intersections.last().unwrap(); + let fcoords = (first_intersection.coords.0 as f32, first_intersection.coords.1 as f32); + let lcoords = (last_intersection.coords.0 as f32, last_intersection.coords.1 as f32); + self.draw_bracket(canvas, fcoords, lcoords, -20.0, factor); // 20.0 is the offset from the main line, adjust as needed + } + } + + fn draw_text(&self, canvas: &mut Canvas, text: &str, position: (f32, f32), angle: f32, size: f32, factor: f32) { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + + let mut font = Font::default(); + font.set_size(size / factor); + + // Calculate text width and adjust the starting position to center the text + let text_bounds = font.measure_str(text, Some(&paint)); + let adjusted_position = ( + position.0 - text_bounds.1.width() / 2.0, + position.1 + text_bounds.1.height() / 2.0 // Adjust for vertical centering + ); + + // Create & apply rotation matrix + let mut transform_matrix = Matrix::new_identity(); + transform_matrix.pre_translate(Point::from(adjusted_position)); + transform_matrix.pre_scale((1.0, -1.0), None); // Flip vertically + transform_matrix.pre_rotate(angle, None); // Apply rotation + canvas.save(); + canvas.concat(&transform_matrix); // Concatenate with existing matrix + + canvas.draw_str(text, Point::from((0., 0.)), &font, &paint); + + // Reset matrix to identity after drawing to avoid affecting other draws + canvas.restore(); + } + + + fn draw_intersection_distances(&self, canvas: &mut Canvas, intersections: &[Intersection], factor: f32) { + let mut paint = Paint::default(); + paint.set_color(MEASURE_STROKE); + paint.set_anti_alias(true); + + let mut font = Font::default(); + font.set_size(16. / factor); + + for window in intersections.windows(2) { + let start = Point::from((window[0].coords.0 as f32, window[0].coords.1 as f32)); + let end = Point::from((window[1].coords.0 as f32, window[1].coords.1 as f32)); + + // Calculate distance + let dx = end.x - start.x; + let dy = end.y - start.y; + let distance = (dx.powi(2) + dy.powi(2)).sqrt(); + + // Find midpoint for the text + let midpoint = ((start.x + end.x) / 2.0, (start.y + end.y) / 2.0); + + let text_angle = dy.atan2(dx).to_degrees(); + + // Draw the distance text + let text = format!("{:.2}", distance); + self.draw_text(canvas, &text, midpoint, -text_angle, 8., factor) + } + } + + fn draw_bracket(&self, canvas: &mut Canvas, start: (f32, f32), end: (f32, f32), offset: f32, factor: f32) { + // Calculate the direction vector of the main line + let dx = end.0 - start.0; + let dy = end.1 - start.1; + + // Calculate a normalized perpendicular vector + let magnitude = (dx.powi(2) + dy.powi(2)).sqrt(); + let perp_dx = -dy / magnitude; + let perp_dy = dx / magnitude; + + // Calculate the endpoints of the bracket on the parallel line + let bracket_start = ( + start.0 + perp_dx * offset / factor, + start.1 + perp_dy * offset / factor, + ); + let bracket_end = ( + end.0 + perp_dx * offset / factor, + end.1 + perp_dy * offset / factor, + ); + + // Set up paint properties for drawing + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(skia_safe::PaintStyle::Stroke); + paint.set_stroke_width(OUTLINE_STROKE_THICKNESS * (1. / factor)); + + // Draw the parallel line for the bracket + let mut path = Path::new(); + path.move_to(Point::from(bracket_start)); + path.line_to(Point::from(bracket_end)); + canvas.draw_path(&path, &paint); + + path.rewind(); + path.move_to(Point::from(bracket_start)); + path.line_to(Point::from(start)); + path.move_to(Point::from(bracket_end)); + path.line_to(Point::from(end)); + canvas.draw_path(&path, &paint); + + // Calculate the distance between start and end intersections + let distance = ((end.0 - start.0).powi(2) + (end.1 - start.1).powi(2)).sqrt(); + + // Calculate the midpoint of the bracket for text placement + let midpoint = ( + (bracket_start.0 + bracket_end.0) / 2.0, + (bracket_start.1 + bracket_end.1) / 2.0 + ); + + // Calculate the text's offset position so it's not obstructed by the bracket line + let text_offset = -30.; // Adjust this value as needed + let text_position = ( + midpoint.0 + perp_dx * text_offset / factor, + midpoint.1 + perp_dy * text_offset / factor, + ); + + // The rotation angle is based on dy and dx which were calculated initially + let rotation_angle = dy.atan2(dx).to_degrees(); + + // Use draw_text function with rotation to display the distance + // at the offset position and aligned with the bracket + self.draw_text(canvas, &format!("{:.2}", distance), text_position, -rotation_angle, 12., factor); + } +} diff --git a/src/render/mod.rs b/src/render/mod.rs index 4e16260..7be2066 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -14,6 +14,9 @@ use skia_safe::{self as skia, Canvas}; use crate::user_interface::PAPER_DRAW_GUIDELINES; use crate::{editor::Editor, user_interface::Interface}; +mod speed_visualization; +pub mod measure; + pub fn render_frame(v: &mut Editor, i: &mut Interface, canvas: &mut Canvas) { canvas.save(); @@ -90,10 +93,12 @@ pub fn render_frame(v: &mut Editor, i: &mut Interface, canvas: &mut Canvas) { glifrenderer::glyph::draw(canvas, v.preview.as_ref().unwrap(), &i.viewport, None); + speed_visualization::draw_velocity(v, i, canvas); v.with_glyph(|glyph| { // Cache component rects and flattened outline on MFEKGlif draw_components(glyph, &i.viewport, canvas); }); + i.measure.draw_line(i, v, canvas, i.viewport.factor); // TODO: let _path = glyph::draw_previews(v, canvas); diff --git a/src/render/speed_visualization.rs b/src/render/speed_visualization.rs new file mode 100644 index 0000000..7cb7e8f --- /dev/null +++ b/src/render/speed_visualization.rs @@ -0,0 +1,103 @@ +use MFEKmath::{Piecewise, Bezier, ArcLengthParameterization, Parameterization, Evaluate, Vector, mfek::ResolveCubic}; +use glifrenderer::toggles::PreviewMode; +use skia_safe::{Canvas, Color, Paint}; + +use crate::{editor::Editor, user_interface::Interface, get_contour}; + +pub fn draw_velocity(v: &Editor, i: &Interface, canvas: &mut Canvas) { + if i.viewport.preview_mode == PreviewMode::Paper { return }; + if i.curvature_vis == false { return }; + + if let Some((cidx, _)) = v.selected_point() { + let selected_contour = &get_contour!(v.get_active_layer_ref(), cidx); + + let scaling_factor: f64 = 200.; + let quality = 6.; + + let piecewise: Piecewise = selected_contour.to_cubic().into(); + let mut first = false; + for bez in piecewise.segs { + let arclen_param = ArcLengthParameterization::from(&bez, 1000); + let total_len = arclen_param.get_total_arclen(); + + let samples = (quality * total_len as f32 * i.viewport.factor) as u32; + let max_velocity_magnitude = 0.2; + let min_velocity_magnitude = 0.0; + + // Function to map velocity magnitude to a color + let alpha = 32u8; // Semi-transparent + let map_velocity_to_color = |velocity_magnitude: f64| -> Color { + let normalized = (velocity_magnitude.abs() - min_velocity_magnitude) / (max_velocity_magnitude - min_velocity_magnitude); + let red = (normalized * 255.0) as u8; + let blue = ((1.0 - normalized) * 255.0) as u8; + Color::from_argb(alpha, red, 0, blue) // Gradient from blue to red + }; + + let derivative = bezier_derivative(&bez); + // Second pass to draw the lines with gradient colors + let start = if first { 0 } else { 1 }; + first = false; + for sample in start..=samples { + let t = sample as f64 / samples as f64; + let t = arclen_param.parameterize(t); + + // Calculate the first and second derivatives + let first_derivative = eval_quadratic_bezier(derivative.0, derivative.1, derivative.2, t); + let second_derivative = eval_linear_bezier(bezier_second_derivative(&bez).0, bezier_second_derivative(&bez).1, t); + + // Calculate the curvature + let curvature = (first_derivative.x * second_derivative.y - first_derivative.y * second_derivative.x) / + (first_derivative.x.powi(2) + first_derivative.y.powi(2)).powf(1.5); + + let scaled_curvature = curvature.signum() * curvature.abs().sqrt(); + let clamped_curvature = scaled_curvature.signum() * scaled_curvature.abs().min(max_velocity_magnitude); + let normal = Vector { x: -first_derivative.y, y: first_derivative.x }.normalize(); + + // Scale the normal by the curvature + let scaled_normal = -normal * scaled_curvature * scaling_factor; + + let color = map_velocity_to_color(clamped_curvature); + + // Draw the normal line + let point_on_curve = bez.at(t); + let start = point_on_curve; + let end = point_on_curve + scaled_normal; + let mut normal_paint = Paint::default(); + normal_paint.set_color(color); + normal_paint.set_stroke_width(1.0 / i.viewport.factor); + normal_paint.set_anti_alias(true); + canvas.draw_line(start.to_skia_point(), end.to_skia_point(), &normal_paint); + } + + } + } +} + + +// Function to calculate the derivative of a cubic Bézier curve +fn bezier_derivative(bez: &Bezier) -> (Vector, Vector, Vector) { + // For a cubic Bézier curve, the derivative is a quadratic curve with these control points + let dp0 = (bez.w2 - bez.w1) * 3.0; + let dp1 = (bez.w3 - bez.w2) * 3.0; + let dp2 = (bez.w4 - bez.w3) * 3.0; + (dp0, dp1, dp2) +} + +// Function to evaluate a quadratic Bézier curve at t +fn eval_quadratic_bezier(p0: Vector, p1: Vector, p2: Vector, t: f64) -> Vector { + let one_minus_t = 1.0 - t as f64; + p0 * one_minus_t.powi(2) + p1 * 2.0 * one_minus_t * t + p2 * t.powi(2) +} + +// Function to calculate the second derivative of a cubic Bézier curve +fn bezier_second_derivative(bez: &Bezier) -> (Vector, Vector) { + // The second derivative of a cubic Bézier curve is a linear curve with these control points + let ddp0 = (bez.w3 - bez.w2 * 2.0 + bez.w1) * 6.0; + let ddp1 = (bez.w4 - bez.w3 * 2.0 + bez.w2) * 6.0; + (ddp0, ddp1) +} + +// Function to evaluate a linear Bézier curve at t (which is essentially linear interpolation) +fn eval_linear_bezier(p0: Vector, p1: Vector, t: f64) -> Vector { + p0 * (1.0 - t as f64) + p1 * t as f64 +} \ No newline at end of file diff --git a/src/tool_behaviors/move_tunni_line.rs b/src/tool_behaviors/move_tunni_line.rs new file mode 100644 index 0000000..b8ac203 --- /dev/null +++ b/src/tool_behaviors/move_tunni_line.rs @@ -0,0 +1,92 @@ +use crate::{get_point_mut, editor::tunni::{TunniLineInfo, construct_tunni_line, get_tunni_line_from_info}}; + +use super::prelude::*; +use MFEKmath::Vector; +use glifparser::glif::{mfek::contour::MFEKContourCommon}; +#[derive(Clone, Debug)] +pub struct MoveTunniLine { + mouse_info: MouseInfo, + tunni_info: TunniLineInfo, + line: (Vector, Vector) // The starting line from first.a(c1) -> second.b(c2) +} + +impl ToolBehavior for MoveTunniLine { + fn event(&mut self, v: &mut Editor, i: &mut Interface, event: EditorEvent) { + match event { + EditorEvent::MouseEvent { event_type, mouse_info } => { + match event_type { + MouseEventType::Moved => self.mouse_moved(v, i, mouse_info), + MouseEventType::Released => v.pop_behavior(), + _ => {} + } + } + _ => {} + } + } +} + +impl MoveTunniLine { + pub fn new(mouse_info: MouseInfo, tunni_info: TunniLineInfo, line: (Vector, Vector)) -> Self { + MoveTunniLine { + mouse_info, + tunni_info, + line + } + } + + pub fn mouse_moved(&mut self, v: &mut Editor, i: &mut Interface, mouse_info: MouseInfo) { + // we stop the drag when there's no longer a well formed tunni point/line + if get_tunni_line_from_info(v, &self.tunni_info).is_none() { + v.pop_behavior(); + } + + let a_point = + get_point!(v.get_active_layer_ref(), self.tunni_info.a.0, self.tunni_info.a.1) + .unwrap() + .cubic() + .expect("A tunni line info should never index to a non-cubic point!") + .clone(); + + let b_point = + get_point!(v.get_active_layer_ref(), self.tunni_info.b.0, self.tunni_info.b.1) + .unwrap() + .cubic() + .expect("A tunni line info should never index to a non-cubic point!") + .clone(); + + let a = Vector::from_point(&a_point); + let b = Vector::from_point(&b_point); + + let c1 = Vector::from_handle(&a_point, a_point.a); + let c2 = Vector::from_handle(&b_point, b_point.b); + + let starting_mp: Vector = self.mouse_info.position.into(); + let current_mp: Vector = mouse_info.position.into(); + + let delta = current_mp - starting_mp; + let offset_c1 = self.line.0 + delta; + let offset_c2 = self.line.1 + delta; + + let a_intersect = flo_curves::line::ray_intersects_ray(&(a, c1), &(offset_c2, offset_c1)); + let b_intersect = flo_curves::line::ray_intersects_ray(&(b, c2), &(offset_c1, offset_c2),); + + match (a_intersect, b_intersect) { + (Some(c1), Some(c2)) => { + v.begin_modification("Move tunni point.", true); + + get_point_mut!(v.get_active_layer_mut(), self.tunni_info.a.0, self.tunni_info.a.1) + .unwrap() + .set_handle(WhichHandle::A, Handle::At(c1.x as f32, c1.y as f32)); + + get_point_mut!(v.get_active_layer_mut(), self.tunni_info.b.0, self.tunni_info.b.1) + .unwrap() + .set_handle(WhichHandle::B, Handle::At(c2.x as f32, c2.y as f32)); + + v.end_modification(); + }, + _ => {} + } + + + } +} \ No newline at end of file diff --git a/src/tool_behaviors/move_tunni_point.rs b/src/tool_behaviors/move_tunni_point.rs new file mode 100644 index 0000000..ae0c42f --- /dev/null +++ b/src/tool_behaviors/move_tunni_point.rs @@ -0,0 +1,91 @@ +use crate::{get_point_mut, editor::tunni::{TunniLineInfo, construct_tunni_line, get_tunni_line_from_info}}; + +use super::prelude::*; +use MFEKmath::Vector; +use glifparser::glif::{mfek::contour::MFEKContourCommon}; +#[derive(Clone, Debug)] +pub struct MoveTunniPoint { + mouse_info: MouseInfo, + tunni_info: TunniLineInfo +} + +impl ToolBehavior for MoveTunniPoint { + fn event(&mut self, v: &mut Editor, i: &mut Interface, event: EditorEvent) { + match event { + EditorEvent::MouseEvent { event_type, mouse_info } => { + match event_type { + MouseEventType::Moved => self.mouse_moved(v, i, mouse_info), + MouseEventType::Released => v.pop_behavior(), + _ => {} + } + } + _ => {} + } + } +} + +impl MoveTunniPoint { + pub fn new(mouse_info: MouseInfo, tunni_info: TunniLineInfo) -> Self { + MoveTunniPoint { + mouse_info, + tunni_info + } + } + + pub fn mouse_moved(&mut self, v: &mut Editor, i: &mut Interface, mouse_info: MouseInfo) { + // we stop the drag when there's no longer a well formed tunni point/line + if get_tunni_line_from_info(v, &self.tunni_info).is_none() { + v.pop_behavior(); + } + + let a_point = + get_point!(v.get_active_layer_ref(), self.tunni_info.a.0, self.tunni_info.a.1) + .unwrap() + .cubic() + .expect("A tunni line info should never index to a non-cubic point!") + .clone(); + + let b_point = + get_point!(v.get_active_layer_ref(), self.tunni_info.b.0, self.tunni_info.b.1) + .unwrap() + .cubic() + .expect("A tunni line info should never index to a non-cubic point!") + .clone(); + + // the new tunni point + let t = Vector::from_components(mouse_info.position.0 as f64, mouse_info.position.1 as f64); + let a = Vector::from_point(&a_point); + let b = Vector::from_point(&b_point); + + let c1 = Vector::from_handle(&a_point, a_point.a); + let c2 = Vector::from_handle(&b_point, b_point.b); + + let ha = (a + t) / 2.; + let hb = (b + t) / 2.; + + let ha1 = ha + c2 - b; + let hb1 = hb + c1 - a; + + let a_intersect = flo_curves::line::ray_intersects_ray(&(a, c1), &(ha1, ha)); + let b_intersect = flo_curves::line::ray_intersects_ray(&(b, c2), &(hb1, hb),); + + match (a_intersect, b_intersect) { + (Some(c1), Some(c2)) => { + v.begin_modification("Move tunni point.", true); + + get_point_mut!(v.get_active_layer_mut(), self.tunni_info.a.0, self.tunni_info.a.1) + .unwrap() + .set_handle(WhichHandle::A, Handle::At(c1.x as f32, c1.y as f32)); + + get_point_mut!(v.get_active_layer_mut(), self.tunni_info.b.0, self.tunni_info.b.1) + .unwrap() + .set_handle(WhichHandle::B, Handle::At(c2.x as f32, c2.y as f32)); + + v.end_modification(); + }, + _ => {} + } + + + } +} \ No newline at end of file diff --git a/src/tools/cut.rs b/src/tools/cut.rs new file mode 100644 index 0000000..ff51b84 --- /dev/null +++ b/src/tools/cut.rs @@ -0,0 +1,306 @@ +use MFEKmath::mfek::ResolveCubic; +use MFEKmath::quadbezier::QuadBezier; +use MFEKmath::subdivide::Subdivide; +use flo_curves::bezier::curve_intersects_line; +use flo_curves::{ + bezier::Curve as FloCurve, + geo::Coord2, +}; +use flo_curves::{BezierCurveFactory, Line, Coordinate2D}; +use glifparser::MFEKPointData; +use glifparser::glif::contour_operations::ContourOperation; +use glifparser::glif::inner::MFEKContourInner; +use glifparser::glif::{MFEKOutline, MFEKContour}; +use glifparser::glif::contour::MFEKContourCommon; +use glifrenderer::constants::{self, OUTLINE_STROKE_THICKNESS}; +use kurbo::{PathSeg, QuadBez, ParamCurve}; +use skia_safe::{Canvas, Paint, Path, Point}; +use MFEKmath::{Piecewise, Bezier, Evaluate}; + +use crate::editor::Editor; +use crate::user_interface::Interface; + +use super::prelude::*; + +#[derive(Clone, Debug)] +pub struct Cut { + start_point: Option<(f32, f32)>, +} + +pub struct Intersection { + pub ci: usize, + pub bi: usize, + pub t: f64, + pub line_t: f64, + pub coords: (f64, f64), +} + +impl Tool for Cut { + #[rustfmt::skip] + fn event(&mut self, v: &mut Editor, _i: &mut Interface, event: EditorEvent) { + if let EditorEvent::MouseEvent { mouse_info, event_type } = event { + match event_type { + MouseEventType::Pressed => self.mouse_pressed(v, mouse_info), + MouseEventType::Released => self.mouse_released(v, mouse_info), + _ => (), + } + } + } + + fn draw(&mut self, v: &Editor, i: &Interface, canvas: &mut Canvas) { + Self::draw_line(i, v, canvas, &self.start_point, &Some(i.mouse_info.position)); + } +} + +impl Cut { + pub fn new() -> Self { + Self { start_point: None } + } + + fn mouse_pressed(&mut self, _v: &Editor, mouse_info: MouseInfo) { + self.start_point = Some(mouse_info.position); + } + + fn mouse_released(&mut self, v: &mut Editor, mouse_info: MouseInfo) { + let intersections = Self::find_intersections(&self.start_point, &Some(mouse_info.position.into()), v, false); + Self::split_at_intersections(v, &intersections); + self.start_point = None; + } + + pub fn find_intersections(start_point: &Option<(f32, f32)>, end_point: &Option<(f32, f32)>, v: &Editor, non_cubic: bool) -> Vec { + let mut intersections = vec![]; + if start_point.is_none() || end_point.is_none() { return intersections } + + let start_point = start_point.unwrap(); + let end_point = end_point.unwrap(); + + fn intersect_cubic(pw: Piecewise, ci: usize, intersections: &mut Vec, sp: (f32, f32), ep: (f32, f32)) { + for (bi, bez) in pw.segs.iter().enumerate() { + let flo_bez = FloCurve::from_points( + Coord2(bez.w1.x, bez.w1.y), + ( + Coord2(bez.w2.x, bez.w2.y), + Coord2(bez.w3.x, bez.w3.y), + ), + Coord2(bez.w4.x, bez.w4.y), + ); + + let flo_line = <(Coord2, Coord2) as Line>::from_points(sp.into(), ep.into()); + + for (curve_t, line_t, coordinate) in curve_intersects_line(&flo_bez, &flo_line) { + let intersection = Intersection { + ci: ci, + t: curve_t, + line_t: line_t, + coords: (coordinate.x(), coordinate.y()), + bi: bi, + }; + + intersections.push(intersection); + } + } + } + + // first we've gotta look at the active layer and find intersections between the line we drew + // and the contours of the active layer + for (ci, c) in v.get_active_layer_ref().outline.iter().enumerate() { + match c.get_type() { + glifparser::glif::inner::MFEKContourInnerType::Cubic => { + let pw: Piecewise = Piecewise::from(c.cubic().unwrap()); + intersect_cubic(pw, ci, &mut intersections, start_point, end_point); + }, + glifparser::glif::inner::MFEKContourInnerType::Quad => { + let start_point = (start_point.0 as f64, start_point.1 as f64); + let end_point = (end_point.0 as f64, end_point.1 as f64); + let pw: Piecewise = Piecewise::from(c.quad().unwrap()); + + for (bi, bez) in pw.segs.iter().enumerate() { + let quadbez = QuadBez::new(bez.w1, bez.w2, bez.w3); + let quad = PathSeg::Quad(quadbez); + let line = kurbo::Line::new(start_point, end_point); + let line_intersections = PathSeg::intersect_line(&quad, line); + + for li in line_intersections { + let line_t = li.line_t; + let curve_t = li.segment_t; + + let coordinate = quadbez.eval(curve_t); + + let intersection = Intersection { + ci: ci, + t: curve_t, + line_t: line_t, + coords: (coordinate.x, coordinate.y), + bi: bi, + }; + + intersections.push(intersection); + } + } + }, + _ => { + let pw: Piecewise = Piecewise::from(c.to_cubic().cubic().unwrap()); + intersect_cubic(pw, ci, &mut intersections, start_point, end_point); + }, + } + }; + + intersections.sort_by(|a, b| a.line_t.partial_cmp(&b.line_t).unwrap_or(std::cmp::Ordering::Equal)); + + intersections + + } + + pub fn split_at_intersections(v: &mut Editor, intersections: &[Intersection]) { + if intersections.len() == 0 { return } + let mut new_outline = MFEKOutline::new(); + + for (ci, contour) in v.get_active_layer_ref().outline.iter().enumerate() { + let mut added_cuts = Vec::new(); + let previous_operation = contour.operation.clone(); + + match contour.get_type() { + glifparser::glif::inner::MFEKContourInnerType::Cubic => { + let mut new_beziers = Vec::new(); + let pw: Piecewise = Piecewise::from(contour.cubic().unwrap()); + + for (bi, bez) in pw.segs.iter().enumerate() { + // Find intersections relevant for the current Bezier + let split_times_for_bezier: Vec<_> = intersections.iter() + .filter(|&intersection| { + intersection.ci == ci && intersection.bi == bi + }) + .map(|intersection| intersection.t) + .collect(); + + if !split_times_for_bezier.is_empty() { + let mut sorted_times = split_times_for_bezier.clone(); + sorted_times.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let split_beziers = bez.split_at_multiple_t(sorted_times); + + + for _ in 0 .. split_times_for_bezier.len() { + added_cuts.push(bi); + } + + // Add the split beziers to the new beziers list + new_beziers.extend(split_beziers); + } else { + // If there were no intersections for this bezier, just add it as is + new_beziers.push(bez.clone()); + } + } + + + // Construct a new contour from the new beziers + let mut new_contour: MFEKContour = Piecewise::new(new_beziers, None).to_contour().into(); + new_contour.set_operation(previous_operation); + + for cut in added_cuts { + new_contour.operation_mut().insert_op(cut) + } + + new_outline.push(new_contour); + }, + glifparser::glif::inner::MFEKContourInnerType::Quad => { + let mut new_beziers = Vec::new(); + let pw: Piecewise = Piecewise::from(contour.quad().unwrap()); + + for (bi, bez) in pw.segs.iter().enumerate() { + // Find intersections relevant for the current Bezier + let split_times_for_bezier: Vec<_> = intersections.iter() + .filter(|&intersection| { + intersection.ci == ci && intersection.bi == bi + }) + .map(|intersection| intersection.t) + .collect(); + + if !split_times_for_bezier.is_empty() { + let mut sorted_times = split_times_for_bezier.clone(); + sorted_times.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let split_beziers = bez.split_at_multiple_t(sorted_times); + + + for _ in 0 .. split_times_for_bezier.len() { + added_cuts.push(bi); + } + + // Add the split beziers to the new beziers list + new_beziers.extend(split_beziers); + } else { + // If there were no intersections for this bezier, just add it as is + new_beziers.push(bez.clone()); + } + } + + + // Construct a new contour from the new beziers + let new_contour = Piecewise::new(new_beziers, None).to_contour(); + let mut mfek_contour = MFEKContour::new(MFEKContourInner::Quad(new_contour), previous_operation); + + match contour.is_closed() { + true => mfek_contour.set_closed(), + false => mfek_contour.set_open(), + } + + for cut in added_cuts { + mfek_contour.operation_mut().insert_op(cut) + } + + new_outline.push(mfek_contour); + }, + glifparser::glif::inner::MFEKContourInnerType::Hyper => {}, + } + } + + v.begin_modification("Cut", false); + v.get_active_layer_mut().outline = new_outline; + v.end_modification(); + } + + + pub fn draw_line(i: &Interface, v: &Editor, canvas: &mut Canvas, start_point: &Option<(f32, f32)>, end_point: &Option<(f32, f32)>) { + let mut path = Path::new(); + let mut paint = Paint::default(); + let factor = i.viewport.factor; + + if let (Some(measure_from), Some(measure_to)) = (start_point, end_point) { + let skpath_start = Point::new(measure_from.0 as f32, measure_from.1 as f32); + let skpath_end = Point::new( + measure_to.0 as f32, + measure_to.1 as f32, + ); + + path.move_to(skpath_start); + path.line_to(skpath_end); + + paint.set_color(constants::MEASURE_STROKE); + paint.set_anti_alias(true); + paint.set_style(skia_safe::PaintStyle::Stroke); + paint.set_stroke_width(OUTLINE_STROKE_THICKNESS * (1. / factor)); + canvas.draw_path(&path, &paint); + + // Draw X's at the intersections + let intersections = Self::find_intersections(&start_point, &end_point, v, true); // Assuming you have access to the editor here + for intersection in intersections { + let intersection_point = Point::new(intersection.coords.0 as f32, intersection.coords.1 as f32); + + let x_size = 10.0 / factor; // Adjust this value for the desired size of the X + + // Drawing the two lines of the X + path.reset(); + let start1 = Point::new(intersection_point.x - x_size, intersection_point.y - x_size); + let end1 = Point::new(intersection_point.x + x_size, intersection_point.y + x_size); + path.move_to(start1); + path.line_to(end1); + + let start2 = Point::new(intersection_point.x + x_size, intersection_point.y - x_size); + let end2 = Point::new(intersection_point.x - x_size, intersection_point.y + x_size); + path.move_to(start2); + path.line_to(end2); + + canvas.draw_path(&path, &paint); + } + } + } +} \ No newline at end of file diff --git a/src/tools/measure.rs b/src/tools/measure.rs index e216680..e5a584e 100644 --- a/src/tools/measure.rs +++ b/src/tools/measure.rs @@ -1,95 +1,55 @@ -use glifrenderer::constants::{self, OUTLINE_STROKE_THICKNESS}; -use glifrenderer::string::UiString; -use skia_safe::{dash_path_effect, Canvas, Paint, Path, Point}; -use MFEKmath::Vector; +use glifrenderer::constants; +use skia_safe::{Point, Color4f, Typeface, Font}; use crate::editor::Editor; +use crate::tool_behaviors::zoom_scroll::ZoomScroll; +use crate::tools::cut::{Cut, Intersection}; use crate::user_interface::Interface; use super::prelude::*; #[derive(Clone, Debug)] pub struct Measure { - measure_from: Option<(f32, f32)>, + dragging: bool, } impl Tool for Measure { #[rustfmt::skip] - fn event(&mut self, v: &mut Editor, _i: &mut Interface, event: EditorEvent) { + fn event(&mut self, v: &mut Editor, i: &mut Interface, event: EditorEvent) { if let EditorEvent::MouseEvent { mouse_info, event_type } = event { match event_type { - MouseEventType::Pressed => self.mouse_pressed(v, mouse_info), - MouseEventType::Released => self.mouse_released(v, mouse_info), + MouseEventType::Pressed => self.mouse_pressed(i, mouse_info), + MouseEventType::Moved => self.mouse_moved(i, mouse_info), + MouseEventType::Released => self.mouse_released(), _ => (), } } - } - fn draw(&mut self, _v: &Editor, i: &Interface, canvas: &mut Canvas) { - self.draw_line(i, canvas); + match event { + EditorEvent::ScrollEvent { .. } => ZoomScroll::default().event(v, i, event), + _ => {}, + } } } impl Measure { pub fn new() -> Self { - Self { measure_from: None } + Self { dragging: false } } - fn mouse_pressed(&mut self, _v: &Editor, mouse_info: MouseInfo) { - self.measure_from = Some(mouse_info.position); + fn mouse_released(&mut self) { + self.dragging = false } - fn mouse_released(&mut self, _v: &Editor, _mouse_info: MouseInfo) { - self.measure_from = None; + fn mouse_pressed(&mut self, i: &mut Interface, mouse_info: MouseInfo) { + i.measure.enabled = true; + self.dragging = true; + i.measure.start_point = Some(mouse_info.position); + i.measure.end_point = None; } - fn draw_line(&self, i: &Interface, canvas: &mut Canvas) { - let mut path = Path::new(); - let mut paint = Paint::default(); - let factor = i.viewport.factor; - - if let Some(measure_from) = self.measure_from { - let skpath_start = Point::new(measure_from.0 as f32, measure_from.1 as f32); - let skpath_end = Point::new( - i.mouse_info.position.0 as f32, - i.mouse_info.position.1 as f32, - ); - - let start_vec = Vector::from_skia_point(&skpath_start); - let end_vec = Vector::from_skia_point(&skpath_end); - let halfway = start_vec.lerp(end_vec, 0.5); - let unit_vec = (end_vec - start_vec).normalize(); - let angle = f64::atan2(unit_vec.y, unit_vec.x); - let distance = start_vec.distance(end_vec) * (1. / factor) as f64; - - path.move_to(skpath_start); - path.line_to(skpath_end); - paint.set_color(constants::MEASURE_STROKE); - paint.set_style(skia_safe::PaintStyle::Stroke); - paint.set_stroke_width(OUTLINE_STROKE_THICKNESS * (1. / factor)); - let dash_offset = (1. / factor) * 5.; - paint.set_path_effect(dash_path_effect::new(&[dash_offset, dash_offset], 0.0)); - canvas.draw_path(&path, &paint); - - draw_measure_string( - i, - (halfway.x as f32, halfway.y as f32), - angle as f32, - format! {"{0:.3}", distance}.as_str(), - canvas, - ); - } + fn mouse_moved(&mut self, i: &mut Interface, mouse_info: MouseInfo) { + if !self.dragging { return } + i.measure.end_point = Some(mouse_info.position); } -} - -pub fn draw_measure_string( - i: &Interface, - at: (f32, f32), - angle: f32, - s: &str, - canvas: &mut Canvas, -) { - let s = UiString::centered_with_colors(s, MEASURE_STROKE, None).rotated(angle.to_degrees()); - - s.draw(&i.viewport, at, canvas); -} +} \ No newline at end of file diff --git a/src/tools/mod.rs b/src/tools/mod.rs index a5eb76c..fde9be2 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,6 +1,7 @@ // Include all tools via procedural macro. Expands to `pub mod pen; pub mod select; ...` pub_mod!("src/tools"); +use self::cut::Cut; use self::prelude::*; use self::{ anchors::Anchors, dash::Dash, guidelines::Guidelines, image::Image, measure::Measure, pan::Pan, @@ -42,6 +43,7 @@ use strum_macros::{AsRefStr, EnumString}; pub enum ToolEnum { Pan, Pen, + Cut, Select, Anchors, Zoom, @@ -80,5 +82,6 @@ pub fn tool_enum_to_tool(tool: ToolEnum) -> Box { ToolEnum::Image => Box::new(Image::new()), ToolEnum::PAP => Box::new(PAP::new()), ToolEnum::Guidelines => Box::new(Guidelines::new()), + ToolEnum::Cut => Box::new(Cut::new()), } } diff --git a/src/tools/pen/modes/cubic.rs b/src/tools/pen/modes/cubic.rs index 0a45fea..7dcb9a5 100644 --- a/src/tools/pen/modes/cubic.rs +++ b/src/tools/pen/modes/cubic.rs @@ -1,4 +1,4 @@ -use MFEKmath::{Bezier, Primitive}; +use MFEKmath::{Bezier, subdivide::Subdivide}; use glifparser::{glif::{contour::MFEKContourCommon, contour_operations::ContourOperation}, Point, PointType, WhichHandle, Contour, MFEKPointData}; use glifrenderer::points::draw_point; @@ -99,7 +99,7 @@ impl PenMode for CubicMode { }; let bez = Bezier::from(&point, &next_point); - let subdivisions = bez.subdivide(info.t); + let subdivisions = bez.split(info.t); if let Some(subdivisions) = subdivisions { let (sub_a, sub_b) = ( diff --git a/src/tools/select/mod.rs b/src/tools/select/mod.rs index 3ebd385..bebc6a7 100644 --- a/src/tools/select/mod.rs +++ b/src/tools/select/mod.rs @@ -3,8 +3,11 @@ use std::collections::HashSet; // Select use super::{prelude::*, EditorEvent, MouseEventType, Tool}; use crate::command::{Command, CommandType}; +use crate::editor::tunni::{get_closest_tunni_line, get_tunni_line_from_info, get_tunni_point_from_info, clicked_tunni_point_or_line}; use crate::get_point_mut; +use crate::tool_behaviors::move_tunni_point::MoveTunniPoint; use crate::tool_behaviors::rotate_selection::RotateSelection; +use crate::tool_behaviors::move_tunni_line::MoveTunniLine; use glifparser::glif::mfek::contour::MFEKContourCommon; use MFEKmath::Vector; @@ -68,6 +71,7 @@ impl Tool for Select { } fn draw(&mut self, v: &Editor, i: &Interface, canvas: &mut Canvas) { + self.draw_tunni_line(v, i, canvas); self.draw_pivot.draw(v, i, canvas); } } @@ -87,6 +91,28 @@ impl Select { v.selected = points; } + fn draw_tunni_line(&mut self, v: &Editor, i: &Interface, canvas: &mut Canvas) { + let mut paint = Paint::default(); + + let closest_tunni = get_closest_tunni_line(v, i); + + if let Some(tunni_info) = closest_tunni { + let tunni_line = get_tunni_line_from_info(v, &tunni_info).unwrap(); + let p1 = (tunni_line.0.x as f32, tunni_line.0.y as f32); + let p2: (f32, f32) = (tunni_line.1.x as f32, tunni_line.1.y as f32); + + paint.set_anti_alias(true); + paint.set_color(0xFF0000FF); + paint.set_style(PaintStyle::Stroke); + paint.set_stroke_width(OUTLINE_STROKE_THICKNESS * (1. / i.viewport.factor)); + canvas.draw_line(p1, p2, &paint); + + let tunni_point = get_tunni_point_from_info(v, &tunni_info).unwrap(); + paint.set_style(PaintStyle::Fill); + canvas.draw_circle((tunni_point.x as f32, tunni_point.y as f32), 5. * (1. / i.viewport.factor), &paint); + } + } + fn nudge_selected(&mut self, v: &mut Editor, command: Command) { let mut selected = v.selected.clone(); if let (Some(ci), Some(pi)) = (v.contour_idx, v.point_idx) { @@ -167,52 +193,68 @@ impl Select { return; } - // if we found a point or handle we're going to start a drag operation - match clicked_point_or_handle(v, i, mouse_info.raw_position, None) { - Some((ci, pi, wh)) => { - // first we check if shift is held, if they are we put the current selection - // into the editor's selected HashSet - if mouse_info.modifiers.shift { - if let Some(point_idx) = v.point_idx { - v.selected.insert((v.contour_idx.unwrap(), point_idx)); - } - } else if !v.selected.contains(&(ci, pi)) { - // if the user isn't holding shift or control, and the point they're clicking is not in the current - // selection we clear the selection - v.selected = HashSet::new(); + if let Some((ci, pi, wh)) = clicked_point_or_handle(v, i, mouse_info.raw_position, None) { + // first we check if shift is held, if they are we put the current selection + // into the editor's selected HashSet + if mouse_info.modifiers.shift { + if let Some(point_idx) = v.point_idx { + v.selected.insert((v.contour_idx.unwrap(), point_idx)); } + } else if !v.selected.contains(&(ci, pi)) { + // if the user isn't holding shift or control, and the point they're clicking is not in the current + // selection we clear the selection + v.selected = HashSet::new(); + } - // Set the editor's selected point to the most recently clicked one. - v.contour_idx = Some(ci); - v.point_idx = Some(pi); + // Set the editor's selected point to the most recently clicked one. + v.contour_idx = Some(ci); + v.point_idx = Some(pi); - if wh == WhichHandle::Neither { - // the user clicked niether handle so that's our cue to push a move_point behavior on the stack - let move_selected = !mouse_info.modifiers.ctrl; - v.set_behavior(Box::new(MovePoint::new(move_selected, mouse_info))); - } else { - // the user clicked a handle so we push a move_handle behavior - v.set_behavior(Box::new(MoveHandle::new(wh, mouse_info, false))); - } + if wh == WhichHandle::Neither { + // the user clicked niether handle so that's our cue to push a move_point behavior on the stack + let move_selected = !mouse_info.modifiers.ctrl; + v.set_behavior(Box::new(MovePoint::new(move_selected, mouse_info))); + } else { + // the user clicked a handle so we push a move_handle behavior + v.set_behavior(Box::new(MoveHandle::new(wh, mouse_info, false))); } - None => { - // if the user isn't holding shift we clear the current selection and the currently selected - // point - if !mouse_info.modifiers.shift { - v.selected = HashSet::new(); - v.contour_idx = None; - v.point_idx = None; - } - // if they clicked right mouse we set the pivot point that will be used by rotate_points behavior. - if mouse_info.button == MouseButton::Right { - self.pivot_point = Some((mouse_info.position.0, mouse_info.position.1)); - } else if mouse_info.button == MouseButton::Left { - v.set_behavior(Box::new(SelectionBox::new(mouse_info))); - } + return + } + + if let Some((tunni_info, tunni_type)) = clicked_tunni_point_or_line(v, i) { + match tunni_type { + editor::tunni::Tunni::Point => { + v.set_selected(tunni_info.a.0, tunni_info.a.1); + v.set_behavior(Box::new(MoveTunniPoint::new(mouse_info, tunni_info))); + return + }, + editor::tunni::Tunni::Line => { + v.set_selected(tunni_info.a.0, tunni_info.a.1); + let line = get_tunni_line_from_info(v, &tunni_info).unwrap(); + v.set_behavior(Box::new(MoveTunniLine::new(mouse_info, tunni_info, line))); + return + }, } - }; + } + + + // the user clicked an empty location + // if the user isn't holding shift we clear the current selection and the currently selected + // point + if !mouse_info.modifiers.shift { + v.selected = HashSet::new(); + v.contour_idx = None; + v.point_idx = None; + } + + // if they clicked right mouse we set the pivot point that will be used by rotate_points behavior. + if mouse_info.button == MouseButton::Right { + self.pivot_point = Some((mouse_info.position.0, mouse_info.position.1)); + } else if mouse_info.button == MouseButton::Left { + v.set_behavior(Box::new(SelectionBox::new(mouse_info))); + } } pub fn mouse_double_pressed(&mut self, v: &mut Editor, i: &Interface, mouse_info: MouseInfo) { diff --git a/src/user_interface/gui/icons/mod.rs b/src/user_interface/gui/icons/mod.rs index 7d4d16f..30c0cd9 100644 --- a/src/user_interface/gui/icons/mod.rs +++ b/src/user_interface/gui/icons/mod.rs @@ -3,7 +3,7 @@ pub use self::button::build as build_button; pub use self::button::build_and_add as build_and_add_button; pub use self::button::IntoResponse as IntoButtonResponse; -pub const _KNIFE: &str = "\u{F000}"; +pub const KNIFE: &str = "\u{F000}"; pub const MEASURE: &str = "\u{F001}"; pub const PAN: &str = "\u{F002}"; pub const PEN: &str = "\u{F003}"; diff --git a/src/user_interface/gui/menu_bar.rs b/src/user_interface/gui/menu_bar.rs index 2f90756..55f5f51 100644 --- a/src/user_interface/gui/menu_bar.rs +++ b/src/user_interface/gui/menu_bar.rs @@ -69,6 +69,8 @@ pub fn menu_bar(ctx: &Context, v: &mut Editor, i: &mut Interface, wm: &mut Windo }); ui.checkbox(&mut i.grid.show, "Grid"); + ui.checkbox(&mut i.curvature_vis, "Curvature Visualization"); + ui.checkbox(&mut i.measure.enabled, "Show Measure"); }); // diff --git a/src/user_interface/gui/tool_bar.rs b/src/user_interface/gui/tool_bar.rs index b95c42a..f3a60bb 100644 --- a/src/user_interface/gui/tool_bar.rs +++ b/src/user_interface/gui/tool_bar.rs @@ -50,6 +50,7 @@ pub fn tool_bar(ctx: &Context, v: &mut Editor, _i: &mut Interface) { build_button(v, ui, icons::PAN, ToolEnum::Pan); build_button(v, ui, icons::SELECT, ToolEnum::Select); build_button(v, ui, icons::PEN, ToolEnum::Pen); + build_button(v, ui, icons::KNIFE, ToolEnum::Cut); ui.separator(); build_button(v, ui, icons::ZOOM, ToolEnum::Zoom); build_button(v, ui, icons::MEASURE, ToolEnum::Measure); diff --git a/src/user_interface/mod.rs b/src/user_interface/mod.rs index 54f9238..a773b8c 100644 --- a/src/user_interface/mod.rs +++ b/src/user_interface/mod.rs @@ -24,6 +24,7 @@ use skia_safe::Surface; use crate::editor::Editor; pub use crate::user_interface::mouse_input::MouseInfo; +pub use crate::render::measure::Measure; use sdl2::{video::Window as SdlWindow, Sdl}; @@ -42,6 +43,8 @@ pub struct Interface { pub context: Option<(f32, f32)>, pub grid: Grid, + pub measure: Measure, + pub curvature_vis: bool, pub mouse_info: MouseInfo, pub viewport: Viewport, @@ -84,6 +87,8 @@ impl Interface { context: None, grid: Grid::default(), + measure: Measure { start_point: None, end_point: None, enabled: true}, + curvature_vis: true, mouse_info: MouseInfo::default(), viewport: Viewport::default(),