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:
Rob Nadal
2023-03-03 14:21:21 -05:00
committed by Keavon Chambers
parent 531438161e
commit ccb698ffa8
16 changed files with 611 additions and 168 deletions

View File

@@ -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,
},

View File

@@ -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;

View File

@@ -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}"))

View File

@@ -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()))
}
}

View File

@@ -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();