Skip to content

Commit 435a6da

Browse files
0HyperCubeKeavon
andauthored
Add the first field-based nodes: 'Instance on Points', 'Instance Position', 'Instance Index', as well as 'Grid' (#2574)
* Basic fields * Add 'Extract XY' and 'Split Vector2' nodes * Add 'Instance Index' node * Fix test again * Improve grid generator to support rectangular as well * Avoid crashing --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 184c009 commit 435a6da

File tree

12 files changed

+515
-26
lines changed

12 files changed

+515
-26
lines changed

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use graphene_core::text::{Font, TypesettingConfig};
2020
use graphene_core::transform::Footprint;
2121
use graphene_core::vector::VectorDataTable;
2222
use graphene_core::*;
23+
use graphene_std::ops::XY;
2324
use std::collections::{HashMap, HashSet, VecDeque};
2425
#[cfg(feature = "gpu")]
2526
use wgpu_executor::{Bindgroup, CommandBuffer, PipelineLayout, ShaderHandle, ShaderInputFrame, WgpuShaderInput};
@@ -900,6 +901,75 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
900901
description: Cow::Borrowed("TODO"),
901902
properties: None,
902903
},
904+
DocumentNodeDefinition {
905+
identifier: "Split Vector2",
906+
category: "Math: Vector",
907+
node_template: NodeTemplate {
908+
document_node: DocumentNode {
909+
implementation: DocumentNodeImplementation::Network(NodeNetwork {
910+
exports: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
911+
nodes: [
912+
DocumentNode {
913+
inputs: vec![NodeInput::network(concrete!(ImageFrameTable<Color>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
914+
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")),
915+
manual_composition: Some(generic!(T)),
916+
..Default::default()
917+
},
918+
DocumentNode {
919+
inputs: vec![NodeInput::network(concrete!(ImageFrameTable<Color>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
920+
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")),
921+
manual_composition: Some(generic!(T)),
922+
..Default::default()
923+
},
924+
]
925+
.into_iter()
926+
.enumerate()
927+
.map(|(id, node)| (NodeId(id as u64), node))
928+
.collect(),
929+
930+
..Default::default()
931+
}),
932+
inputs: vec![NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true)],
933+
..Default::default()
934+
},
935+
persistent_node_metadata: DocumentNodePersistentMetadata {
936+
input_properties: vec!["Vector2".into()],
937+
output_names: vec!["X".to_string(), "Y".to_string()],
938+
has_primary_output: false,
939+
network_metadata: Some(NodeNetworkMetadata {
940+
persistent_metadata: NodeNetworkPersistentMetadata {
941+
node_metadata: [
942+
DocumentNodeMetadata {
943+
persistent_metadata: DocumentNodePersistentMetadata {
944+
display_name: "Extract XY".to_string(),
945+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
946+
..Default::default()
947+
},
948+
..Default::default()
949+
},
950+
DocumentNodeMetadata {
951+
persistent_metadata: DocumentNodePersistentMetadata {
952+
display_name: "Extract XY".to_string(),
953+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)),
954+
..Default::default()
955+
},
956+
..Default::default()
957+
},
958+
]
959+
.into_iter()
960+
.enumerate()
961+
.map(|(id, node)| (NodeId(id as u64), node))
962+
.collect(),
963+
..Default::default()
964+
},
965+
..Default::default()
966+
}),
967+
..Default::default()
968+
},
969+
},
970+
description: Cow::Borrowed("TODO"),
971+
properties: None,
972+
},
903973
DocumentNodeDefinition {
904974
identifier: "Brush",
905975
category: "Raster",
@@ -2889,6 +2959,7 @@ fn static_node_properties() -> NodeProperties {
28892959
map.insert("exposure_properties".to_string(), Box::new(node_properties::exposure_properties));
28902960
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
28912961
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
2962+
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
28922963
map.insert(
28932964
"identity_properties".to_string(),
28942965
Box::new(|_node_id, _context| node_properties::string_properties("The identity node simply passes its data through.")),

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

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ use graphene_core::vector::misc::CentroidType;
2121
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
2222
use graphene_std::animation::RealTimeMode;
2323
use graphene_std::application_io::TextureFrameTable;
24+
use graphene_std::ops::XY;
2425
use graphene_std::transform::Footprint;
2526
use graphene_std::vector::VectorDataTable;
26-
use graphene_std::vector::misc::BooleanOperation;
27+
use graphene_std::vector::misc::{BooleanOperation, GridType};
2728
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
2829
use graphene_std::{GraphicGroupTable, RasterFrame};
2930

@@ -168,6 +169,7 @@ pub(crate) fn property_from_type(
168169
Some(x) if x == TypeId::of::<RealTimeMode>() => real_time_mode(document_node, node_id, index, name, true),
169170
Some(x) if x == TypeId::of::<RedGreenBlue>() => color_channel(document_node, node_id, index, name, true),
170171
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => rgba_channel(document_node, node_id, index, name, true),
172+
Some(x) if x == TypeId::of::<XY>() => xy_components(document_node, node_id, index, name, true),
171173
Some(x) if x == TypeId::of::<NoiseType>() => noise_type(document_node, node_id, index, name, true),
172174
Some(x) if x == TypeId::of::<FractalType>() => fractal_type(document_node, node_id, index, name, true, false),
173175
Some(x) if x == TypeId::of::<CellularDistanceFunction>() => cellular_distance_function(document_node, node_id, index, name, true, false),
@@ -185,6 +187,7 @@ pub(crate) fn property_from_type(
185187
.widget_holder(),
186188
]
187189
.into(),
190+
Some(x) if x == TypeId::of::<GridType>() => grid_type_widget(document_node, node_id, index, name, true),
188191
Some(x) if x == TypeId::of::<LineCap>() => line_cap_widget(document_node, node_id, index, name, true),
189192
Some(x) if x == TypeId::of::<LineJoin>() => line_join_widget(document_node, node_id, index, name, true),
190193
Some(x) if x == TypeId::of::<FillType>() => vec![
@@ -567,6 +570,28 @@ pub fn vec2_widget(
567570
.widget_holder(),
568571
]);
569572
}
573+
Some(&TaggedValue::F64(value)) => {
574+
widgets.extend_from_slice(&[
575+
Separator::new(SeparatorType::Unrelated).widget_holder(),
576+
NumberInput::new(Some(value))
577+
.label(x)
578+
.unit(unit)
579+
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
580+
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
581+
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), value)), node_id, index))
582+
.on_commit(commit_value)
583+
.widget_holder(),
584+
Separator::new(SeparatorType::Related).widget_holder(),
585+
NumberInput::new(Some(value))
586+
.label(y)
587+
.unit(unit)
588+
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
589+
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
590+
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(value, input.value.unwrap())), node_id, index))
591+
.on_commit(commit_value)
592+
.widget_holder(),
593+
]);
594+
}
570595
_ => {}
571596
}
572597

