mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add Area and Centroid nodes (#1749)
* initial attempt for area node * allow node preview for more types * make AreaNode sync and add CentroidNode * cargo fmt * preview of DVec2 * make the nodes async again * use segment domain instead of region domain * modify the check for linearity * create a limit for area in centroid calculation * cargo fmt * reverse unnecessary changes * add threshold to area calculation too. * handle zero area edge case * add todo comment * implement 1D centroid and use it as fallback * formatting floats to skip last zero * add Centroid Type radio button to Centroid Node * rename docs to use area and perimeter centroid * add web demos for perimeter centroid * add tests for perimeter centroid * add fallback to use average of points * Fix for broken area * missing fixes * Code review and rename Perimeter Centroid to Length Centroid * Use dummy footprint in Area and Centroid nodes * add doc and todo to clarify when `is_linear` fails * use epsilon instead of zero --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
@@ -66,6 +66,10 @@ const bezierFeatures = {
|
||||
name: "Length",
|
||||
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.length(),
|
||||
},
|
||||
"length-centroid": {
|
||||
name: "Length Centroid",
|
||||
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.length_centroid(),
|
||||
},
|
||||
evaluate: {
|
||||
name: "Evaluate",
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.evaluate(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
|
||||
|
||||
@@ -16,16 +16,25 @@ const subpathFeatures = {
|
||||
name: "Length",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
|
||||
},
|
||||
"length-centroid": {
|
||||
name: "Length Centroid",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.length_centroid(),
|
||||
},
|
||||
area: {
|
||||
name: "Area",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.area(options.error, options.minimum_separation),
|
||||
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
|
||||
},
|
||||
centroid: {
|
||||
name: "Centroid",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.centroid(options.error, options.minimum_separation),
|
||||
"area-centroid": {
|
||||
name: "Area Centroid",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.area_centroid(options.error, options.minimum_separation),
|
||||
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
|
||||
},
|
||||
"poisson-disk-points": {
|
||||
name: "Poisson-Disk Points",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.poisson_disk_points(options.separation_disk_diameter),
|
||||
inputOptions: [separationDiskDiameter],
|
||||
},
|
||||
evaluate: {
|
||||
name: "Evaluate",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.evaluate(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
|
||||
@@ -69,11 +78,6 @@ const subpathFeatures = {
|
||||
name: "Bounding Box",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.bounding_box(),
|
||||
},
|
||||
"poisson-disk-points": {
|
||||
name: "Poisson-Disk Points",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.poisson_disk_points(options.separation_disk_diameter),
|
||||
inputOptions: [separationDiskDiameter],
|
||||
},
|
||||
inflections: {
|
||||
name: "Inflections",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.inflections(),
|
||||
|
||||
@@ -148,6 +148,13 @@ impl WasmBezier {
|
||||
wrap_svg_tag(format!("{bezier}{}", draw_text(format!("Length: {:.2}", self.0.length(None)), TEXT_OFFSET_X, TEXT_OFFSET_Y, BLACK)))
|
||||
}
|
||||
|
||||
pub fn length_centroid(&self) -> String {
|
||||
let bezier = self.get_bezier_path();
|
||||
let centroid = self.0.length_centroid(None);
|
||||
let point_text = draw_circle(centroid, 4., RED, 1.5, WHITE);
|
||||
wrap_svg_tag(format!("{bezier}{}", point_text))
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, raw_t: f64, t_variant: String) -> String {
|
||||
let bezier = self.get_bezier_path();
|
||||
let t = parse_t_variant(&t_variant, raw_t);
|
||||
|
||||
@@ -76,6 +76,18 @@ impl WasmSubpath {
|
||||
subpath_svg
|
||||
}
|
||||
|
||||
fn to_filled_svg(&self) -> String {
|
||||
let mut subpath_svg = String::new();
|
||||
self.0.to_svg(
|
||||
&mut subpath_svg,
|
||||
CURVE_FILLED_ATTRIBUTES.to_string(),
|
||||
ANCHOR_ATTRIBUTES.to_string(),
|
||||
HANDLE_ATTRIBUTES.to_string(),
|
||||
HANDLE_LINE_ATTRIBUTES.to_string(),
|
||||
);
|
||||
subpath_svg
|
||||
}
|
||||
|
||||
pub fn insert(&self, t: f64, t_variant: String) -> String {
|
||||
let mut subpath = self.0.clone();
|
||||
let t = parse_t_variant(&t_variant, t);
|
||||
@@ -92,14 +104,35 @@ impl WasmSubpath {
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text))
|
||||
}
|
||||
|
||||
pub fn area(&self, error: f64, minimum_separation: f64) -> String {
|
||||
let area_text = draw_text(format!("Area: {}", self.0.area(Some(error), Some(minimum_separation))), 5., 193., BLACK);
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), area_text))
|
||||
pub fn length_centroid(&self) -> String {
|
||||
let centroid = self.0.length_centroid(None, true).unwrap();
|
||||
let point_text = draw_circle(centroid, 4., RED, 1.5, WHITE);
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
|
||||
}
|
||||
|
||||
pub fn centroid(&self, error: f64, minimum_separation: f64) -> String {
|
||||
let point_text = draw_circle(self.0.centroid(Some(error), Some(minimum_separation)).unwrap(), 4., RED, 1.5, WHITE);
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
|
||||
pub fn area(&self, error: f64, minimum_separation: f64) -> String {
|
||||
let area_text = draw_text(format!("Area: {}", self.0.area(Some(error), Some(minimum_separation))), 5., 193., BLACK);
|
||||
wrap_svg_tag(format!("{}{}", self.to_filled_svg(), area_text))
|
||||
}
|
||||
|
||||
pub fn area_centroid(&self, error: f64, minimum_separation: f64) -> String {
|
||||
let point_text = draw_circle(self.0.area_centroid(Some(error), Some(minimum_separation), None).unwrap(), 4., RED, 1.5, WHITE);
|
||||
wrap_svg_tag(format!("{}{}", self.to_filled_svg(), point_text))
|
||||
}
|
||||
|
||||
pub fn poisson_disk_points(&self, separation_disk_diameter: f64) -> String {
|
||||
let r = separation_disk_diameter / 2.;
|
||||
|
||||
let subpath_svg = self.to_default_svg();
|
||||
let points = self.0.poisson_disk_points(separation_disk_diameter, Math::random);
|
||||
|
||||
let points_style = format!("<style class=\"poisson\">style.poisson ~ circle {{ fill: {RED}; opacity: 0.25; }}</style>");
|
||||
let content = points
|
||||
.iter()
|
||||
.map(|point| format!("<circle cx=\"{}\" cy=\"{}\" r=\"{r}\" />", point.x, point.y))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
wrap_svg_tag(format!("{subpath_svg}{points_style}{content}"))
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64, t_variant: String) -> String {
|
||||
@@ -193,21 +226,6 @@ impl WasmSubpath {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poisson_disk_points(&self, separation_disk_diameter: f64) -> String {
|
||||
let r = separation_disk_diameter / 2.;
|
||||
|
||||
let subpath_svg = self.to_default_svg();
|
||||
let points = self.0.poisson_disk_points(separation_disk_diameter, Math::random);
|
||||
|
||||
let points_style = format!("<style class=\"poisson\">style.poisson ~ circle {{ fill: {RED}; opacity: 0.25; }}</style>");
|
||||
let content = points
|
||||
.iter()
|
||||
.map(|point| format!("<circle cx=\"{}\" cy=\"{}\" r=\"{r}\" />", point.x, point.y))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
wrap_svg_tag(format!("{subpath_svg}{points_style}{content}"))
|
||||
}
|
||||
|
||||
pub fn inflections(&self) -> String {
|
||||
let inflections: Vec<f64> = self.0.inflections();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ pub const NONE: &str = "none";
|
||||
|
||||
// Default attributes
|
||||
pub const CURVE_ATTRIBUTES: &str = "stroke=\"black\" stroke-width=\"2\" fill=\"none\"";
|
||||
pub const CURVE_FILLED_ATTRIBUTES: &str = "stroke=\"black\" stroke-width=\"2\" fill=\"lightgray\"";
|
||||
pub const HANDLE_LINE_ATTRIBUTES: &str = "stroke=\"gray\" stroke-width=\"1\" fill=\"none\"";
|
||||
pub const ANCHOR_ATTRIBUTES: &str = "r=\"4\" stroke=\"black\" stroke-width=\"2\" fill=\"white\"";
|
||||
pub const HANDLE_ATTRIBUTES: &str = "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"";
|
||||
@@ -59,7 +60,7 @@ pub fn draw_sector(center: DVec2, radius: f64, start_angle: f64, end_angle: f64,
|
||||
let [end_x, end_y] = polar_to_cartesian(center.x, center.y, radius, end_angle);
|
||||
// draw sector with fill color
|
||||
let sector_svg = format!(
|
||||
r#"<path d="M {start_x} {start_y} A {radius} {radius} 0 0 1 {end_x} {end_y} L {} {} L {start_x} {start_y} Z" stroke="none" fill="{fill}" />"#,
|
||||
r#"<path d="M {start_x} {start_y} A {radius} {radius} 0 0 1 {end_x} {end_y} L {} {} L {start_x} {start_y} Z" stroke="none" fill="{fill}" />"#,
|
||||
center.x, center.y
|
||||
);
|
||||
// draw arc with stroke color
|
||||
|
||||
Reference in New Issue
Block a user