Skip to content

Commit 76938eb

Browse files
committed
Implement dynamic table attributes to generalize the graphic-specific Table type (#4050)
* Feature-gate serde derives behind cfg_attr in all runtime node graph type crates * Refactor Table to move its hard-coded fields into an attributes field * Encapsulate TableRow/TableRowRef/TableRowMut attribute fields behind accessor methods * Remove TaggedValue::GraphicUnused * Refactor Table<T> to use dynamic attributes instead fixed names * Fix code review soundness concerns * Add todo work * Replace row-oriented Table<T> API with column-oriented access * Fix attribute propagation bugs ---------
1 parent 324b9e6 commit 76938eb

75 files changed

Lines changed: 2327 additions & 1346 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ use crate::messages::portfolio::document::data_panel::DataPanelMessage;
44
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
55
use crate::messages::prelude::*;
66
use crate::messages::tool::tool_messages::tool_prelude::*;
7-
use glam::{Affine2, Vec2};
7+
use glam::{Affine2, DAffine2, Vec2};
88
use graph_craft::document::NodeId;
9-
use graphene_std::Color;
109
use graphene_std::Context;
1110
use graphene_std::gradient::GradientStops;
1211
use graphene_std::memo::IORecord;
1312
use graphene_std::raster_types::{CPU, GPU, Raster};
1413
use graphene_std::table::Table;
1514
use graphene_std::vector::Vector;
1615
use graphene_std::vector::style::{Fill, FillChoice};
16+
use graphene_std::{AlphaBlending, Color};
1717
use graphene_std::{Artboard, Graphic};
1818
use std::any::Any;
1919
use std::sync::Arc;
@@ -247,9 +247,9 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
247247
}
248248
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
249249
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
250-
if let Some(row) = self.get(index) {
250+
if let Some(element) = self.element(index) {
251251
data.current_depth += 1;
252-
let result = row.element.layout_with_breadcrumb(data);
252+
let result = element.layout_with_breadcrumb(data);
253253
data.current_depth -= 1;
254254
return result;
255255
} else {
@@ -258,23 +258,37 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
258258
}
259259
}
260260

261-
let mut rows = self
262-
.iter()
263-
.enumerate()
264-
.map(|(index, row)| {
265-
vec![
266-
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
267-
row.element.element_widget(index),
268-
TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_instance(),
269-
TextLabel::new(format!("{}", row.alpha_blending)).narrow(true).widget_instance(),
270-
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0)))
271-
.narrow(true)
272-
.widget_instance(),
273-
]
261+
let attribute_keys: Vec<String> = self.attribute_keys().map(str::to_string).collect();
262+
263+
let mut rows = (0..self.len())
264+
.map(|index| {
265+
let element = self.element(index).unwrap();
266+
let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), element.element_widget(index)];
267+
for key in &attribute_keys {
268+
let value = self
269+
.attribute_display_value(key, index, |ty| {
270+
if let Some(&value) = ty.downcast_ref::<DAffine2>() {
271+
Some(format_transform_matrix(value))
272+
} else if let Some(&value) = ty.downcast_ref::<DVec2>() {
273+
Some(format_dvec2(value))
274+
} else if let Some(&value) = ty.downcast_ref::<AlphaBlending>() {
275+
Some(format_alpha_blending(value))
276+
} else if let Some(&value) = ty.downcast_ref::<Option<NodeId>>() {
277+
Some(value.map_or_else(|| "-".to_string(), |id| id.to_string()))
278+
} else {
279+
None
280+
}
281+
})
282+
.unwrap_or_else(|| "-".to_string());
283+
cells.push(TextLabel::new(value).narrow(true).widget_instance());
284+
}
285+
cells
274286
})
275287
.collect::<Vec<_>>();
276288

277-
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
289+
let mut column_names = vec!["", "element"];
290+
column_names.extend(attribute_keys.iter().map(|s| s.as_str()));
291+
rows.insert(0, column_headings(&column_names));
278292

279293
vec![LayoutGroup::table(rows, false)]
280294
}
@@ -430,7 +444,7 @@ impl TableRowLayout for Vector {
430444
]);
431445
table_rows.push(vec![
432446
TextLabel::new("Stroke Transform").narrow(true).widget_instance(),
433-
TextLabel::new(format_transform_matrix(&stroke.transform)).narrow(true).widget_instance(),
447+
TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(),
434448
]);
435449
table_rows.push(vec![
436450
TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(),
@@ -695,7 +709,7 @@ impl TableRowLayout for DAffine2 {
695709
"Transform".to_string()
696710
}
697711
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
698-
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_instance()];
712+
let widgets = vec![TextLabel::new(format_transform_matrix(*self)).widget_instance()];
699713
vec![LayoutGroup::row(widgets)]
700714
}
701715
}
@@ -709,12 +723,12 @@ impl TableRowLayout for Affine2 {
709723
}
710724
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
711725
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
712-
let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_instance()];
726+
let widgets = vec![TextLabel::new(format_transform_matrix(matrix)).widget_instance()];
713727
vec![LayoutGroup::row(widgets)]
714728
}
715729
}
716730