@@ -745,6 +770,15 @@ pub fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize
745770
.widget_holder(),
746771
]);
747772
}
773+
Some(&TaggedValue::DVec2(dvec2)) => widgets.extend_from_slice(&[
774+
Separator::new(SeparatorType::Unrelated).widget_holder(),
775+
number_props
776+
// We use an arbitrary `y` instead of an arbitrary `x` here because the "Grid" node's "Spacing" value's height should be used from rectangular mode when transferred to "Y Spacing" in isometric mode
777+
.value(Some(dvec2.y))
778+
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index))
779+
.on_commit(commit_value)
780+
.widget_holder(),
781+
]),
748782
_ => {}
749783
}
750784

@@ -840,6 +874,33 @@ pub fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize,
840874
LayoutGroup::Row { widgets }.with_tooltip("Color Channel")
841875
}
842876

877+
pub fn xy_components(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
878+
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
879+
let Some(input) = document_node.inputs.get(index) else {
880+
log::warn!("A widget failed to be built because its node's input index is invalid.");
881+
return LayoutGroup::Row { widgets: vec![] };
882+
};
883+
if let Some(&TaggedValue::XY(mode)) = input.as_non_exposed_value() {
884+
let calculation_modes = [XY::X, XY::Y];
885+
let mut entries = Vec::with_capacity(calculation_modes.len());
886+
for method in calculation_modes {
887+
entries.push(
888+
MenuListEntry::new(format!("{method:?}"))
889+
.label(method.to_string())
890+
.on_update(update_value(move |_| TaggedValue::XY(method), node_id, index))
891+
.on_commit(commit_value),
892+
);
893+
}
894+
let entries = vec![entries];
895+
896+
widgets.extend_from_slice(&[
897+
Separator::new(SeparatorType::Unrelated).widget_holder(),
898+
DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(),
899+
]);
900+
}
901+
LayoutGroup::Row { widgets }.with_tooltip("X or Y Component of Vector2")
902+
}
903+
843904
// TODO: Generalize this instead of using a separate function per dropdown menu enum
844905
pub fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
845906
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
@@ -1064,6 +1125,31 @@ pub fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: No
10641125
LayoutGroup::Row { widgets }
10651126
}
10661127

