mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Bezier-rs: Subpath offset and bezier offset improvements (#1039)
* Added subpath offset * Enhanced offset to produce smooth curves * Lots of outline bugfixes * Fixed failing unit tests * Added subpath outline * Refactor bezier offset and outline to return Subpaths * Fix outline bug due to smooth joining and removed reduce optimization that causes jumping approximations * Bugfix when subpath angle is acute but doesn't intersect * Stylistic changes per review * Stylistic changes per review and updated doc comments --------- Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
531438161e
commit
ccb698ffa8
@@ -239,10 +239,10 @@ const bezierFeatures = {
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -50,
|
||||
max: 50,
|
||||
min: -30,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -257,9 +257,9 @@ const bezierFeatures = {
|
||||
{
|
||||
variable: "distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -274,16 +274,16 @@ const bezierFeatures = {
|
||||
{
|
||||
variable: "start_distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
default: 5,
|
||||
},
|
||||
{
|
||||
variable: "end_distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -306,28 +306,28 @@ const bezierFeatures = {
|
||||
{
|
||||
variable: "distance1",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
},
|
||||
{
|
||||
variable: "distance2",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
{
|
||||
variable: "distance3",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
},
|
||||
{
|
||||
variable: "distance4",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 5,
|
||||
},
|
||||
|
||||
@@ -114,6 +114,32 @@ const subpathFeatures = {
|
||||
],
|
||||
chooseTVariant: true,
|
||||
},
|
||||
offset: {
|
||||
name: "Offset",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance),
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -25,
|
||||
max: 25,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
outline: {
|
||||
name: "Outline",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance),
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: 0,
|
||||
max: 25,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::svg_drawing::*;
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions, TValue};
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -49,6 +49,16 @@ fn parse_t_variant(t_variant: &String, t: f64) -> TValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// An empty id type for use in tests
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct EmptyId;
|
||||
|
||||
impl Identifier for EmptyId {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmBezier {
|
||||
/// Expect js_points to be a list of 2 pairs.
|
||||
@@ -542,7 +552,7 @@ impl WasmBezier {
|
||||
let original_curve_svg = self.get_bezier_path();
|
||||
let bezier_curves_svg = self
|
||||
.0
|
||||
.offset(distance)
|
||||
.offset::<EmptyId>(distance)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, bezier_curve)| {
|
||||
@@ -561,36 +571,39 @@ impl WasmBezier {
|
||||
}
|
||||
|
||||
pub fn outline(&self, distance: f64) -> String {
|
||||
let outline_beziers = self.0.outline(distance);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.outline::<EmptyId>(distance);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
let mut outline_svg = String::new();
|
||||
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
let bezier_svg = self.get_bezier_path();
|
||||
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
}
|
||||
|
||||
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> String {
|
||||
let outline_beziers = self.0.graduated_outline(start_distance, end_distance);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.graduated_outline::<EmptyId>(start_distance, end_distance);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
let mut outline_svg = String::new();
|
||||
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
let bezier_svg = self.get_bezier_path();
|
||||
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
}
|
||||
|
||||
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> String {
|
||||
let outline_beziers = self.0.skewed_outline(distance1, distance2, distance3, distance4);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.skewed_outline::<EmptyId>(distance1, distance2, distance3, distance4);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
let mut outline_svg = String::new();
|
||||
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
let bezier_svg = self.get_bezier_path();
|
||||
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
|
||||
@@ -376,4 +376,29 @@ impl WasmSubpath {
|
||||
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg))
|
||||
}
|
||||
|
||||
pub fn offset(&self, distance: f64) -> String {
|
||||
let offset_subpath = self.0.offset(distance, bezier_rs::Joint::Bevel);
|
||||
|
||||
let mut offset_svg = String::new();
|
||||
offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg()))
|
||||
}
|
||||
|
||||
pub fn outline(&self, distance: f64) -> String {
|
||||
let (outline_piece1, outline_piece2) = self.0.outline(distance, bezier_rs::Joint::Bevel);
|
||||
|
||||
let mut outline_piece1_svg = String::new();
|
||||
outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
let mut outline_piece2_svg = String::new();
|
||||
if outline_piece2.is_some() {
|
||||
outline_piece2
|
||||
.unwrap()
|
||||
.to_svg(&mut outline_piece2_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
}
|
||||
|
||||
wrap_svg_tag(format!("{}{outline_piece1_svg}{outline_piece2_svg}", self.to_default_svg()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use std::fmt::Write;
|
||||
|
||||
// SVG drawing constants
|
||||
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px">"#;
|
||||
@@ -48,19 +46,6 @@ pub fn draw_line(start_x: f64, start_y: f64, end_x: f64, end_y: f64, stroke: &st
|
||||
format!(r#"<line x1="{start_x}" y1="{start_y}" x2="{end_x}" y2="{end_y}" stroke="{stroke}" stroke-width="{stroke_width}"/>"#)
|
||||
}
|
||||
|
||||
/// Helper function to draw a list of beziers.
|
||||
pub fn draw_beziers(beziers: Vec<Bezier>, options: String) -> String {
|
||||
let start_point = beziers.first().unwrap().start();
|
||||
let mut svg = format!("<path d=\"M {} {}", start_point.x, start_point.y);
|
||||
|
||||
beziers.iter().for_each(|bezier| {
|
||||
let _ = write!(svg, " {}", bezier.svg_curve_argument());
|
||||
});
|
||||
|
||||
let _ = write!(svg, " Z\" {}/>", options);
|
||||
svg
|
||||
}
|
||||
|
||||
// Helper function to convert polar to cartesian coordinates
|
||||
fn polar_to_cartesian(center_x: f64, center_y: f64, radius: f64, angle_in_rad: f64) -> [f64; 2] {
|
||||
let x = center_x + radius * angle_in_rad.cos();
|
||||
|
||||
Reference in New Issue
Block a user