Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use graphene_std::raster::{
use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
use graphene_std::vector::misc::{AngularSpacing, ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RepeatSpacing, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};

pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
Expand Down Expand Up @@ -151,7 +151,18 @@ pub(crate) fn property_from_type(
// Aliased types (ambiguous values)
Some("Percentage") | Some("PercentageF32") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
Some("SignedPercentage") | Some("SignedPercentageF32") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
Some("Angle") | Some("AngleF32") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
Some("Angle") | Some("AngleF32") => {
let mut angle_input = number_input.unit(unit.unwrap_or("°"));
if range.is_none() {
if let Some(number_min) = number_min {
angle_input = angle_input.min(number_min);
}
if let Some(number_max) = number_max {
angle_input = angle_input.max(number_max);
}
}
number_widget(default_info, angle_input).into()
}
Some("Multiplier") => number_widget(default_info, number_input.unit(unit.unwrap_or("x"))).into(),
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
Expand Down Expand Up @@ -223,6 +234,8 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<RepeatSpacing>() => enum_choice::<RepeatSpacing>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<AngularSpacing>() => enum_choice::<AngularSpacing>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
Expand Down
2 changes: 2 additions & 0 deletions node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ tagged_value! {
MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm),
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
PointSpacingType(vector::misc::PointSpacingType),
RepeatSpacing(vector::misc::RepeatSpacing),
AngularSpacing(vector::misc::AngularSpacing),
SpiralType(vector::misc::SpiralType),
#[serde(alias = "LineCap")]
StrokeCap(vector::style::StrokeCap),
Expand Down
20 changes: 20 additions & 0 deletions node-graph/libraries/vector-types/src/vector/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ pub enum PointSpacingType {
Quantity,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum RepeatSpacing {
Comment thread
watermelon-neko marked this conversation as resolved.
Envelope,
#[default]
Span,
Pitch,
Gap,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum AngularSpacing {
#[default]
Span,
Pitch,
}

pub fn point_to_dvec2(point: Point) -> DVec2 {
DVec2 { x: point.x, y: point.y }
}
Expand Down
125 changes: 117 additions & 8 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{AngularSpacing, CentroidType, ExtrudeJoiningAlgorithm, RepeatSpacing, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
Expand Down Expand Up @@ -235,16 +235,75 @@ async fn repeat<I: 'n + Send + Clone>(
direction: PixelSize,
angle: Angle,
#[default(5)] count: IntegerCount,
) -> Table<I> {
let angle = angle.to_radians();
spacing: RepeatSpacing,
) -> Table<I>
where
Table<I>: BoundingBox,
{
let count = count.max(1);
let total = (count - 1) as f64;

let direction_length = direction.length();
let direction_unit = if direction_length > 1e-10 { direction / direction_length } else { DVec2::ZERO };
let width_along_direction = if matches!(spacing, RepeatSpacing::Envelope | RepeatSpacing::Gap) && direction_unit != DVec2::ZERO {
match instance.bounding_box(DAffine2::IDENTITY, false) {
RenderBoundingBox::Rectangle(bounds) => {
let corners = [
DVec2::new(bounds[0].x, bounds[0].y),
DVec2::new(bounds[0].x, bounds[1].y),
DVec2::new(bounds[1].x, bounds[0].y),
DVec2::new(bounds[1].x, bounds[1].y),
];
let mut min_proj = f64::INFINITY;
let mut max_proj = f64::NEG_INFINITY;
for corner in corners {
let proj = corner.dot(direction_unit);
min_proj = min_proj.min(proj);
max_proj = max_proj.max(proj);
}
(max_proj - min_proj).abs()
}
_ => 0.,
}
} else {
0.
};

let width_vector = direction_unit * width_along_direction;
let translation_step = match spacing {
RepeatSpacing::Span => {
if total > 0. {
direction / total
} else {
DVec2::ZERO
}
}
RepeatSpacing::Pitch => direction,
RepeatSpacing::Envelope => {
if total > 0. {
(direction - width_vector) / total
} else {
DVec2::ZERO
}
}
RepeatSpacing::Gap => direction + width_vector,
};
let angle_step = match spacing {
RepeatSpacing::Pitch => angle.to_radians(),
_ => {
if total > 0. {
angle.to_radians() / total
} else {
0.
}
}
};

let mut result_table = Table::new();

for index in 0..count {
let angle = index as f64 * angle / total;
let translation = index as f64 * direction / total;
let angle = angle_step * index as f64;
let translation = translation_step * index as f64;
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);

for row in instance.iter() {
Expand All @@ -266,17 +325,31 @@ async fn circular_repeat<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
start_angle: Angle,
delta_angle: Angle,
spacing: AngularSpacing,
#[unit(" px")]
#[default(5)]
radius: f64,
#[default(5)] count: IntegerCount,
) -> Table<I> {
let count = count.max(1);
let total = (count - 1) as f64;
let start_angle = start_angle.to_radians();
let angle_step = match spacing {
AngularSpacing::Span => {
let span = delta_angle.to_radians();
if total > 0. { span / total } else { 0. }
}
AngularSpacing::Pitch => {
let pitch = delta_angle.to_radians();
if pitch.abs() > 1e-10 { pitch } else { TAU / count as f64 }
}
};

let mut result_table = Table::new();

for index in 0..count {
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians());
let angle = DAffine2::from_angle(start_angle + angle_step * index as f64);
let translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation;

Expand Down Expand Up @@ -2429,6 +2502,7 @@ mod test {
direction,
0.,
count,
RepeatSpacing::Span,
)
.await;
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
Expand All @@ -2448,6 +2522,7 @@ mod test {
direction,
0.,
count,
RepeatSpacing::Span,
)
.await;
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
Expand All @@ -2459,7 +2534,16 @@ mod test {
}
#[tokio::test]
async fn circular_repeat() {
let repeated = super::circular_repeat(Footprint::default(), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)), 45., 4., 8).await;
let repeated = super::circular_repeat(
Footprint::default(),
vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)),
45.,
45.,
AngularSpacing::Pitch,
4.,
8,
)
.await;
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
assert_eq!(vector.region_manipulator_groups().count(), 8);
Expand All @@ -2474,6 +2558,31 @@ mod test {
}
}
#[tokio::test]
async fn circular_repeat_span() {
let repeated = super::circular_repeat(
Footprint::default(),
vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)),
45.,
315.,
AngularSpacing::Span,
4.,
8,
)
.await;
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
assert_eq!(vector.region_manipulator_groups().count(), 8);

for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
let expected_angle = 45. + index as f64 * 45.;

let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();

assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
#[tokio::test]
async fn bounding_box() {
let bounding_box = super::bounding_box((), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await;
let bounding_box = bounding_box.iter().next().unwrap().element;
Expand Down Expand Up @@ -2600,7 +2709,7 @@ mod test {
#[tokio::test]
async fn morph() {
let rectangle = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2).await;
let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2, RepeatSpacing::Span).await;
let morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
let element = morphed.iter().next().unwrap().element;
assert_eq!(
Expand Down