1128+
pub fn grid_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
1129+
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
1130+
let Some(input) = document_node.inputs.get(index) else {
1131+
log::warn!("A widget failed to be built because its node's input index is invalid.");
1132+
return LayoutGroup::Row { widgets: vec![] };
1133+
};
1134+
if let Some(&TaggedValue::GridType(grid_type)) = input.as_non_exposed_value() {
1135+
let entries = [("Rectangular", GridType::Rectangular), ("Isometric", GridType::Isometric)]
1136+
.into_iter()
1137+
.map(|(name, val)| {
1138+
RadioEntryData::new(format!("{val:?}"))
1139+
.label(name)
1140+
.on_update(update_value(move |_| TaggedValue::GridType(val), node_id, index))
1141+
.on_commit(commit_value)
1142+
})
1143+
.collect();
1144+
1145+
widgets.extend_from_slice(&[
1146+
Separator::new(SeparatorType::Unrelated).widget_holder(),
1147+
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_holder(),
1148+
]);
1149+
}
1150+
LayoutGroup::Row { widgets }
1151+
}
1152+
10671153
pub fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
10681154
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
10691155
let Some(input) = document_node.inputs.get(index) else {
@@ -1484,6 +1570,52 @@ pub(crate) fn _gpu_map_properties(document_node: &DocumentNode, node_id: NodeId,
14841570
vec![LayoutGroup::Row { widgets: map }]
14851571
}
14861572

1573+
pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
1574+
let grid_type_index = 1;
1575+
let spacing_index = 2;
1576+
let angles_index = 3;
1577+
let rows_index = 4;
1578+
let columns_index = 5;
1579+
1580+
let document_node = match get_document_node(node_id, context) {
1581+
Ok(document_node) => document_node,
1582+
Err(err) => {
1583+
log::error!("Could not get document node in exposure_properties: {err}");
1584+
return Vec::new();
1585+
}
1586+
};
1587+
let grid_type = grid_type_widget(document_node, node_id, grid_type_index, "Grid Type", true);
1588+
1589+
let mut widgets = vec![grid_type];
1590+
1591+
let Some(grid_type_input) = document_node.inputs.get(grid_type_index) else {
1592+
log::warn!("A widget failed to be built because its node's input index is invalid.");
1593+
return vec![];
1594+
};
1595+
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
1596+
match grid_type {
1597+
GridType::Rectangular => {
1598+
let spacing = vec2_widget(document_node, node_id, spacing_index, "Spacing", "W", "H", " px", Some(0.), add_blank_assist);
1599+
widgets.push(spacing);
1600+
}
1601+
GridType::Isometric => {
1602+
let spacing = LayoutGroup::Row {
1603+
widgets: number_widget(document_node, node_id, spacing_index, "Spacing", NumberInput::default().label("H").min(0.).unit(" px"), true),
1604+
};
1605+
let angles = vec2_widget(document_node, node_id, angles_index, "Angles", "", "", "°", None, add_blank_assist);
1606+
widgets.extend([spacing, angles]);
1607+
}
1608+
}
1609+
}
1610+
1611+
let rows = number_widget(document_node, node_id, rows_index, "Rows", NumberInput::default().min(1.), true);
1612+
let columns = number_widget(document_node, node_id, columns_index, "Columns", NumberInput::default().min(1.), true);
1613+
1614+
widgets.extend([LayoutGroup::Row { widgets: rows }, LayoutGroup::Row { widgets: columns }]);
1615+
1616+
widgets
1617+
}
1618+
14871619
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
14881620
let document_node = match get_document_node(node_id, context) {
14891621
Ok(document_node) => document_node,

editor/src/messages/portfolio/document/overlays/grid_overlays.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
176176

177177
pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
178178
match document.snapping_state.grid.grid_type {
179-
GridType::Rectangle { spacing } => {
179+
GridType::Rectangular { spacing } => {
180180
if document.snapping_state.grid.dot_display {
181181
grid_overlay_rectangular_dot(document, overlay_context, spacing)
182182
} else {
@@ -238,14 +238,14 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
238238
RadioInput::new(vec![
239239
RadioEntryData::new("rectangular")
240240
.label("Rectangular")
241-
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)),
241+
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGULAR)),
242242
RadioEntryData::new("isometric")
243243
.label("Isometric")
244244
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)),
245245
])
246246
.min_width(200)
247247
.selected_index(Some(match grid.grid_type {
248-
GridType::Rectangle { .. } => 0,
248+
GridType::Rectangular { .. } => 0,
249249
GridType::Isometric { .. } => 1,
250250
}))
251251
.widget_holder(),
@@ -291,7 +291,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
291291
});
292292

