Skip to content

Commit 561b671

Browse files
authored
Add transform-related nodes to improve transformation abilities (#2893)
* Improve transformation abilities with transform-related nodes * Fix Transform -> Merge and Transform -> Artboard connections
1 parent 8f26c5c commit 561b671

File tree

11 files changed

+257
-29
lines changed

11 files changed

+257
-29
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,11 +1303,11 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
13031303
},
13041304
DocumentNodeDefinition {
13051305
identifier: "Transform",
1306-
category: "General",
1306+
category: "Math: Transform",
13071307
node_template: NodeTemplate {
13081308
document_node: DocumentNode {
13091309
inputs: vec![
1310-
NodeInput::value(TaggedValue::VectorData(VectorDataTable::default()), true),
1310+
NodeInput::value(TaggedValue::DAffine2(DAffine2::default()), true),
13111311
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
13121312
NodeInput::value(TaggedValue::F64(0.), false),
13131313
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
@@ -1317,7 +1317,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
13171317
exports: vec![NodeInput::node(NodeId(1), 0)],
13181318
nodes: [
13191319
DocumentNode {
1320-
inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0)],
1320+
inputs: vec![NodeInput::network(generic!(T), 0)],
13211321
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
13221322
manual_composition: Some(generic!(T)),
13231323
skip_deduplication: true,
@@ -1374,7 +1374,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
13741374
..Default::default()
13751375
}),
13761376
input_metadata: vec![
1377-
("Vector Data", "TODO").into(),
1377+
("Value", "TODO").into(),
13781378
InputMetadata::with_name_description_override(
13791379
"Translation",
13801380
"TODO",

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

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use graphene_std::raster::{
2121
};
2222
use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
2323
use graphene_std::text::Font;
24-
use graphene_std::transform::{Footprint, ReferencePoint};
24+
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
2525
use graphene_std::vector::VectorDataTable;
2626
use graphene_std::vector::misc::GridType;
2727
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
@@ -176,6 +176,7 @@ pub(crate) fn property_from_type(
176176
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
177177
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
178178
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None, false),
179+
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
179180
// ==========================
180181
// PRIMITIVE COLLECTION TYPES
181182
// ==========================
@@ -504,6 +505,126 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
504505
last.clone()
505506
}
506507

508+
pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widgets: &mut Vec<LayoutGroup>) -> LayoutGroup {
509+
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
510+
511+
let mut location_widgets = start_widgets(parameter_widgets_info);
512+
location_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
513+
514+
let mut rotation_widgets = vec![TextLabel::new("").widget_holder()];
515+
add_blank_assist(&mut rotation_widgets);
516+
rotation_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
517+
518+
let mut scale_widgets = vec![TextLabel::new("").widget_holder()];
519+
add_blank_assist(&mut scale_widgets);
520+
scale_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
521+
522+
let Some(document_node) = document_node else { return LayoutGroup::default() };
523+
let Some(input) = document_node.inputs.get(index) else {
524+
log::warn!("A widget failed to be built because its node's input index is invalid.");
525+
return Vec::new().into();
526+
};
527+
528+
let widgets = if let Some(&TaggedValue::DAffine2(transform)) = input.as_non_exposed_value() {
529+
let translation = transform.translation;
530+
let rotation = transform.decompose_rotation();
531+
let scale = transform.decompose_scale();
532+
533+
location_widgets.extend_from_slice(&[
534+
NumberInput::new(Some(translation.x))
535+
.label("X")
536+
.unit(" px")
537+
.on_update(update_value(
538+
move |x: &NumberInput| {
539+
let mut transform = transform;
540+
transform.translation.x = x.value.unwrap_or(transform.translation.x);
541+
TaggedValue::DAffine2(transform)
542+
},
543+
node_id,
544+
index,
545+
))
546+
.on_commit(commit_value)
547+
.widget_holder(),
548+
Separator::new(SeparatorType::Related).widget_holder(),
549+
NumberInput::new(Some(translation.y))
550+
.label("Y")
551+
.unit(" px")
552+
.on_update(update_value(
553+
move |y: &NumberInput| {
554+
let mut transform = transform;
555+
transform.translation.y = y.value.unwrap_or(transform.translation.y);
556+
TaggedValue::DAffine2(transform)
557+
},
558+
node_id,
559+
index,
560+
))
561+
.on_commit(commit_value)
562+
.widget_holder(),
563+
]);
564+
565+
rotation_widgets.extend_from_slice(&[NumberInput::new(Some(rotation.to_degrees()))
566+
.unit("°")
567+
.mode(NumberInputMode::Range)
568+
.range_min(Some(-180.))
569+
.range_max(Some(180.))
570+
.on_update(update_value(
571+
move |r: &NumberInput| {
572+
let transform = DAffine2::from_scale_angle_translation(scale, r.value.map(|r| r.to_radians()).unwrap_or(rotation), translation);
573+
TaggedValue::DAffine2(transform)
574+
},
575+
node_id,
576+
index,
577+
))
578+
.on_commit(commit_value)
579+
.widget_holder()]);
580+
581+
scale_widgets.extend_from_slice(&[
582+
NumberInput::new(Some(scale.x))
583+
.label("W")
584+
.unit("x")
585+
.on_update(update_value(
586+
move |w: &NumberInput| {
587+
let transform = DAffine2::from_scale_angle_translation(DVec2::new(w.value.unwrap_or(scale.x), scale.y), rotation, translation);
588+
TaggedValue::DAffine2(transform)
589+
},
590+
node_id,
591+
index,
592+
))
593+
.on_commit(commit_value)
594+
.widget_holder(),
595+
Separator::new(SeparatorType::Related).widget_holder(),
596+
NumberInput::new(Some(scale.y))
597+
.label("H")
598+
.unit("x")
599+
.on_update(update_value(
600+
move |h: &NumberInput| {
601+
let transform = DAffine2::from_scale_angle_translation(DVec2::new(scale.x, h.value.unwrap_or(scale.y)), rotation, translation);
602+
TaggedValue::DAffine2(transform)
603+
},
604+
node_id,
605+
index,
606+
))
607+
.on_commit(commit_value)
608+
.widget_holder(),
609+
]);
610+
611+
vec![
612+
LayoutGroup::Row { widgets: location_widgets },
613+
LayoutGroup::Row { widgets: rotation_widgets },
614+
LayoutGroup::Row { widgets: scale_widgets },
615+
]
616+
} else {
617+
vec![LayoutGroup::Row { widgets: location_widgets }]
618+
};
619+
620+
if let Some((last, rest)) = widgets.split_last() {
621+
*extra_widgets = rest.to_vec();
622+
last.clone()
623+
} else {
624+
LayoutGroup::default()
625+
}
626+
}
627+
507628
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
508629
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
509630

@@ -1345,9 +1466,9 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
13451466

13461467
pub(crate) fn node_no_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
13471468
let text = if context.network_interface.is_layer(&node_id, context.selection_network_path) {
1348-
"Layer has no properties"
1469+
"Layer has no parameters"
13491470
} else {
1350-
"Node has no properties"
1471+
"Node has no parameters"
13511472
};
13521473
string_properties(text)
13531474
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ impl FrontendGraphDataType {
2727
| TaggedValue::OptionalDVec2(_)
2828
| TaggedValue::F64Array4(_)
2929
| TaggedValue::VecF64(_)
30-
| TaggedValue::VecDVec2(_) => Self::Number,
30+
| TaggedValue::VecDVec2(_)
31+
| TaggedValue::DAffine2(_) => Self::Number,
3132
TaggedValue::GraphicGroup(_) | TaggedValue::GraphicElement(_) => Self::Group, // TODO: Is GraphicElement supposed to be included here?
3233
TaggedValue::ArtboardGroup(_) => Self::Artboard,
3334
_ => Self::General,

node-graph/gcore/src/graphic_element.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ impl From<RasterDataTable<GPU>> for GraphicGroupTable {
100100
Self::new(GraphicElement::RasterDataGPU(raster_data_table))
101101
}
102102
}
103+
impl From<DAffine2> for GraphicGroupTable {
104+
fn from(_: DAffine2) -> Self {
105+
GraphicGroupTable::default()
106+
}
107+
}
103108

104109
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
105110
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
@@ -118,6 +123,12 @@ impl Default for GraphicElement {
118123
}
119124
}
120125

126+
impl From<DAffine2> for GraphicElement {
127+
fn from(_: DAffine2) -> Self {
128+
GraphicElement::default()
129+
}
130+
}
131+
121132
impl GraphicElement {
122133
pub fn as_group(&self) -> Option<&GraphicGroupTable> {
123134
match self {
@@ -351,6 +362,7 @@ async fn to_element<Data: Into<GraphicElement> + 'n>(
351362
VectorDataTable,
352363
RasterDataTable<CPU>,
353364
RasterDataTable<GPU>,
365+
DAffine2,
354366
)]
355367
data: Data,
356368
) -> GraphicElement {
@@ -463,6 +475,7 @@ async fn to_artboard<Data: Into<GraphicGroupTable> + 'n>(
463475
Context -> VectorDataTable,
464476
Context -> RasterDataTable<CPU>,
465477
Context -> RasterDataTable<GPU>,
478+
Context -> DAffine2,
466479
)]
467480
contents: impl Node<Context<'static>, Output = Data>,
468481
label: String,

