mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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 ---------
This commit is contained in:
@@ -4,9 +4,8 @@ use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::{Affine2, Vec2};
|
||||
use glam::{Affine2, DAffine2, Vec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Context;
|
||||
use graphene_std::gradient::GradientStops;
|
||||
use graphene_std::memo::IORecord;
|
||||
@@ -14,6 +13,7 @@ use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::style::{Fill, FillChoice};
|
||||
use graphene_std::{AlphaBlending, Color};
|
||||
use graphene_std::{Artboard, Graphic};
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
@@ -247,9 +247,9 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
|
||||
}
|
||||
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
|
||||
if let Some(row) = self.get(index) {
|
||||
if let Some(element) = self.element(index) {
|
||||
data.current_depth += 1;
|
||||
let result = row.element.layout_with_breadcrumb(data);
|
||||
let result = element.layout_with_breadcrumb(data);
|
||||
data.current_depth -= 1;
|
||||
return result;
|
||||
} else {
|
||||
@@ -258,23 +258,37 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut rows = self
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, row)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
|
||||
row.element.element_widget(index),
|
||||
TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{}", row.alpha_blending)).narrow(true).widget_instance(),
|
||||
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0)))
|
||||
.narrow(true)
|
||||
.widget_instance(),
|
||||
]
|
||||
let attribute_keys: Vec<String> = self.attribute_keys().map(str::to_string).collect();
|
||||
|
||||
let mut rows = (0..self.len())
|
||||
.map(|index| {
|
||||
let element = self.element(index).unwrap();
|
||||
let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), element.element_widget(index)];
|
||||
for key in &attribute_keys {
|
||||
let value = self
|
||||
.attribute_display_value(key, index, |ty| {
|
||||
if let Some(&value) = ty.downcast_ref::<DAffine2>() {
|
||||
Some(format_transform_matrix(value))
|
||||
} else if let Some(&value) = ty.downcast_ref::<DVec2>() {
|
||||
Some(format_dvec2(value))
|
||||
} else if let Some(&value) = ty.downcast_ref::<AlphaBlending>() {
|
||||
Some(format_alpha_blending(value))
|
||||
} else if let Some(&value) = ty.downcast_ref::<Option<NodeId>>() {
|
||||
Some(value.map_or_else(|| "-".to_string(), |id| id.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
cells.push(TextLabel::new(value).narrow(true).widget_instance());
|
||||
}
|
||||
cells
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
|
||||
let mut column_names = vec!["", "element"];
|
||||
column_names.extend(attribute_keys.iter().map(|s| s.as_str()));
|
||||
rows.insert(0, column_headings(&column_names));
|
||||
|
||||
vec![LayoutGroup::table(rows, false)]
|
||||
}
|
||||
@@ -430,7 +444,7 @@ impl TableRowLayout for Vector {
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Transform").narrow(true).widget_instance(),
|
||||
TextLabel::new(format_transform_matrix(&stroke.transform)).narrow(true).widget_instance(),
|
||||
TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(),
|
||||
@@ -695,7 +709,7 @@ impl TableRowLayout for DAffine2 {
|
||||
"Transform".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_instance()];
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(*self)).widget_instance()];
|
||||
vec![LayoutGroup::row(widgets)]
|
||||
}
|
||||
}
|
||||
@@ -709,12 +723,12 @@ impl TableRowLayout for Affine2 {
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_instance()];
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(matrix)).widget_instance()];
|
||||
vec![LayoutGroup::row(widgets)]
|
||||
}
|
||||
}
|
||||
|
||||
fn format_transform_matrix(transform: &DAffine2) -> String {
|
||||
fn format_transform_matrix(transform: DAffine2) -> String {
|
||||
let (scale, angle, translation) = if transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
let [col_0, col_1] = transform.matrix2.to_cols_array_2d().map(|[x, y]| DVec2::new(x, y));
|
||||
|
||||
@@ -748,3 +762,14 @@ fn format_dvec2(value: DVec2) -> String {
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
format!("({} px, {} px)", round(value.x), round(value.y))
|
||||
}
|
||||
|
||||
fn format_alpha_blending(value: AlphaBlending) -> String {
|
||||
let round = |x: f32| (x * 1e3).round() / 1e3;
|
||||
format!(
|
||||
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
|
||||
value.blend_mode,
|
||||
round(value.opacity * 100.),
|
||||
round(value.fill * 100.),
|
||||
if value.clip { "Yes" } else { "No" }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -713,10 +713,10 @@ fn set_import_child_positions(
|
||||
let child_pos = IVec2::new(child_x, current_y);
|
||||
|
||||
if i == 0 {
|
||||
// Top of stack — set to `Absolute` position
|
||||
// Top of stack: set to `Absolute` position
|
||||
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Absolute(child_pos), &[]);
|
||||
} else {
|
||||
// Below top — set `Stack` with `y_offset` based on previous sibling's subtree extent
|
||||
// Below top: set `Stack` with `y_offset` based on previous sibling's subtree extent
|
||||
let prev_sibling_svg_index = n - i;
|
||||
let y_offset = child_extents_svg_order[prev_sibling_svg_index] + STACK_VERTICAL_GAP as u32;
|
||||
network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Stack(y_offset), &[]);
|
||||
|
||||
@@ -1178,8 +1178,8 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
||||
match &**tagged_value {
|
||||
TaggedValue::Color(color_table) => widgets.push(
|
||||
color_button
|
||||
.value(match color_table.iter().next() {
|
||||
Some(color) => FillChoice::Solid(*color.element),
|
||||
.value(match color_table.element(0) {
|
||||
Some(color) => FillChoice::Solid(*color),
|
||||
None => FillChoice::None,
|
||||
})
|
||||
.on_update(update_value(
|
||||
@@ -1192,8 +1192,8 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
||||
),
|
||||
TaggedValue::GradientTable(gradient_table) => widgets.push(
|
||||
color_button
|
||||
.value(match gradient_table.iter().next() {
|
||||
Some(row) => FillChoice::Gradient(row.element.clone()),
|
||||
.value(match gradient_table.element(0) {
|
||||
Some(gradient) => FillChoice::Gradient(gradient.clone()),
|
||||
None => FillChoice::Gradient(GradientStops::default()),
|
||||
})
|
||||
.on_update(update_value(
|
||||
|
||||
@@ -1166,16 +1166,18 @@ impl OverlayContextInternal {
|
||||
fn render_text_paths(&mut self, text_table: &Table<Vector>, font_color: &str, base_transform: kurbo::Affine) {
|
||||
let color = Self::parse_color(font_color);
|
||||
|
||||
for row in text_table.iter() {
|
||||
for index in 0..text_table.len() {
|
||||
// Use the existing bezier_to_path infrastructure to convert Vector to BezPath
|
||||
let mut path = BezPath::new();
|
||||
let mut last_point = None;
|
||||
let transform: DAffine2 = text_table.attribute_cloned_or_default("transform", index);
|
||||
|
||||
for (_, bezier, start_id, end_id) in row.element.segment_iter() {
|
||||
let Some(element) = text_table.element(index) else { continue };
|
||||
for (_, bezier, start_id, end_id) in element.segment_iter() {
|
||||
let move_to = last_point != Some(start_id);
|
||||
last_point = Some(end_id);
|
||||
|
||||
self.bezier_to_path(bezier, *row.transform, move_to, &mut path);
|
||||
self.bezier_to_path(bezier, transform, move_to, &mut path);
|
||||
}
|
||||
|
||||
// Render the path
|
||||
|
||||
@@ -104,7 +104,7 @@ impl DocumentMetadata {
|
||||
.any(|upstream| Some(upstream) == source)
|
||||
{
|
||||
use_local = false;
|
||||
info!("Local transform is invalid — using the identity for the local transform instead")
|
||||
info!("Local transform is invalid. Using the identity for the local transform instead.");
|
||||
}
|
||||
let local_transform = use_local.then(|| self.local_transforms.get(&layer.to_node()).copied()).flatten().unwrap_or_default();
|
||||
|
||||
|
||||
@@ -5521,11 +5521,11 @@ impl NodeNetworkInterface {
|
||||
|
||||
match post_node_input {
|
||||
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
|
||||
// First child in the stack — wire layer output to the post_node input
|
||||
// First child in the stack: wire layer output to the post_node input
|
||||
self.set_input_for_import(&post_node, layer_output, network_path);
|
||||
}
|
||||
NodeInput::Node { .. } => {
|
||||
// Subsequent child — insert layer between post_node and its current upstream:
|
||||
// Subsequent child: insert layer between post_node and its current upstream...
|
||||
// 1. Disconnect old upstream from post_node, wire layer output to post_node
|
||||
self.set_input_for_import(&post_node, layer_output, network_path);
|
||||
// 2. Wire old upstream into layer's primary (stack) input
|
||||
|
||||
@@ -1962,7 +1962,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
&& let TaggedValue::Vector(vector_table) = &**tagged_value
|
||||
&& !vector_table.is_empty()
|
||||
{
|
||||
let vector = vector_table.iter().next()?.element;
|
||||
let vector = vector_table.element(0)?;
|
||||
let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(vector));
|
||||
|
||||
// Reset input 0 to the default exposed state
|
||||
@@ -1981,9 +1981,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER)
|
||||
&& let Some(NodeInput::Value { tagged_value, .. }) = node.inputs.get(1)
|
||||
&& let TaggedValue::Raster(raster_table) = &**tagged_value
|
||||
&& let Some(row) = raster_table.iter().next()
|
||||
&& let Some(element) = raster_table.element(0)
|
||||
{
|
||||
let image = row.element.data().clone();
|
||||
let image = element.data().clone();
|
||||
|
||||
document
|
||||
.network_interface
|
||||
|
||||
@@ -646,10 +646,12 @@ mod test_artboard {
|
||||
|
||||
/// Check if all of the artboards exist in any ordering
|
||||
async fn has_artboards(editor: &mut EditorTestUtils, mut expected: Vec<ArtboardLayoutDocument>) {
|
||||
let artboards = get_artboards(editor)
|
||||
.await
|
||||
.iter()
|
||||
.map(|row| ArtboardLayoutDocument::new(row.element.location, row.element.dimensions))
|
||||
let artboards = get_artboards(editor).await;
|
||||
let artboards = (0..artboards.len())
|
||||
.map(|index| {
|
||||
let element = artboards.element(index).unwrap();
|
||||
ArtboardLayoutDocument::new(element.location, element.dimensions)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(artboards.len(), expected.len(), "incorrect len: actual {:?}, expected {:?}", artboards, expected);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use graphene_std::ops::Convert;
|
||||
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
|
||||
use graphene_std::raster_types::Raster;
|
||||
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
|
||||
use graphene_std::table::{Table, TableRow};
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::FontCache;
|
||||
use graphene_std::transform::RenderQuality;
|
||||
use graphene_std::vector::Vector;
|
||||
@@ -441,9 +441,7 @@ impl NodeRuntime {
|
||||
// Vector table: vector modifications
|
||||
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
|
||||
// Insert the vector modify
|
||||
let default = TableRow::default();
|
||||
self.vector_modify
|
||||
.insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
|
||||
self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default());
|
||||
}
|
||||
// Other
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user