Skip to content

Commit 3e7d6bb

Browse files
committed
feat: Add spacing method options to Repeat and Circular Repeat nodes
1 parent e73e524 commit 3e7d6bb

File tree

6 files changed

+127
-9
lines changed

6 files changed

+127
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,6 +2181,8 @@ fn static_node_properties() -> NodeProperties {
21812181
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
21822182
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
21832183
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
2184+
map.insert("repeat_properties".to_string(), Box::new(node_properties::repeat_properties));
2185+
map.insert("circular_repeat_properties".to_string(), Box::new(node_properties::circular_repeat_properties));
21842186
map.insert(
21852187
"monitor_properties".to_string(),
21862188
Box::new(|_node_id, _context| node_properties::string_properties("Used internally by the editor to obtain a layer thumbnail.")),

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use graphene_std::text::{Font, TextAlign};
2626
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
2727
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
2828
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
29+
use graphene_std::vector::{AngularSpacingMethod, RepeatSpacingMethod};
2930

3031
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
3132
let widget = TextLabel::new(text).widget_instance();
@@ -222,6 +223,8 @@ pub(crate) fn property_from_type(
222223
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
223224
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
224225
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
226+
Some(x) if x == TypeId::of::<RepeatSpacingMethod>() => enum_choice::<RepeatSpacingMethod>().for_socket(default_info).property_row(),
227+
Some(x) if x == TypeId::of::<AngularSpacingMethod>() => enum_choice::<AngularSpacingMethod>().for_socket(default_info).property_row(),
225228
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
226229
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
227230
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
@@ -1446,6 +1449,46 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
14461449
widgets
14471450
}
14481451

1452+
pub(crate) fn repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
1453+
const DIRECTION_INDEX: usize = 1;
1454+
const ANGLE_INDEX: usize = 2;
1455+
const COUNT_INDEX: usize = 3;
1456+
const SPACING_METHOD_INDEX: usize = 4;
1457+
1458+
let direction = vec2_widget(ParameterWidgetsInfo::new(node_id, DIRECTION_INDEX, true, context), "X", "Y", " px", None, false);
1459+
let angle = number_widget(ParameterWidgetsInfo::new(node_id, ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
1460+
let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int());
1461+
let spacing_method = enum_choice::<RepeatSpacingMethod>()
1462+
.for_socket(ParameterWidgetsInfo::new(node_id, SPACING_METHOD_INDEX, true, context))
1463+
.property_row();
1464+
1465+
vec![direction, LayoutGroup::Row { widgets: angle }, LayoutGroup::Row { widgets: count }, spacing_method]
1466+
}
1467+
1468+
pub(crate) fn circular_repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
1469+
const START_ANGLE_INDEX: usize = 1;
1470+
const END_ANGLE_INDEX: usize = 2;
1471+
const RADIUS_INDEX: usize = 3;
1472+
const COUNT_INDEX: usize = 4;
1473+
const ANGULAR_SPACING_METHOD_INDEX: usize = 5;
1474+
1475+
let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, START_ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
1476+
let end_angle = number_widget(ParameterWidgetsInfo::new(node_id, END_ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
1477+
let radius = number_widget(ParameterWidgetsInfo::new(node_id, RADIUS_INDEX, true, context), NumberInput::default().min(0.).unit(" px"));
1478+
let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int());
1479+
let angular_spacing_method = enum_choice::<AngularSpacingMethod>()
1480+
.for_socket(ParameterWidgetsInfo::new(node_id, ANGULAR_SPACING_METHOD_INDEX, true, context))
1481+
.property_row();
1482+
1483+
vec![
1484+
LayoutGroup::Row { widgets: start_angle },
1485+
LayoutGroup::Row { widgets: end_angle },
1486+
LayoutGroup::Row { widgets: radius },
1487+
LayoutGroup::Row { widgets: count },
1488+
angular_spacing_method,
1489+
]
1490+
}
1491+
14491492
pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
14501493
use graphene_std::vector::generator_nodes::spiral::*;
14511494

node-graph/graph-craft/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ graphene-core = { workspace = true }
2121
graphene-application-io = { workspace = true }
2222
rendering = { workspace = true }
2323
raster-nodes = { workspace = true }
24+
vector-nodes = { workspace = true }
2425
graphic-types = { workspace = true }
2526
text-nodes = { workspace = true }
2627

node-graph/graph-craft/src/document/value.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::hash::Hash;
2626
use std::marker::PhantomData;
2727
use std::str::FromStr;
2828
pub use std::sync::Arc;
29+
use vector_nodes::{AngularSpacingMethod, RepeatSpacingMethod};
2930

3031
pub struct TaggedValueTypeError;
3132

@@ -248,6 +249,8 @@ tagged_value! {
248249
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
249250
PointSpacingType(vector::misc::PointSpacingType),
250251
SpiralType(vector::misc::SpiralType),
252+
RepeatSpacingMethod(RepeatSpacingMethod),
253+
AngularSpacingMethod(AngularSpacingMethod),
251254
#[serde(alias = "LineCap")]
252255
StrokeCap(vector::style::StrokeCap),
253256
#[serde(alias = "LineJoin")]

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use core_types::registry::types::{Angle, IntegerCount, Length, Multiplier, Perce
55
use core_types::table::{Table, TableRow, TableRowMut};
66
use core_types::transform::{Footprint, Transform};
77
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
8+
use dyn_any::DynAny;
89
use glam::{DAffine2, DVec2};
910
use graphic_types::Vector;
1011
use graphic_types::raster_types::{CPU, GPU, Raster};
@@ -225,8 +226,32 @@ where
225226
content
226227
}
227228

228-
#[node_macro::node(category("Instancing"), path(core_types::vector))]
229-
async fn repeat<I: 'n + Send + Clone>(
229+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
230+
#[widget(Radio)]
231+
pub enum RepeatSpacingMethod {
232+
#[default]
233+
#[serde(rename = "span")]
234+
Span,
235+
#[serde(rename = "envelope")]
236+
Envelope,
237+
#[serde(rename = "pitch")]
238+
Pitch,
239+
#[serde(rename = "gap")]
240+
Gap,
241+
}
242+
243+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
244+
#[widget(Radio)]
245+
pub enum AngularSpacingMethod {
246+
#[default]
247+
#[serde(rename = "span")]
248+
Span,
249+
#[serde(rename = "pitch")]
250+
Pitch,
251+
}
252+
253+
#[node_macro::node(category("Instancing"), path(graphene_core::vector), properties("repeat_properties"))]
254+
async fn repeat<I: 'n + Send + Clone + BoundingBox>(
230255
_: impl Ctx,
231256
// TODO: Implement other graphical types.
232257
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
@@ -235,16 +260,38 @@ async fn repeat<I: 'n + Send + Clone>(
235260
direction: PixelSize,
236261
angle: Angle,
237262
#[default(5)] count: IntegerCount,
263+
#[default(RepeatSpacingMethod::Span)] spacing_method: RepeatSpacingMethod,
238264
) -> Table<I> {
239265
let angle = angle.to_radians();
240266
let count = count.max(1);
241267
let total = (count - 1) as f64;
268+
let direction_normalized = direction.normalize();
269+
270+
let width = if matches!(spacing_method, RepeatSpacingMethod::Envelope | RepeatSpacingMethod::Gap) {
271+
match instance.bounding_box(DAffine2::IDENTITY, false) {
272+
RenderBoundingBox::Rectangle([min, max]) => {
273+
let size = max - min;
274+
let dir_abs = direction_normalized.abs();
275+
size.x * dir_abs.x + size.y * dir_abs.y
276+
}
277+
_ => 0.0,
278+
}
279+
} else {
280+
0.0
281+
};
282+
283+
let (pitch, offset) = match spacing_method {
284+
RepeatSpacingMethod::Span => (direction.length() / total.max(1.), DVec2::ZERO),
285+
RepeatSpacingMethod::Envelope => ((direction.length() - width) / total.max(1.), DVec2::ZERO),
286+
RepeatSpacingMethod::Pitch => (direction.length(), DVec2::ZERO),
287+
RepeatSpacingMethod::Gap => (direction.length() + width, DVec2::ZERO),
288+
};
242289

243290
let mut result_table = Table::new();
244291

245292
for index in 0..count {
246-
let angle = index as f64 * angle / total;
247-
let translation = index as f64 * direction / total;
293+
let angle = index as f64 * angle / total.max(1.);
294+
let translation = offset + index as f64 * pitch * direction_normalized;
248295
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
249296

250297
for row in instance.iter() {
@@ -261,22 +308,32 @@ async fn repeat<I: 'n + Send + Clone>(
261308
result_table
262309
}
263310

264-
#[node_macro::node(category("Instancing"), path(core_types::vector))]
311+
#[node_macro::node(category("Instancing"), path(graphene_core::vector), properties("circular_repeat_properties"))]
265312
async fn circular_repeat<I: 'n + Send + Clone>(
266313
_: impl Ctx,
267314
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
268-
start_angle: Angle,
315+
#[default(0.)] start_angle: Angle,
316+
#[default(360.)] end_angle: Angle,
269317
#[unit(" px")]
270318
#[default(5)]
271319
radius: f64,
272320
#[default(5)] count: IntegerCount,
321+
#[default(AngularSpacingMethod::Span)] angular_spacing_method: AngularSpacingMethod,
273322
) -> Table<I> {
274323
let count = count.max(1);
324+
let start_rad = start_angle.to_radians();
325+
let end_rad = end_angle.to_radians();
326+
327+
let angular_pitch = match angular_spacing_method {
328+
AngularSpacingMethod::Span => TAU / count as f64,
329+
AngularSpacingMethod::Pitch => (end_rad - start_rad) / count as f64,
330+
};
275331

276332
let mut result_table = Table::new();
277333

278334
for index in 0..count {
279-
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians());
335+
let angle_rad = start_rad + index as f64 * angular_pitch;
336+
let angle = DAffine2::from_angle(angle_rad);
280337
let translation = DAffine2::from_translation(radius * DVec2::Y);
281338
let transform = angle * translation;
282339

@@ -2417,6 +2474,7 @@ mod test {
24172474
direction,
24182475
0.,
24192476
count,
2477+
super::RepeatSpacingMethod::Span,
24202478
)
24212479
.await;
24222480
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
@@ -2436,6 +2494,7 @@ mod test {
24362494
direction,
24372495
0.,
24382496
count,
2497+
super::RepeatSpacingMethod::Span,
24392498
)
24402499
.await;
24412500
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
@@ -2447,7 +2506,16 @@ mod test {
24472506
}
24482507
#[tokio::test]
24492508
async fn circular_repeat() {
2450-
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;
2509+
let repeated = super::circular_repeat(
2510+
Footprint::default(),
2511+
vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)),
2512+
45.,
2513+
360.,
2514+
4.,
2515+
8,
2516+
super::AngularSpacingMethod::Span,
2517+
)
2518+
.await;
24512519
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
24522520
let vector = vector_table.iter().next().unwrap().element;
24532521
assert_eq!(vector.region_manipulator_groups().count(), 8);
@@ -2588,7 +2656,7 @@ mod test {
25882656
#[tokio::test]
25892657
async fn morph() {
25902658
let rectangle = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
2591-
let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2).await;
2659+
let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2, super::RepeatSpacingMethod::Span).await;
25922660
let morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
25932661
let element = morphed.iter().next().unwrap().element;
25942662
assert_eq!(

0 commit comments

Comments
 (0)