mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 13:48:12 +08:00
Bezier-rs: Add joins and caps to offsets and outlines (#1083)
* Intial work * Improve miter and add round join * Get arcs to go opposite direction * Add cap and other refactors * Rename joint to join, fix some bugs * Fix single point issue * Clean up * Fix iframe sizes and update UI * Address comments and handle single point outline * Rename variables, fix branches in outline * Address comments
This commit is contained in:
committed by
Keavon Chambers
parent
7e124c8035
commit
c0576ab4e0
@@ -1,6 +1,6 @@
|
||||
import subpathFeatures, { SubpathFeatureKey } from "@graphite/features/subpath-features";
|
||||
import { renderDemoPane } from "@graphite/utils/render";
|
||||
import { Demo, DemoPane, InputOption, SubpathDemoArgs } from "@graphite/utils/types";
|
||||
import { Demo, DemoPane, SubpathDemoArgs, SubpathInputOption } from "@graphite/utils/types";
|
||||
|
||||
class SubpathDemoPane extends HTMLElement implements DemoPane {
|
||||
// Props
|
||||
@@ -8,7 +8,7 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
|
||||
|
||||
name!: string;
|
||||
|
||||
inputOptions!: InputOption[];
|
||||
inputOptions!: SubpathInputOption[];
|
||||
|
||||
triggerOnMouseMove!: boolean;
|
||||
|
||||
@@ -62,7 +62,12 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
|
||||
subpathDemo.setAttribute("triples", JSON.stringify(demo.triples));
|
||||
subpathDemo.setAttribute("closed", String(demo.closed));
|
||||
subpathDemo.setAttribute("key", this.key);
|
||||
subpathDemo.setAttribute("inputOptions", JSON.stringify(this.inputOptions));
|
||||
|
||||
const inputOptions = this.inputOptions.map((option) => ({
|
||||
...option,
|
||||
disabled: option.isDisabledForClosed && demo.closed,
|
||||
}));
|
||||
subpathDemo.setAttribute("inputOptions", JSON.stringify(inputOptions));
|
||||
subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
|
||||
return subpathDemo;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WasmBezier } from "@graphite/../wasm/pkg";
|
||||
import { tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@graphite/utils/options";
|
||||
import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@graphite/utils/options";
|
||||
import { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption, BEZIER_T_VALUE_VARIANTS } from "@graphite/utils/types";
|
||||
|
||||
const bezierFeatures = {
|
||||
@@ -251,7 +251,7 @@ const bezierFeatures = {
|
||||
},
|
||||
outline: {
|
||||
name: "Outline",
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.outline(options.distance),
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.outline(options.distance, options.cap),
|
||||
demoOptions: {
|
||||
Quadratic: {
|
||||
inputOptions: [
|
||||
@@ -262,13 +262,14 @@ const bezierFeatures = {
|
||||
step: 1,
|
||||
default: 15,
|
||||
},
|
||||
capOptions,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"graduated-outline": {
|
||||
name: "Graduated Outline",
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.graduated_outline(options.start_distance, options.end_distance),
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.cap),
|
||||
demoOptions: {
|
||||
Quadratic: {
|
||||
inputOptions: [
|
||||
@@ -286,6 +287,7 @@ const bezierFeatures = {
|
||||
step: 1,
|
||||
default: 15,
|
||||
},
|
||||
capOptions,
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -300,7 +302,8 @@ const bezierFeatures = {
|
||||
},
|
||||
"skewed-outline": {
|
||||
name: "Skewed Outline",
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4),
|
||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string =>
|
||||
bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.cap),
|
||||
demoOptions: {
|
||||
Quadratic: {
|
||||
inputOptions: [
|
||||
@@ -332,6 +335,7 @@ const bezierFeatures = {
|
||||
step: 1,
|
||||
default: 5,
|
||||
},
|
||||
capOptions,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options";
|
||||
import { InputOption, SubpathCallback, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types";
|
||||
import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options";
|
||||
import { SubpathCallback, SubpathInputOption, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types";
|
||||
|
||||
const subpathFeatures = {
|
||||
constructor: {
|
||||
@@ -107,7 +107,7 @@ const subpathFeatures = {
|
||||
},
|
||||
offset: {
|
||||
name: "Offset",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance),
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance, options.join),
|
||||
inputOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
@@ -116,11 +116,12 @@ const subpathFeatures = {
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
joinOptions,
|
||||
],
|
||||
},
|
||||
outline: {
|
||||
name: "Outline",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance),
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance, options.join, options.cap),
|
||||
inputOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
@@ -129,6 +130,8 @@ const subpathFeatures = {
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
joinOptions,
|
||||
{ ...capOptions, isDisabledForClosed: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -137,7 +140,7 @@ export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
||||
export type SubpathFeatureOptions = {
|
||||
name: string;
|
||||
callback: SubpathCallback;
|
||||
inputOptions?: InputOption[];
|
||||
inputOptions?: SubpathInputOption[];
|
||||
triggerOnMouseMove?: boolean;
|
||||
};
|
||||
export default subpathFeatures as Record<SubpathFeatureKey, SubpathFeatureOptions>;
|
||||
|
||||
@@ -45,3 +45,17 @@ export const subpathTValueVariantOptions = {
|
||||
inputType: "dropdown",
|
||||
options: SUBPATH_T_VALUE_VARIANTS,
|
||||
};
|
||||
|
||||
export const joinOptions = {
|
||||
variable: "join",
|
||||
default: 0,
|
||||
inputType: "dropdown",
|
||||
options: ["Bevel", "Miter", "Round"],
|
||||
};
|
||||
|
||||
export const capOptions = {
|
||||
variable: "cap",
|
||||
default: 0,
|
||||
inputType: "dropdown",
|
||||
options: ["Butt", "Round", "Square"],
|
||||
};
|
||||
|
||||
@@ -43,6 +43,10 @@ export function renderDemo(demo: Demo): void {
|
||||
selectInput.append(option);
|
||||
});
|
||||
|
||||
if (inputOption.disabled) {
|
||||
selectInput.disabled = true;
|
||||
}
|
||||
|
||||
selectInput.addEventListener("change", (event: Event): void => {
|
||||
demo.sliderData[inputOption.variable] = Number((event.target as HTMLInputElement).value);
|
||||
demo.drawDemo(figure);
|
||||
|
||||
@@ -22,6 +22,10 @@ export type BezierDemoOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SubpathInputOption = InputOption & {
|
||||
isDisabledForClosed?: boolean;
|
||||
};
|
||||
|
||||
export type InputOption = {
|
||||
variable: string;
|
||||
min?: number;
|
||||
@@ -31,6 +35,7 @@ export type InputOption = {
|
||||
unit?: string | string[];
|
||||
inputType?: "slider" | "dropdown";
|
||||
options?: string[];
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function getCurveType(numPoints: number): BezierCurveType {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::svg_drawing::*;
|
||||
use crate::utils::parse_cap;
|
||||
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -219,22 +221,25 @@ impl WasmBezier {
|
||||
}
|
||||
|
||||
pub fn curvature(&self, raw_t: f64, t_variant: String) -> String {
|
||||
let bezier = self.get_bezier_path();
|
||||
let mut content = self.get_bezier_path();
|
||||
let t = parse_t_variant(&t_variant, raw_t);
|
||||
|
||||
let radius = 1. / self.0.curvature(t);
|
||||
let normal_point = self.0.normal(t);
|
||||
let intersection_point = self.0.evaluate(t);
|
||||
let curvature = self.0.curvature(t);
|
||||
if curvature > 0. {
|
||||
let radius = 1. / self.0.curvature(t);
|
||||
let normal_point = self.0.normal(t);
|
||||
let intersection_point = self.0.evaluate(t);
|
||||
|
||||
let curvature_center = intersection_point + normal_point * radius;
|
||||
let curvature_center = intersection_point + normal_point * radius;
|
||||
|
||||
let content = format!(
|
||||
"{bezier}{}{}{}{}",
|
||||
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
|
||||
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
|
||||
draw_circle(intersection_point, 3., RED, 1., WHITE),
|
||||
draw_circle(curvature_center, 3., RED, 1., WHITE),
|
||||
);
|
||||
content = format!(
|
||||
"{content}{}{}{}{}",
|
||||
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
|
||||
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
|
||||
draw_circle(intersection_point, 3., RED, 1., WHITE),
|
||||
draw_circle(curvature_center, 3., RED, 1., WHITE),
|
||||
);
|
||||
}
|
||||
wrap_svg_tag(content)
|
||||
}
|
||||
|
||||
@@ -242,19 +247,10 @@ impl WasmBezier {
|
||||
let t = parse_t_variant(&t_variant, raw_t);
|
||||
let beziers: [Bezier; 2] = self.0.split(t);
|
||||
|
||||
let mut original_bezier_svg = String::new();
|
||||
self.0.to_svg(
|
||||
&mut original_bezier_svg,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, WHITE),
|
||||
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, WHITE),
|
||||
HANDLE_ATTRIBUTES.to_string(),
|
||||
HANDLE_LINE_ATTRIBUTES.to_string(),
|
||||
);
|
||||
|
||||
let mut bezier_svg_1 = String::new();
|
||||
beziers[0].to_svg(
|
||||
&mut bezier_svg_1,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
|
||||
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
|
||||
HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
|
||||
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
|
||||
@@ -263,13 +259,13 @@ impl WasmBezier {
|
||||
let mut bezier_svg_2 = String::new();
|
||||
beziers[1].to_svg(
|
||||
&mut bezier_svg_2,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
|
||||
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
|
||||
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
|
||||
);
|
||||
|
||||
wrap_svg_tag(format!("{original_bezier_svg}{bezier_svg_1}{bezier_svg_2}"))
|
||||
wrap_svg_tag(format!("{}{bezier_svg_1}{bezier_svg_2}", self.get_bezier_path()))
|
||||
}
|
||||
|
||||
pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String {
|
||||
@@ -279,7 +275,7 @@ impl WasmBezier {
|
||||
let mut trimmed_bezier_svg = String::new();
|
||||
trimmed_bezier.to_svg(
|
||||
&mut trimmed_bezier_svg,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
|
||||
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
|
||||
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
|
||||
@@ -570,8 +566,9 @@ impl WasmBezier {
|
||||
wrap_svg_tag(bezier_curves_svg)
|
||||
}
|
||||
|
||||
pub fn outline(&self, distance: f64) -> String {
|
||||
let outline_subpath = self.0.outline::<EmptyId>(distance);
|
||||
pub fn outline(&self, distance: f64, cap: i32) -> String {
|
||||
let cap = parse_cap(cap);
|
||||
let outline_subpath = self.0.outline::<EmptyId>(distance, cap);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
@@ -583,8 +580,9 @@ impl WasmBezier {
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
}
|
||||
|
||||
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> String {
|
||||
let outline_subpath = self.0.graduated_outline::<EmptyId>(start_distance, end_distance);
|
||||
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: i32) -> String {
|
||||
let cap = parse_cap(cap);
|
||||
let outline_subpath = self.0.graduated_outline::<EmptyId>(start_distance, end_distance, cap);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
@@ -596,8 +594,9 @@ impl WasmBezier {
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
}
|
||||
|
||||
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> String {
|
||||
let outline_subpath = self.0.skewed_outline::<EmptyId>(distance1, distance2, distance3, distance4);
|
||||
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: i32) -> String {
|
||||
let cap = parse_cap(cap);
|
||||
let outline_subpath = self.0.skewed_outline::<EmptyId>(distance1, distance2, distance3, distance4, cap);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod bezier;
|
||||
pub mod subpath;
|
||||
mod svg_drawing;
|
||||
mod utils;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::svg_drawing::*;
|
||||
use crate::utils::{parse_cap, parse_join};
|
||||
|
||||
use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue};
|
||||
|
||||
@@ -377,8 +378,9 @@ 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);
|
||||
pub fn offset(&self, distance: f64, join: i32) -> String {
|
||||
let join = parse_join(join);
|
||||
let offset_subpath = self.0.offset(distance, join);
|
||||
|
||||
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());
|
||||
@@ -386,8 +388,10 @@ impl WasmSubpath {
|
||||
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);
|
||||
pub fn outline(&self, distance: f64, join: i32, cap: i32) -> String {
|
||||
let join = parse_join(join);
|
||||
let cap = parse_cap(cap);
|
||||
let (outline_piece1, outline_piece2) = self.0.outline(distance, join, cap);
|
||||
|
||||
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());
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use bezier_rs::{Cap, Join};
|
||||
|
||||
pub fn parse_join(join: i32) -> Join {
|
||||
match join {
|
||||
0 => Join::Bevel,
|
||||
1 => Join::Miter,
|
||||
2 => Join::Round,
|
||||
_ => panic!("Unexpected Join value: '{}'", join),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_cap(cap: i32) -> Cap {
|
||||
match cap {
|
||||
0 => Cap::Butt,
|
||||
1 => Cap::Round,
|
||||
2 => Cap::Square,
|
||||
_ => panic!("Unexpected Cap value: '{}'", cap),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user