node-graph/gcore/src/instances.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::AlphaBlending;
2+
use crate::transform::ApplyTransform;
23
use crate::uuid::NodeId;
34
use dyn_any::StaticType;
45
use glam::DAffine2;
@@ -136,6 +137,20 @@ impl<T: Hash> Hash for Instances<T> {
136137
}
137138
}
138139

140+
impl<T> ApplyTransform for Instances<T> {
141+
fn apply_transform(&mut self, modification: &DAffine2) {
142+
for transform in &mut self.transform {
143+
*transform *= *modification;
144+
}
145+
}
146+
147+
fn left_apply_transform(&mut self, modification: &DAffine2) {
148+
for transform in &mut self.transform {
149+
*transform = *modification * *transform;
150+
}
151+
}
152+
}
153+
139154
impl<T: PartialEq> PartialEq for Instances<T> {
140155
fn eq(&self, other: &Self) -> bool {
141156
self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) }

node-graph/gcore/src/transform.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@ use glam::{DAffine2, DMat2, DVec2};
66

77
pub trait Transform {
88
fn transform(&self) -> DAffine2;
9+
910
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
1011
pivot
1112
}
13+
1214
fn decompose_scale(&self) -> DVec2 {
13-
DVec2::new(
14-
self.transform().transform_vector2((1., 0.).into()).length(),
15-
self.transform().transform_vector2((0., 1.).into()).length(),
16-
)
15+
DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
16+
}
17+
18+
/// Requires that the transform does not contain any skew.
19+
fn decompose_rotation(&self) -> f64 {
20+
let rotation_matrix = (self.transform() * DAffine2::from_scale(self.decompose_scale().recip())).matrix2;
21+
let rotation = -rotation_matrix.mul_vec2(DVec2::X).angle_to(DVec2::X);
22+
if rotation == -0. { 0. } else { rotation }
1723
}
1824
}
1925

