mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
feat: Add spacing method options to Repeat and Circular Repeat nodes
This commit is contained in:
parent
e73e524f3d
commit
3e7d6bbd62
6 changed files with 127 additions and 9 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2198,6 +2198,7 @@ dependencies = [
|
|||
"text-nodes",
|
||||
"tokio",
|
||||
"url",
|
||||
"vector-nodes",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"wgpu-executor",
|
||||
|
|
|
|||
|
|
@ -2181,6 +2181,8 @@ fn static_node_properties() -> NodeProperties {
|
|||
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
||||
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
|
||||
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
|
||||
map.insert("repeat_properties".to_string(), Box::new(node_properties::repeat_properties));
|
||||
map.insert("circular_repeat_properties".to_string(), Box::new(node_properties::circular_repeat_properties));
|
||||
map.insert(
|
||||
"monitor_properties".to_string(),
|
||||
Box::new(|_node_id, _context| node_properties::string_properties("Used internally by the editor to obtain a layer thumbnail.")),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ 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::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::{AngularSpacingMethod, RepeatSpacingMethod};
|
||||
|
||||
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
||||
let widget = TextLabel::new(text).widget_instance();
|
||||
|
|
@ -222,6 +223,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::<RepeatSpacingMethod>() => enum_choice::<RepeatSpacingMethod>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<AngularSpacingMethod>() => enum_choice::<AngularSpacingMethod>().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(),
|
||||
|
|
@ -1446,6 +1449,46 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||
widgets
|
||||
}
|
||||
|
||||
pub(crate) fn repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
const DIRECTION_INDEX: usize = 1;
|
||||
const ANGLE_INDEX: usize = 2;
|
||||
const COUNT_INDEX: usize = 3;
|
||||
const SPACING_METHOD_INDEX: usize = 4;
|
||||
|
||||
let direction = vec2_widget(ParameterWidgetsInfo::new(node_id, DIRECTION_INDEX, true, context), "X", "Y", " px", None, false);
|
||||
let angle = number_widget(ParameterWidgetsInfo::new(node_id, ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
|
||||
let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int());
|
||||
let spacing_method = enum_choice::<RepeatSpacingMethod>()
|
||||
.for_socket(ParameterWidgetsInfo::new(node_id, SPACING_METHOD_INDEX, true, context))
|
||||
.property_row();
|
||||
|
||||
vec![direction, LayoutGroup::Row { widgets: angle }, LayoutGroup::Row { widgets: count }, spacing_method]
|
||||
}
|
||||
|
||||
pub(crate) fn circular_repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
const START_ANGLE_INDEX: usize = 1;
|
||||
const END_ANGLE_INDEX: usize = 2;
|
||||
const RADIUS_INDEX: usize = 3;
|
||||
const COUNT_INDEX: usize = 4;
|
||||
const ANGULAR_SPACING_METHOD_INDEX: usize = 5;
|
||||
|
||||
let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, START_ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
|
||||
let end_angle = number_widget(ParameterWidgetsInfo::new(node_id, END_ANGLE_INDEX, true, context), NumberInput::default().unit("°"));
|
||||
let radius = number_widget(ParameterWidgetsInfo::new(node_id, RADIUS_INDEX, true, context), NumberInput::default().min(0.).unit(" px"));
|
||||
let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int());
|
||||
let angular_spacing_method = enum_choice::<AngularSpacingMethod>()
|
||||
.for_socket(ParameterWidgetsInfo::new(node_id, ANGULAR_SPACING_METHOD_INDEX, true, context))
|
||||
.property_row();
|
||||
|
||||
vec![
|
||||
LayoutGroup::Row { widgets: start_angle },
|
||||
LayoutGroup::Row { widgets: end_angle },
|
||||
LayoutGroup::Row { widgets: radius },
|
||||
LayoutGroup::Row { widgets: count },
|
||||
angular_spacing_method,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::generator_nodes::spiral::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ graphene-core = { workspace = true }
|
|||
graphene-application-io = { workspace = true }
|
||||
rendering = { workspace = true }
|
||||
raster-nodes = { workspace = true }
|
||||
vector-nodes = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
text-nodes = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use std::hash::Hash;
|
|||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
pub use std::sync::Arc;
|
||||
use vector_nodes::{AngularSpacingMethod, RepeatSpacingMethod};
|
||||
|
||||
pub struct TaggedValueTypeError;
|
||||
|
||||
|
|
@ -248,6 +249,8 @@ tagged_value! {
|
|||
ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm),
|
||||
PointSpacingType(vector::misc::PointSpacingType),
|
||||
SpiralType(vector::misc::SpiralType),
|
||||
RepeatSpacingMethod(RepeatSpacingMethod),
|
||||
AngularSpacingMethod(AngularSpacingMethod),
|
||||
#[serde(alias = "LineCap")]
|
||||
StrokeCap(vector::style::StrokeCap),
|
||||
#[serde(alias = "LineJoin")]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use core_types::registry::types::{Angle, IntegerCount, Length, Multiplier, Perce
|
|||
use core_types::table::{Table, TableRow, TableRowMut};
|
||||
use core_types::transform::{Footprint, Transform};
|
||||
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphic_types::Vector;
|
||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||
|
|
@ -225,8 +226,32 @@ where
|
|||
content
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Instancing"), path(core_types::vector))]
|
||||
async fn repeat<I: 'n + Send + Clone>(
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum RepeatSpacingMethod {
|
||||
#[default]
|
||||
#[serde(rename = "span")]
|
||||
Span,
|
||||
#[serde(rename = "envelope")]
|
||||
Envelope,
|
||||
#[serde(rename = "pitch")]
|
||||
Pitch,
|
||||
#[serde(rename = "gap")]
|
||||
Gap,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum AngularSpacingMethod {
|
||||
#[default]
|
||||
#[serde(rename = "span")]
|
||||
Span,
|
||||
#[serde(rename = "pitch")]
|
||||
Pitch,
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector), properties("repeat_properties"))]
|
||||
async fn repeat<I: 'n + Send + Clone + BoundingBox>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other graphical types.
|
||||
#[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>(
|
|||
direction: PixelSize,
|
||||
angle: Angle,
|
||||
#[default(5)] count: IntegerCount,
|
||||
#[default(RepeatSpacingMethod::Span)] spacing_method: RepeatSpacingMethod,
|
||||
) -> Table<I> {
|
||||
let angle = angle.to_radians();
|
||||
let count = count.max(1);
|
||||
let total = (count - 1) as f64;
|
||||
let direction_normalized = direction.normalize();
|
||||
|
||||
let width = if matches!(spacing_method, RepeatSpacingMethod::Envelope | RepeatSpacingMethod::Gap) {
|
||||
match instance.bounding_box(DAffine2::IDENTITY, false) {
|
||||
RenderBoundingBox::Rectangle([min, max]) => {
|
||||
let size = max - min;
|
||||
let dir_abs = direction_normalized.abs();
|
||||
size.x * dir_abs.x + size.y * dir_abs.y
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let (pitch, offset) = match spacing_method {
|
||||
RepeatSpacingMethod::Span => (direction.length() / total.max(1.), DVec2::ZERO),
|
||||
RepeatSpacingMethod::Envelope => ((direction.length() - width) / total.max(1.), DVec2::ZERO),
|
||||
RepeatSpacingMethod::Pitch => (direction.length(), DVec2::ZERO),
|
||||
RepeatSpacingMethod::Gap => (direction.length() + width, DVec2::ZERO),
|
||||
};
|
||||
|
||||
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 = index as f64 * angle / total.max(1.);
|
||||
let translation = offset + index as f64 * pitch * direction_normalized;
|
||||
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
|
||||
|
||||
for row in instance.iter() {
|
||||
|
|
@ -261,22 +308,32 @@ async fn repeat<I: 'n + Send + Clone>(
|
|||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Instancing"), path(core_types::vector))]
|
||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector), properties("circular_repeat_properties"))]
|
||||
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,
|
||||
#[default(0.)] start_angle: Angle,
|
||||
#[default(360.)] end_angle: Angle,
|
||||
#[unit(" px")]
|
||||
#[default(5)]
|
||||
radius: f64,
|
||||
#[default(5)] count: IntegerCount,
|
||||
#[default(AngularSpacingMethod::Span)] angular_spacing_method: AngularSpacingMethod,
|
||||
) -> Table<I> {
|
||||
let count = count.max(1);
|
||||
let start_rad = start_angle.to_radians();
|
||||
let end_rad = end_angle.to_radians();
|
||||
|
||||
let angular_pitch = match angular_spacing_method {
|
||||
AngularSpacingMethod::Span => TAU / count as f64,
|
||||
AngularSpacingMethod::Pitch => (end_rad - start_rad) / 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_rad = start_rad + index as f64 * angular_pitch;
|
||||
let angle = DAffine2::from_angle(angle_rad);
|
||||
let translation = DAffine2::from_translation(radius * DVec2::Y);
|
||||
let transform = angle * translation;
|
||||
|
||||
|
|
@ -2417,6 +2474,7 @@ mod test {
|
|||
direction,
|
||||
0.,
|
||||
count,
|
||||
super::RepeatSpacingMethod::Span,
|
||||
)
|
||||
.await;
|
||||
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
|
||||
|
|
@ -2436,6 +2494,7 @@ mod test {
|
|||
direction,
|
||||
0.,
|
||||
count,
|
||||
super::RepeatSpacingMethod::Span,
|
||||
)
|
||||
.await;
|
||||
let vector_table = super::flatten_path(Footprint::default(), repeated).await;
|
||||
|
|
@ -2447,7 +2506,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.,
|
||||
360.,
|
||||
4.,
|
||||
8,
|
||||
super::AngularSpacingMethod::Span,
|
||||
)
|
||||
.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);
|
||||
|
|
@ -2588,7 +2656,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, super::RepeatSpacingMethod::Span).await;
|
||||
let morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
|
||||
let element = morphed.iter().next().unwrap().element;
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue