Skip to content

Commit

Permalink
New node: Offset Path (#2030)
Browse files Browse the repository at this point in the history
* New node: Offset Path

* Fix CI
  • Loading branch information
Keavon authored Oct 11, 2024
1 parent 7a56af0 commit f7d83d2
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2462,6 +2462,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
("graphene_core::raster::adjustments::ChannelMixerNode", &node_properties::channel_mixer_properties as PropertiesLayout),
("graphene_core::vector::FillNode", &node_properties::fill_properties as PropertiesLayout),
("graphene_core::vector::StrokeNode", &node_properties::stroke_properties as PropertiesLayout),
("graphene_core::vector::OffsetPathNode", &node_properties::offset_path_properties as PropertiesLayout),
(
"graphene_core::raster::adjustments::SelectiveColorNode",
&node_properties::selective_color_properties as PropertiesLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ pub(crate) fn property_from_type(
Some("Percentage") => number_widget(document_node, node_id, index, name, number_input.percentage().min(min(0.)).max(max(100.)), true).into(),
Some("SignedPercentage") => number_widget(document_node, node_id, index, name, number_input.percentage().min(min(-100.)).max(max(100.)), true).into(),
Some("Angle") => number_widget(document_node, node_id, index, name, number_input.mode_range().min(min(-180.)).max(max(180.)).unit("°"), true).into(),
Some("PixelLength") => number_widget(document_node, node_id, index, name, number_input.min(min(0.)).unit("px"), true).into(),
Some("PixelLength") => number_widget(document_node, node_id, index, name, number_input.min(min(0.)).unit(" px"), true).into(),
Some("Length") => number_widget(document_node, node_id, index, name, number_input.min(min(0.)), true).into(),
Some("Fraction") => number_widget(document_node, node_id, index, name, number_input.min(min(0.)).max(max(1.)), true).into(),
Some("IntegerCount") => number_widget(document_node, node_id, index, name, number_input.int().min(min(1.)), true).into(),
Some("SeedValue") => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)), true).into(),
Some("Resolution") => vec2_widget(document_node, node_id, index, name, "W", "H", "px", Some(64.), add_blank_assist),
Some("Resolution") => vec2_widget(document_node, node_id, index, name, "W", "H", " px", Some(64.), add_blank_assist),

// For all other types, use TypeId-based matching
_ => {
Expand Down Expand Up @@ -1681,9 +1681,10 @@ pub(crate) fn rectangle_properties(document_node: &DocumentNode, node_id: NodeId
}

pub(crate) fn line_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let operand = |name: &str, index| vec2_widget(document_node, node_id, index, name, "X", "Y", "px", None, add_blank_assist);
let operand = |name: &str, index| vec2_widget(document_node, node_id, index, name, "X", "Y", " px", None, add_blank_assist);
vec![operand("Start", 1), operand("End", 2)]
}

pub(crate) fn spline_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row {
widgets: vec_dvec2_input(document_node, node_id, 1, "Points", TextInput::default().centered(true), true),
Expand Down Expand Up @@ -2555,14 +2556,14 @@ pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context
let miter_limit_index = 7;

let color = color_widget(document_node, node_id, color_index, "Color", ColorButton::default(), true);
let weight = number_widget(document_node, node_id, weight_index, "Weight", NumberInput::default().unit("px").min(0.), true);
let weight = number_widget(document_node, node_id, weight_index, "Weight", NumberInput::default().unit(" px").min(0.), true);

let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() {
Some(TaggedValue::VecF64(x)) => x,
_ => &vec![],
};
let dash_lengths = vec_f64_input(document_node, node_id, dash_lengths_index, "Dash Lengths", TextInput::default().centered(true), true);
let number_input = NumberInput::default().unit("px").disabled(dash_lengths_val.is_empty());
let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty());
let dash_offset = number_widget(document_node, node_id, dash_offset_index, "Dash Offset", number_input, true);
let line_cap = line_cap_widget(document_node, node_id, line_cap_index, "Line Cap", true);
let line_join = line_join_widget(document_node, node_id, line_join_index, "Line Join", true);
Expand All @@ -2584,6 +2585,26 @@ pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context
]
}

pub fn offset_path_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let distance_index = 1;
let line_join_index = 2;
let miter_limit_index = 3;

let number_input = NumberInput::default().unit(" px");
let distance = number_widget(document_node, node_id, distance_index, "Offset", number_input, true);

let line_join = line_join_widget(document_node, node_id, line_join_index, "Line Join", true);
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
Some(TaggedValue::LineJoin(x)) => x,
_ => &LineJoin::Miter,
};

let number_input = NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter);
let miter_limit = number_widget(document_node, node_id, miter_limit_index, "Miter Limit", number_input, true);

vec![LayoutGroup::Row { widgets: distance }, line_join, LayoutGroup::Row { widgets: miter_limit }]
}