@@ -141,12 +147,21 @@ impl std::hash::Hash for Footprint {
141147

142148
pub trait ApplyTransform {
143149
fn apply_transform(&mut self, modification: &DAffine2);
150+
fn left_apply_transform(&mut self, modification: &DAffine2);
144151
}
145152
impl<T: TransformMut> ApplyTransform for T {
146153
fn apply_transform(&mut self, &modification: &DAffine2) {
147154
*self.transform_mut() = self.transform() * modification
148155
}
156+
fn left_apply_transform(&mut self, &modification: &DAffine2) {
157+
*self.transform_mut() = modification * self.transform()
158+
}
149159
}
150-
impl ApplyTransform for () {
151-
fn apply_transform(&mut self, &_modification: &DAffine2) {}
160+
impl ApplyTransform for DVec2 {
161+
fn apply_transform(&mut self, modification: &DAffine2) {
162+
*self = modification.transform_point2(*self);
163+
}
164+
fn left_apply_transform(&mut self, modification: &DAffine2) {
165+
*self = modification.inverse().transform_point2(*self);
166+
}
152167
}

node-graph/gcore/src/transform_nodes.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ use core::f64;
77
use glam::{DAffine2, DVec2};
88

99
#[node_macro::node(category(""))]
10-
async fn transform<T: 'n + 'static>(
10+
async fn transform<T: ApplyTransform + 'n + 'static>(
1111
ctx: impl Ctx + CloneVarArgs + ExtractAll,
1212
#[implementations(
13+
Context -> DAffine2,
14+
Context -> DVec2,
1315
Context -> VectorDataTable,
1416
Context -> GraphicGroupTable,
1517
Context -> RasterDataTable<CPU>,
1618
Context -> RasterDataTable<GPU>,
1719
)]
18-
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
20+
value: impl Node<Context<'static>, Output = T>,
1921
translate: DVec2,
2022
rotate: f64,
2123
scale: DVec2,
2224
skew: DVec2,
23-
) -> Instances<T> {
25+
) -> T {
2426
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
2527

2628
let footprint = ctx.try_footprint().copied();
@@ -31,11 +33,9 @@ async fn transform<T: 'n + 'static>(
3133
ctx = ctx.with_footprint(footprint);
3234
}
3335

34-
let mut transform_target = transform_target.eval(ctx.into_context()).await;
36+
let mut transform_target = value.eval(ctx.into_context()).await;
3537

36-
for data_transform in transform_target.instance_mut_iter() {
37-
*data_transform.transform = matrix * *data_transform.transform;
38-
}
38+
transform_target.left_apply_transform(&matrix);
3939

4040
transform_target
4141
}
@@ -52,6 +52,40 @@ fn replace_transform<Data, TransformInput: Transform>(
5252
data
5353
}
5454

55+
#[node_macro::node(category("Math: Transform"), path(graphene_core::vector))]
56+
async fn extract_transform<T>(
57+
_: impl Ctx,
58+
#[implementations(
59+
GraphicGroupTable,
60+
VectorDataTable,
61+
RasterDataTable<CPU>,
62+
RasterDataTable<GPU>,
63+
)]
64+
vector_data: Instances<T>,
65+
) -> DAffine2 {
66+
vector_data.instance_ref_iter().next().map(|vector_data| *vector_data.transform).unwrap_or_default()
67+
}
68+
69+
#[node_macro::node(category("Math: Transform"))]
70+
fn invert_transform(_: impl Ctx, transform: DAffine2) -> DAffine2 {
71+
transform.inverse()
72+
}
73+
74+
#[node_macro::node(category("Math: Transform"))]
75+
fn decompose_translation(_: impl Ctx, transform: DAffine2) -> DVec2 {
76+
transform.translation
77+
}
78+
79+
#[node_macro::node(category("Math: Transform"))]
80+
fn decompose_rotation(_: impl Ctx, transform: DAffine2) -> f64 {
81+
transform.decompose_rotation()
82+
}
83+
84+
#[node_macro::node(category("Math: Transform"))]
85+
fn decompose_scale(_: impl Ctx, transform: DAffine2) -> DVec2 {
86+
transform.decompose_scale()
87+
}
88+
5589
#[node_macro::node(category("Debug"))]
5690
async fn boundless_footprint<T: 'n + 'static>(
5791
ctx: impl Ctx + CloneVarArgs + ExtractAll,

0 commit comments

Comments
 (0)