717-
fn format_transform_matrix(transform: &DAffine2) -> String {
731+
fn format_transform_matrix(transform: DAffine2) -> String {
718732
let (scale, angle, translation) = if transform.matrix2.determinant().abs() <= f64::EPSILON {
719733
let [col_0, col_1] = transform.matrix2.to_cols_array_2d().map(|[x, y]| DVec2::new(x, y));
720734

@@ -748,3 +762,14 @@ fn format_dvec2(value: DVec2) -> String {
748762
let round = |x: f64| (x * 1e3).round() / 1e3;
749763
format!("({} px, {} px)", round(value.x), round(value.y))
750764
}
765+
766+
fn format_alpha_blending(value: AlphaBlending) -> String {
767+
let round = |x: f32| (x * 1e3).round() / 1e3;
768+
format!(
769+
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
770+
value.blend_mode,
771+
round(value.opacity * 100.),
772+
round(value.fill * 100.),
773+
if value.clip { "Yes" } else { "No" }
774+
)
775+
}

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,10 +713,10 @@ fn set_import_child_positions(
713713
let child_pos = IVec2::new(child_x, current_y);
714714

715715
if i == 0 {
716-
// Top of stack set to `Absolute` position
716+
// Top of stack: set to `Absolute` position
717717
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Absolute(child_pos), &[]);
718718
} else {
719-
// Below top set `Stack` with `y_offset` based on previous sibling's subtree extent
719+
// Below top: set `Stack` with `y_offset` based on previous sibling's subtree extent
720720
let prev_sibling_svg_index = n - i;
721721
let y_offset = child_extents_svg_order[prev_sibling_svg_index] + STACK_VERTICAL_GAP as u32;
722722
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Stack(y_offset), &[]);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,8 +1178,8 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
11781178
match &**tagged_value {
11791179
TaggedValue::Color(color_table) => widgets.push(
11801180
color_button
1181-
.value(match color_table.iter().next() {
1182-
Some(color) => FillChoice::Solid(*color.element),
1181+
.value(match color_table.element(0) {
1182+
Some(color) => FillChoice::Solid(*color),
11831183
None => FillChoice::None,
11841184
})
11851185
.on_update(update_value(
@@ -1192,8 +1192,8 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
11921192
),
11931193
TaggedValue::GradientTable(gradient_table) => widgets.push(
11941194
color_button
1195-
.value(match gradient_table.iter().next() {
1196-
Some(row) => FillChoice::Gradient(row.element.clone()),
1195+
.value(match gradient_table.element(0) {
1196+
Some(gradient) => FillChoice::Gradient(gradient.clone()),
11971197
None => FillChoice::Gradient(GradientStops::default()),
11981198
})
11991199
.on_update(update_value(

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,16 +1166,18 @@ impl OverlayContextInternal {
11661166
fn render_text_paths(&mut self, text_table: &Table<Vector>, font_color: &str, base_transform: kurbo::Affine) {
11671167
let color = Self::parse_color(font_color);
11681168

1169-
for row in text_table.iter() {
1169+
for index in 0..text_table.len() {
11701170
// Use the existing bezier_to_path infrastructure to convert Vector to BezPath
11711171
let mut path = BezPath::new();
11721172
let mut last_point = None;
1173+
let transform: DAffine2 = text_table.attribute_cloned_or_default("transform", index);
11731174

1174-
for (_, bezier, start_id, end_id) in row.element.segment_iter() {
1175+
let Some(element) = text_table.element(index) else { continue };
1176+
for (_, bezier, start_id, end_id) in element.segment_iter() {
11751177
let move_to = last_point != Some(start_id);
11761178
last_point = Some(end_id);
11771179

1178-
self.bezier_to_path(bezier, *row.transform, move_to, &mut path);
1180+
self.bezier_to_path(bezier, transform, move_to, &mut path);
11791181
}
11801182

11811183
// Render the path

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl DocumentMetadata {
104104
.any(|upstream| Some(upstream) == source)
105105
{
106106
use_local = false;
107-
info!("Local transform is invalid — using the identity for the local transform instead")
107+
info!("Local transform is invalid. Using the identity for the local transform instead.");
108108
}
109109
let local_transform = use_local.then(|| self.local_transforms.get(&layer.to_node()).copied()).flatten().unwrap_or_default();
110110

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5521,11 +5521,11 @@ impl NodeNetworkInterface {
55215521

55225522
match post_node_input {
55235523
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
5524-
// First child in the stack wire layer output to the post_node input
5524+
// First child in the stack: wire layer output to the post_node input
55255525
self.set_input_for_import(&post_node, layer_output, network_path);
55265526
}
55275527
NodeInput::Node { .. } => {
5528-
// Subsequent childinsert layer between post_node and its current upstream:
5528+
// Subsequent child: insert layer between post_node and its current upstream...
55295529
// 1. Disconnect old upstream from post_node, wire layer output to post_node
55305530
self.set_input_for_import(&post_node, layer_output, network_path);
55315531
// 2. Wire old upstream into layer's primary (stack) input

editor/src/messages/portfolio/document_migration.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,7 +1962,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
19621962
&& let TaggedValue::Vector(vector_table) = &**tagged_value
19631963
&& !vector_table.is_empty()
19641964
{
1965-
let vector = vector_table.iter().next()?.element;
1965+
let vector = vector_table.element(0)?;
19661966
let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(vector));
19671967

19681968
// Reset input 0 to the default exposed state
@@ -1981,9 +1981,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
19811981
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER)
19821982
&& let Some(NodeInput::Value { tagged_value, .. }) = node.inputs.get(1)
19831983
&& let TaggedValue::Raster(raster_table) = &**tagged_value
1984-
&& let Some(row) = raster_table.iter().next()
1984+
&& let Some(element) = raster_table.element(0)
19851985
{
1986-
let image = row.element.data().clone();
1986+
let image = element.data().clone();
19871987

19881988
document
19891989
.network_interface

editor/src/messages/tool/tool_messages/artboard_tool.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -646,10 +646,12 @@ mod test_artboard {
646646

647647
/// Check if all of the artboards exist in any ordering
648648
async fn has_artboards(editor: &mut EditorTestUtils, mut expected: Vec<ArtboardLayoutDocument>) {
649-
let artboards = get_artboards(editor)
650-
.await
651-
.iter()
652-
.map(|row| ArtboardLayoutDocument::new(row.element.location, row.element.dimensions))
649+
let artboards = get_artboards(editor).await;
650+
let artboards = (0..artboards.len())
651+
.map(|index| {
652+
let element = artboards.element(index).unwrap();
653+
ArtboardLayoutDocument::new(element.location, element.dimensions)
654+
})
653655
.collect::<Vec<_>>();
654656
assert_eq!(artboards.len(), expected.len(), "incorrect len: actual {:?}, expected {:?}", artboards, expected);
655657

editor/src/node_graph_executor/runtime.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use graphene_std::ops::Convert;
1515
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
1616
use graphene_std::raster_types::Raster;
1717
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
18-
use graphene_std::table::{Table, TableRow};
18+
use graphene_std::table::Table;
1919
use graphene_std::text::FontCache;
2020
use graphene_std::transform::RenderQuality;
2121
use graphene_std::vector::Vector;
@@ -441,9 +441,7 @@ impl NodeRuntime {
441441
// Vector table: vector modifications
442442
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
443443
// Insert the vector modify
444-
let default = TableRow::default();
445-
self.vector_modify
446-
.insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
444+
self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default());
447445
}
448446
// Other
449447
else {

node-graph/graph-craft/Cargo.toml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ wasm = [
2323
# Local dependencies
2424
dyn-any = { workspace = true }
2525
graphene-hash = { workspace = true }
26-
core-types = { workspace = true }
27-
brush-nodes = { workspace = true }
28-
graphene-core = { workspace = true }
29-
graphene-application-io = { workspace = true }
30-
rendering = { workspace = true }
31-
raster-nodes = { workspace = true }
32-
vector-nodes = { workspace = true }
33-
graphic-types = { workspace = true }
34-
text-nodes = { workspace = true }
26+
core-types = { workspace = true, features = ["serde"] }
27+
brush-nodes = { workspace = true, features = ["serde"] }
28+
graphene-core = { workspace = true, features = ["serde"] }
29+
graphene-application-io = { workspace = true, features = ["serde"] }
30+
rendering = { workspace = true, features = ["serde"] }
31+
raster-nodes = { workspace = true, features = ["serde"] }
32+
vector-nodes = { workspace = true, features = ["serde"] }
33+
graphic-types = { workspace = true, features = ["serde"] }
34+
text-nodes = { workspace = true, features = ["serde"] }
3535

3636
# Workspace dependencies
3737
log = { workspace = true }

0 commit comments

Comments
 (0)