pub(crate) fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let location = vec2_widget(document_node, node_id, 2, "Location", "X", "Y", " px", None, add_blank_assist);
let dimensions = vec2_widget(document_node, node_id, 3, "Dimensions", "W", "H", " px", None, add_blank_assist);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/floating-menus/ColorPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { clamp } from "@graphite/utility-functions/math";
import type { Editor } from "@graphite/wasm-communication/editor";
import { type HSV, type RGB, type FillChoice } from "@graphite/wasm-communication/messages";
import type { HSV, RGB, FillChoice } from "@graphite/wasm-communication/messages";
import { Color, Gradient } from "@graphite/wasm-communication/messages";
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
Expand Down
22 changes: 17 additions & 5 deletions libraries/bezier-rs/src/subpath/solvers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,17 +380,24 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
///
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
/// When the limit is exceeded, no manipulator group will be returned.
/// This value should be at least 1. If not, the default of 4 will be used.
/// This value should be greater than 0. If not, the default of 4 will be used.
pub(crate) fn miter_line_join(&self, other: &Subpath<PointId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<PointId>> {
let miter_limit = match miter_limit {
Some(miter_limit) if miter_limit >= 1. => miter_limit,
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
_ => 4.,
};
let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
let out_segment = other.get_segment(0).unwrap();
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
let in_segment = self.get_segment(self.len_segments() - 1)?;
let out_segment = other.get_segment(0)?;

let in_tangent = in_segment.tangent(TValue::Parametric(1.));
let out_tangent = out_segment.tangent(TValue::Parametric(0.));

if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
// Avoid panic from normalizing zero vectors
// TODO: Besides returning None, is there a more appropriate way to handle this?
return None;
}
let normalized_in_tangent = in_tangent.normalize();
let normalized_out_tangent = out_tangent.normalize();

Expand All @@ -400,11 +407,16 @@ impl<PointId: crate::Identifier> Subpath<PointId> {

let start_to_intersection = intersection - in_segment.end();
let intersection_to_end = out_segment.start() - intersection;
if start_to_intersection == DVec2::ZERO || intersection_to_end == DVec2::ZERO {
// Avoid panic from normalizing zero vectors
// TODO: Besides returning None, is there a more appropriate way to handle this?
return None;
}

// Draw the miter join if the intersection occurs in the correct direction with respect to the path
if start_to_intersection.normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
&& intersection_to_end.normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
&& miter_limit >= 1. / (start_to_intersection.angle_to(-intersection_to_end).abs() / 2.).sin()
&& miter_limit > f64::EPSILON / (start_to_intersection.angle_to(-intersection_to_end).abs() / 2.).sin()
{
return Some(ManipulatorGroup {
anchor: intersection,
Expand Down
55 changes: 50 additions & 5 deletions node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
use crate::vector::style::LineJoin;
use crate::{Color, GraphicGroup};

use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
Expand Down Expand Up @@ -60,7 +61,7 @@ async fn assign_colors<F: 'n + Send, T: VectorIterMut>(
let factor = match randomize {
true => rng.gen::<f64>(),
false => match repeat_every {
0 => i as f64 / (length - 1) as f64,
0 => i as f64 / (length - 1).max(1) as f64,
1 => 0.,
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
Expand Down Expand Up @@ -145,7 +146,7 @@ async fn stroke<F: 'n + Send, T: Into<Option<Color>> + 'n + Send>(
dash_lengths: Vec<f64>,
dash_offset: f64,
line_cap: crate::vector::style::LineCap,
line_join: crate::vector::style::LineJoin,
line_join: LineJoin,
#[default(4.)] miter_limit: f64,
) -> VectorData {
let mut vector_data = vector_data.eval(footprint).await;
Expand Down Expand Up @@ -277,6 +278,50 @@ async fn bounding_box<F: 'n + Send>(
result
}

#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn offset_path<F: 'n + Send>(
#[implementations(
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> VectorData,
Footprint -> VectorData,
)]
vector_data: impl Node<F, Output = VectorData>,
distance: f64,
line_join: LineJoin,
#[default(4.)] miter_limit: f64,
) -> VectorData {
let vector_data = vector_data.eval(footprint).await;

let subpaths = vector_data.stroke_bezier_paths();
let mut result = VectorData::empty();
result.style = vector_data.style.clone();
result.style.set_stroke_transform(DAffine2::IDENTITY);

// Perform operation on all subpaths in this shape.
for mut subpath in subpaths {
subpath.apply_transform(vector_data.transform);

// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
let subpath_out = subpath.offset(
-distance,
match line_join {
LineJoin::Miter => Join::Miter(Some(miter_limit)),
LineJoin::Bevel => Join::Bevel,
LineJoin::Round => Join::Round,
},
);

// One closed subpath, open path.
result.append_subpath(subpath_out, false);
}

result
}

#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn solidify_stroke<F: 'n + Send>(
#[implementations(
Expand Down Expand Up @@ -305,9 +350,9 @@ async fn solidify_stroke<F: 'n + Send>(
let subpath_out = subpath.outline(
stroke.weight / 2., // Diameter to radius.
match stroke.line_join {
crate::vector::style::LineJoin::Miter => Join::Miter(Some(stroke.line_join_miter_limit)),
crate::vector::style::LineJoin::Bevel => Join::Bevel,
crate::vector::style::LineJoin::Round => Join::Round,
LineJoin::Miter => Join::Miter(Some(stroke.line_join_miter_limit)),
LineJoin::Bevel => Join::Bevel,
LineJoin::Round => Join::Round,
},
match stroke.line_cap {
crate::vector::style::LineCap::Butt => Cap::Butt,
Expand Down

0 comments on commit f7d83d2

Please sign in to comment.