293293
match grid.grid_type {
294-
GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row {
294+
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::Row {
295295
widgets: vec![
296296
TextLabel::new("Spacing").table_align(true).widget_holder(),
297297
Separator::new(SeparatorType::Unrelated).widget_holder(),
@@ -300,15 +300,15 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
300300
.unit(" px")
301301
.min(0.)
302302
.min_width(98)
303-
.on_update(update_origin(grid, |grid| grid.grid_type.rect_spacing().map(|spacing| &mut spacing.x)))
303+
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x)))
304304
.widget_holder(),
305305
Separator::new(SeparatorType::Related).widget_holder(),
306306
NumberInput::new(Some(spacing.y))
307307
.label("Y")
308308
.unit(" px")
309309
.min(0.)
310310
.min_width(98)
311-
.on_update(update_origin(grid, |grid| grid.grid_type.rect_spacing().map(|spacing| &mut spacing.y)))
311+
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y)))
312312
.widget_holder(),
313313
],
314314
}),

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,26 +160,33 @@ impl Default for PathSnapping {
160160

161161
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
162162
pub enum GridType {
163-
Rectangle { spacing: DVec2 },
164-
Isometric { y_axis_spacing: f64, angle_a: f64, angle_b: f64 },
163+
#[serde(alias = "Rectangle")]
164+
Rectangular {
165+
spacing: DVec2,
166+
},
167+
Isometric {
168+
y_axis_spacing: f64,
169+
angle_a: f64,
170+
angle_b: f64,
171+
},
165172
}
166173

167174
impl Default for GridType {
168175
fn default() -> Self {
169-
Self::RECTANGLE
176+
Self::RECTANGULAR
170177
}
171178
}
172179

173180
impl GridType {
174-
pub const RECTANGLE: Self = GridType::Rectangle { spacing: DVec2::ONE };
181+
pub const RECTANGULAR: Self = GridType::Rectangular { spacing: DVec2::ONE };
175182
pub const ISOMETRIC: Self = GridType::Isometric {
176183
y_axis_spacing: 1.,
177184
angle_a: 30.,
178185
angle_b: 30.,
179186
};
180-
pub fn rect_spacing(&mut self) -> Option<&mut DVec2> {
187+
pub fn rectangular_spacing(&mut self) -> Option<&mut DVec2> {
181188
match self {
182-
Self::Rectangle { spacing } => Some(spacing),
189+
Self::Rectangular { spacing } => Some(spacing),
183190
_ => None,
184191
}
185192
}

editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ impl GridSnapper {
3434
}
3535
lines
3636
}
37+
3738
// Isometric grid has 6 lines around a point, 2 y axis, 2 on the angle a, and 2 on the angle b.
3839
fn get_snap_lines_isometric(&self, document_point: DVec2, snap_data: &mut SnapData, y_axis_spacing: f64, angle_a: f64, angle_b: f64) -> Vec<Line> {
3940
let document = snap_data.document;
@@ -86,9 +87,10 @@ impl GridSnapper {
8687

8788
lines
8889
}
90+
8991
fn get_snap_lines(&self, document_point: DVec2, snap_data: &mut SnapData) -> Vec<Line> {
9092
match snap_data.document.snapping_state.grid.grid_type {
91-
GridType::Rectangle { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing),
93+
GridType::Rectangular { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing),
9294
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => self.get_snap_lines_isometric(document_point, snap_data, y_axis_spacing, angle_a, angle_b),
9395
}
9496
}

0 commit comments

Comments
 (0)