Skip to content

Commit 2ab55ec

Browse files
committed
feat: Add spacing method options to Repeat and Circular Repeat nodes
1 parent 94e5c8f commit 2ab55ec

File tree

6 files changed

+129
-9
lines changed

6 files changed

+129
-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
@@ -2175,6 +2175,8 @@ fn static_node_properties() -> NodeProperties {
21752175
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
21762176
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
21772177
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
2178+
map.insert("repeat_properties".to_string(), Box::new(node_properties::repeat_properties));
2179+
map.insert("circular_repeat_properties".to_string(), Box::new(node_properties::circular_repeat_properties));
21782180
map.insert(
21792181
"monitor_properties".to_string(),
21802182
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
@@ -25,6 +25,7 @@ use graphene_std::text::{Font, TextAlign};
2525
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
2626
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
2727
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
28+
use graphene_std::vector::{AngularSpacingMethod, RepeatSpacingMethod};
2829

2930
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
3031
let widget = TextLabel::new(text).widget_holder();
@@ -221,6 +222,8 @@ pub(crate) fn property_from_type(
221222
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
222223
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
223224
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
225+
Some(x) if x == TypeId::of::<RepeatSpacingMethod>() => enum_choice::<RepeatSpacingMethod>().for_socket(default_info).property_row(),
226+
Some(x) if x == TypeId::of::<AngularSpacingMethod>() => enum_choice::<AngularSpacingMethod>().for_socket(default_info).property_row(),
224227
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
225228
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
226229
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
@@ -1340,6 +1343,46 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
13401343
widgets
13411344
}
13421345

1346+
pub(crate) fn repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
1347+
const DIRECTION_INDEX: usize = 1;
1348+
const ANGLE_INDEX: usize = 2;
1349+
const COUNT_INDEX: usize = 3;
1350+
const SPACING_METHOD_INDEX: usize = 4;
1351+
1352+
let direction = vec2_widget(ParameterWidgetsInfo::new(node_id, DIRECTION_INDEX, true, context), "X", "Y", " px", None, false);
1353+
let angle = number_widget(ParameterWidgetsInfo::new(node_id, ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
1354+
let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int());
1355+
let spacing_method = enum_choice::<RepeatSpacingMethod>()
1356+
.for_socket(ParameterWidgetsInfo::new(node_id, SPACING_METHOD_INDEX, true, context))
1357+
.property_row();
1358+
1359+
vec![direction, LayoutGroup::Row { widgets: angle }, LayoutGroup::Row { widgets: count }, spacing_method]
1360+
}
1361+
1362+
pub(crate) fn circular_repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
1363+
const START_ANGLE_INDEX: usize = 1;
1364+
const END_ANGLE_INDEX: usize = 2;
1365+
const RADIUS_INDEX: usize = 3;
1366+
const COUNT_INDEX: usize = 4;
1367+
const ANGULAR_SPACING_METHOD_INDEX: usize = 5;
1368+
1369+
let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, START_ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
1370+
let end_angle = number_widget(ParameterWidgetsInfo::new(node_id, END_ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
1371+
let radius = number_widget(ParameterWidgetsInfo::new(node_id, RADIUS_INDEX, true, context), NumberInput::default().min(0.).unit(" px"));
1372+
let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int());
1373+
let angular_spacing_method = enum_choice::<AngularSpacingMethod>()
1374+
.for_socket(ParameterWidgetsInfo::new(node_id, ANGULAR_SPACING_METHOD_INDEX, true, context))
1375+
.property_row();
1376+
1377+
vec![
1378+
LayoutGroup::Row { widgets: start_angle },
1379+
LayoutGroup::Row { widgets: end_angle },
1380+
LayoutGroup::Row { widgets: radius },
1381+
LayoutGroup::Row { widgets: count },
1382+
angular_spacing_method,
1383+
]
1384+
}
1385+
13431386
pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
13441387
use graphene_std::vector::generator_nodes::spiral::*;
13451388

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: 79 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.), width / 2. * direction_normalized),
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,34 @@ 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+
let total_angle = end_rad - start_rad;
327+
let total = (count - 1) as f64;
328+
329+
let angular_pitch = match angular_spacing_method {
330+
AngularSpacingMethod::Span => total_angle / total.max(1.),
331+
AngularSpacingMethod::Pitch => total_angle / count as f64,
332+
};
275333

276334
let mut result_table = Table::new();
277335

278336
for index in 0..count {
279-
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians());
337+
let angle_rad = start_rad + index as f64 * angular_pitch;
338+
let angle = DAffine2::from_angle(angle_rad);
280339
let translation = DAffine2::from_translation(radius * DVec2::Y);
281340
let transform = angle * translation;
282341

@@ -2417,6 +2476,7 @@ mod test {
24172476
direction,
24182477
0.,
24192478
count,
2479+
super::RepeatSpacingMethod::Span,
24202480
)
24212481
.await;
24222482
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
@@ -2436,6 +2496,7 @@ mod test {
24362496
direction,
24372497
0.,
24382498
count,
2499+
super::RepeatSpacingMethod::Span,
24392500
)
24402501
.await;
24412502
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
@@ -2447,7 +2508,16 @@ mod test {
24472508
}
24482509
#[tokio::test]
24492510
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;
2511+
let repeated = super::circular_repeat(
2512+
Footprint::default(),
2513+
vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)),
2514+
45.,
2515+
360.,
2516+
4.,
2517+
8,
2518+
super::AngularSpacingMethod::Span,
2519+
)
2520+
.await;
24512521
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
24522522
let vector = vector_table.iter().next().unwrap().element;
24532523
assert_eq!(vector.region_manipulator_groups().count(), 8);
@@ -2588,7 +2658,7 @@ mod test {
25882658
#[tokio::test]
25892659
async fn morph() {
25902660
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;
2661+
let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2, super::RepeatSpacingMethod::Span).await;
25922662
let morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
25932663
let element = morphed.iter().next().unwrap().element;
25942664
assert_eq!(

0 commit comments

Comments
 (0)