From fb71ea426d7f770e276f20a1a1584a0ee44a292c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 22 Aug 2026 14:25:31 +0000 Subject: [PATCH] Bridge legacy seams and convert the transform family --- .../document/graph_operation/utility_types.rs | 8 +- .../node_graph/document_node_definitions.rs | 18 ++- .../node_graph/node_graph_message_handler.rs | 2 +- .../document/node_graph/node_properties.rs | 24 ++-- .../document/storage_tests/test_support.rs | 23 +++ .../graph_modification_utils.rs | 20 +-- .../common_functionality/stroke_options.rs | 3 +- .../tool/tool_messages/artboard_tool.rs | 5 +- .../tool/tool_messages/gradient_tool.rs | 4 +- .../interpreted-executor/src/node_registry.rs | 16 +++ node-graph/nodes/graphic/src/graphic.rs | 38 ++++- .../nodes/transform/src/transform_nodes.rs | 134 +++++++++--------- .../vector/src/vector_modification_nodes.rs | 4 +- node-graph/nodes/vector/src/vector_nodes.rs | 85 +++++------ 14 files changed, 227 insertions(+), 157 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4920f0aeb5..e5211280bf 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -457,7 +457,7 @@ impl<'a> ModifyInputsContext<'a> { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; - let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::>::INDEX); + let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::::INDEX); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX); self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true); @@ -474,7 +474,7 @@ impl<'a> ModifyInputsContext<'a> { // Skip the rerender on all but the last input so the whole update triggers a single graph run self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::>::INDEX), + InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::::INDEX), NodeInput::value(TaggedValue::Gradient(gradient), false), true, ); @@ -715,7 +715,7 @@ impl<'a> ModifyInputsContext<'a> { return; }; - let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::>::INDEX); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), true); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true); @@ -729,7 +729,7 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false); - let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::>::INDEX); + let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(stroke.dash_lengths), false), true); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 39188756ff..f3662b6bd9 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -159,9 +159,9 @@ fn document_node_definitions() -> HashMap HashMap HashMap MessageHandler> for NodeG let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)); let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER)); let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)); - let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::>::INDEX; + let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::::INDEX; let is_shape_generator_node = reference.as_ref().is_some_and(|r| { [regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER] .into_iter() diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index be62ab3e70..16848c4909 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2457,13 +2457,13 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte Other, } - let connector = InputConnector::node(node_id, FillInput::>::INDEX); + let connector = InputConnector::node(node_id, FillInput::::INDEX); let input_type = context.network_interface.input_type(&connector, context.selection_network_path); // Pass blank_assist=false because the assist slot is filled below ("Reverse Stops" button when in gradient mode) - let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::>::INDEX, false, context)); + let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::::INDEX, false, context)); - if get_document_node(node_id, context).is_ok_and(|node| node.inputs.get(FillInput::>::INDEX).is_some_and(|input| input.is_exposed())) { + if get_document_node(node_id, context).is_ok_and(|node| node.inputs.get(FillInput::::INDEX).is_some_and(|input| input.is_exposed())) { return vec![LayoutGroup::row(widgets_first_row)]; } @@ -2474,7 +2474,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte let fill = match input_type.compiled_nested_type() { Some(ty) if ty == &concrete!(List) => { if let Ok(document_node) = get_document_node(node_id, context) { - let color = match document_node.inputs[FillInput::>::INDEX].as_value() { + let color = match document_node.inputs[FillInput::::INDEX].as_value() { Some(&TaggedValue::Color(c)) => c, _ => None, }; @@ -2525,7 +2525,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte let reverse_button = IconButton::new("Reverse", 24) .tooltip_label("Reverse Stops") .tooltip_description("Reverse the gradient color stops.") - .on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput::>::INDEX)) + .on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput::::INDEX)) .widget_instance(); widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets_first_row.push(reverse_button); @@ -2549,7 +2549,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte messages: Box::new([ NodeGraphMessage::SetInputValue { node_id, - input_index: FillInput::>::INDEX, + input_index: FillInput::::INDEX, value: TaggedValue::Color(color), } .into(), @@ -2566,7 +2566,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte messages: Box::new([ NodeGraphMessage::SetInputValue { node_id, - input_index: FillInput::>::INDEX, + input_index: FillInput::::INDEX, value: TaggedValue::Gradient(gradient.clone()), } .into(), @@ -2607,11 +2607,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte let entries = vec![ RadioEntryData::new("solid") .label("Solid") - .on_update(update_value(move |_| TaggedValue::Color(backup_color), node_id, FillInput::>::INDEX)) + .on_update(update_value(move |_| TaggedValue::Color(backup_color), node_id, FillInput::::INDEX)) .on_commit(commit_value), RadioEntryData::new("gradient") .label("Gradient") - .on_update(update_value(move |_| TaggedValue::Gradient(backup_gradient.clone()), node_id, FillInput::>::INDEX)) + .on_update(update_value(move |_| TaggedValue::Gradient(backup_gradient.clone()), node_id, FillInput::::INDEX)) .on_commit(commit_value), ]; @@ -2717,14 +2717,14 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - _ => &StrokeJoin::Miter, }; - let has_dash_lengths = match &document_node.inputs[DashLengthsInput::>::INDEX].as_value() { + let has_dash_lengths = match &document_node.inputs[DashLengthsInput::INDEX].as_value() { Some(TaggedValue::F64Array(values)) => values.is_empty(), _ => true, }; let miter_limit_disabled = join_value != &StrokeJoin::Miter; let color = color_widget( - ParameterWidgetsInfo::new(node_id, PaintInput::>::INDEX, true, context), + ParameterWidgetsInfo::new(node_id, PaintInput::::INDEX, true, context), crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(), ); let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.)); @@ -2745,7 +2745,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - .property_row(); let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths); let dash_lengths = array_of_number_widget( - ParameterWidgetsInfo::new(node_id, DashLengthsInput::>::INDEX, true, context), + ParameterWidgetsInfo::new(node_id, DashLengthsInput::INDEX, true, context), TextInput::default().centered(true), ); let number_input = disabled_number_input; diff --git a/editor/src/messages/portfolio/document/storage_tests/test_support.rs b/editor/src/messages/portfolio/document/storage_tests/test_support.rs index bc511de704..bf898b7552 100644 --- a/editor/src/messages/portfolio/document/storage_tests/test_support.rs +++ b/editor/src/messages/portfolio/document/storage_tests/test_support.rs @@ -20,6 +20,29 @@ pub fn load_demo(file_name: &str) -> DocumentMessageHandler { DocumentMessageHandler::deserialize_document(&content).unwrap_or_else(|e| panic!("Failed to deserialize {path}: {e:?}")) } +/// Dev tool for the flip: runs each demo through the full open-time migration and writes the +/// re-saved form into the directory named by `DEMO_OUT`, so the CLI can render migrated copies +/// while the checked-in originals stay pristine until the flip's re-save. +#[test] +#[ignore = "dev tool: set DEMO_OUT and run explicitly"] +fn migrate_demo_artwork_into_demo_out() { + use crate::messages::portfolio::document_migration::{document_migration_reset_node_definition, document_migration_string_preprocessing, document_migration_upgrades}; + let out_dir = std::env::var("DEMO_OUT").expect("set DEMO_OUT to the output directory"); + for entry in std::fs::read_dir("../demo-artwork").unwrap() { + let path = entry.unwrap().path(); + if path.extension().and_then(|extension| extension.to_str()) != Some("graphite") { + continue; + } + let content = std::fs::read_to_string(&path).unwrap(); + let content = document_migration_string_preprocessing(content); + let reset = document_migration_reset_node_definition(&content); + let mut document = DocumentMessageHandler::deserialize_document(&content).unwrap_or_else(|e| panic!("Failed to deserialize {}: {e:?}", path.display())); + document_migration_upgrades(&mut document, reset); + let out = format!("{out_dir}/{}", path.file_name().unwrap().to_string_lossy()); + std::fs::write(out, document.serialize_document()).unwrap(); + } +} + /// Walk every node in every nested network and collect `(network_path, local_id)` pairs, so a test can /// iterate every node addressable from the metadata side. pub fn node_paths(interface: &NodeNetworkInterface) -> Vec<(Vec, NodeId)> { diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c68bc5ac56..1ad5a38970 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -275,14 +275,14 @@ pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeN pub fn get_fill_node_id_with_direct_fill_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?; let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?; - matches!(fill_node.inputs.get(graphene_std::vector::fill::FillInput::>::INDEX)?, NodeInput::Value { .. }).then_some(fill_node_id) + matches!(fill_node.inputs.get(graphene_std::vector::fill::FillInput::::INDEX)?, NodeInput::Value { .. }).then_some(fill_node_id) } /// Determine the input connector where the gradient chain enters the layer. /// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input. pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector { if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) { - InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::>::INDEX) + InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::::INDEX) } else { InputConnector::node(layer.to_node(), 1) } @@ -303,7 +303,7 @@ pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_i pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?; let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?; - let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::>::INDEX)? else { + let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::::INDEX)? else { return None; }; Some(*node_id) @@ -317,7 +317,7 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe .document_network() .nodes .get(&fill_node_id) - .and_then(|node| node.inputs.get(graphene_std::vector::fill::FillInput::>::INDEX)) + .and_then(|node| node.inputs.get(graphene_std::vector::fill::FillInput::::INDEX)) .and_then(|input| input.as_value()) .and_then(|value| if let TaggedValue::Gradient(gradient) = value { Some(gradient.clone()) } else { None }); } @@ -363,7 +363,7 @@ pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool { /// Get the current fill of a layer from the closest "Fill" node. pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?; - let &TaggedValue::Color(color) = inputs.get(graphene_std::vector::fill::FillInput::>::INDEX)?.as_value()? else { + let &TaggedValue::Color(color) = inputs.get(graphene_std::vector::fill::FillInput::::INDEX)?.as_value()? else { return None; }; color @@ -570,7 +570,7 @@ pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNe Some(TaggedValue::PaintOrder(value)) => *value, _ => PaintOrder::default(), }; - let dash_lengths = match read(graphene_std::vector::stroke::DashLengthsInput::>::INDEX) { + let dash_lengths = match read(graphene_std::vector::stroke::DashLengthsInput::INDEX) { Some(TaggedValue::F64Array(value)) => value.clone(), _ => Vec::new(), }; @@ -638,7 +638,7 @@ pub struct FillNodeGradient { pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOnce() -> [DVec2; 2]) -> Option { use graphene_std::vector::fill; - let TaggedValue::Gradient(stops) = fill_node.inputs.get(fill::FillInput::>::INDEX)?.as_value()? else { + let TaggedValue::Gradient(stops) = fill_node.inputs.get(fill::FillInput::::INDEX)?.as_value()? else { return None; }; let gradient_type = match fill_node.inputs.get(fill::GradientTypeInput::INDEX).and_then(|input| input.as_value()) { @@ -665,7 +665,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn } /// Returns the stroke color from a layer's upstream Stroke node. pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option> { - let color_index = graphene_std::vector::stroke::PaintInput::>::INDEX; + let color_index = graphene_std::vector::stroke::PaintInput::::INDEX; let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?; if let TaggedValue::Color(color) = tagged { Some(*color) } else { None } } @@ -699,7 +699,7 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option>::INDEX)?.as_value()? { + match fill_node.inputs.get(graphene_std::vector::fill::FillInput::::INDEX)?.as_value()? { &TaggedValue::Color(color) => Some(color.map_or(FillChoice::None, FillChoice::Solid)), TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())), _ => None, @@ -825,7 +825,7 @@ pub fn set_stroke_color_for_selected_layers(color: Option, weight: f64, d let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect(); for layer in layers { if let Some(node_id) = get_stroke_id(layer, &document.network_interface) { - let input_index = graphene_std::vector::stroke::PaintInput::>::INDEX; + let input_index = graphene_std::vector::stroke::PaintInput::::INDEX; let value = TaggedValue::Color(color); responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value }); } else { diff --git a/editor/src/messages/tool/common_functionality/stroke_options.rs b/editor/src/messages/tool/common_functionality/stroke_options.rs index a5dd9ef21e..a6b9e7744c 100644 --- a/editor/src/messages/tool/common_functionality/stroke_options.rs +++ b/editor/src/messages/tool/common_functionality/stroke_options.rs @@ -5,7 +5,6 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use graph_craft::document::value::TaggedValue; use graphene_std::NodeInputDecleration; use graphene_std::choice_type::ChoiceTypeStatic; -use graphene_std::list::List; use graphene_std::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; /// All non-color stroke-related options surfaced in the control bar popover. @@ -215,7 +214,7 @@ pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, docu pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec, document: &DocumentMessageHandler, responses: &mut VecDeque) { drawing.dash_lengths = Some(lengths.clone()); - set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashLengthsInput::>::INDEX, TaggedValue::F64Array(lengths), responses); + set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashLengthsInput::INDEX, TaggedValue::F64Array(lengths), responses); } pub fn apply_dash_offset(drawing: &mut DrawingToolState, offset: f64, document: &DocumentMessageHandler, responses: &mut VecDeque) { diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 37cf819c0c..70f34be928 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -579,7 +579,10 @@ mod test_artboard { Ok(instrumented) => instrumented, Err(e) => panic!("Failed to evaluate graph: {e}"), }; - instrumented.grab_all_input::>(&editor.runtime).flatten().collect() + instrumented + .grab_all_input::>(&editor.runtime) + .map(graphene_std::list::Item::new_from_element) + .collect() } #[derive(Debug, PartialEq)] diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index cd3d6672f0..1f62130a70 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -2058,7 +2058,7 @@ mod test_gradient { let fill_node_id = get_fill_node_id_with_direct_fill_input(layer, &document.network_interface)?; let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?; - let stops = match fill_node.inputs.get(fill::FillInput::>::INDEX)?.as_value()? { + let stops = match fill_node.inputs.get(fill::FillInput::::INDEX)?.as_value()? { TaggedValue::Gradient(stops) => stops.clone(), _ => return None, }; @@ -2176,7 +2176,7 @@ mod test_gradient { editor .handle_message(NodeGraphMessage::CreateWire { output_connector: OutputConnector::node(gradient_node_id, 0), - input_connector: InputConnector::node(fill_node_id, fill::FillInput::>::INDEX), + input_connector: InputConnector::node(fill_node_id, fill::FillInput::::INDEX), }) .await; diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 33b9557b4b..4746e8b0c4 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -109,6 +109,22 @@ fn node_registry() -> HashMap> { // MEMO NODES // ========== ]; + // The transform's value-typed rows, served by `transform_value` under the + // leveled transform's identifier. + node_types.extend( + graphene_std::transform_nodes::transform_nodes::transform_value_entries() + .into_iter() + .map(|entry| (graphene_std::transform_nodes::transform_nodes::transform::IDENTIFIER.clone(), entry)), + ); + // The transitional level bridge: a leveled wire materializes into the legacy + // list an unconverted consumer expects. The rows are keyed under the legacy + // convert identifiers and die with the last legacy consumer. + node_types.extend( + graphene_std::graphic::level_to_list_entries() + .into_iter() + .zip(["List", "List", "List>", "List>", "List", "List", "List"]) + .map(|(entry, target)| (ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{target}>")), entry)), + ); // ============= // CONVERT NODES // ============= diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 070519b09e..041ca693c1 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -581,15 +581,49 @@ fn wrap_graphic_extent(_content: ListIn<'_, Graphic>, _level: LevelIn) -> GPoll< GPoll::Final(Extent::Exactly(1)) } -/// Converts the level's elements into `Graphic` elements. A `Graphic` level passes through unchanged. +/// Converts the level's elements into `Graphic` elements. A `Graphic` level passes through +/// unchanged. The legacy list rows accept an unconverted producer's list value as one element. #[node_macro::node(category("General"))] pub fn to_graphic + Clone + Send + Sync + core_types::CacheHash + 'static>( _: impl Ctx, - #[implementations(Graphic, Vector, Raster, Raster, Color, GradientStops, String)] content: T, + #[implementations( + Graphic, + Vector, + Raster, + Raster, + Color, + GradientStops, + String, + List, + List, + List>, + List>, + List, + List, + List, + )] + content: T, ) -> Graphic { content.into() } +/// The transitional level bridge: the wire's records as the legacy list an +/// unconverted consumer expects, attributes copied through their erased +/// reads. Registered under the legacy convert identifiers; the rows die with +/// the last legacy consumer. +#[node_macro::node(category(""))] +pub fn level_to_list( + _: impl Ctx + ExtractIndex + InjectIndex + Copy, + #[implementations(Graphic, Vector, Raster, Raster, Color, GradientStops, String)] value: IList, + _converter: (), +) -> List { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(value.batch()) }; + graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type") +} + +pub use _level_to_list_mod::level_to_list_entries; + /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. #[node_macro::node(category("General"), extent(flatten_graphic_extent))] pub fn flatten_graphic(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList, fully_flatten: bool) -> Result)>, Interrupt> { diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 40930110fd..4e5747de88 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -1,30 +1,48 @@ use core::f64; +use core_types::attribute::{Attr, Transform as TransformAttr}; use core_types::color::Color; -use core_types::gpoll::Interrupt; -use core_types::list::{List, ListDyn}; +use core_types::extent::{ExtentIn, LevelIn, ValueIn}; +use core_types::gpoll::{Extent, GPoll, Interrupt}; use core_types::transform::{ApplyTransform, ScaleType, Transform}; -use core_types::{ATTR_TRANSFORM, Context, Ctx, DeriveCtx, InjectFootprint, ModifyFootprint}; +use core_types::{CacheHash, Context, Ctx, DeriveCtx, ExtractIndex, InjectFootprint, InjectIndex, ModifyFootprint}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Graphic; use graphic_types::Vector; use graphic_types::raster_types::{CPU, GPU, Raster}; use vector_types::GradientStops; -/// Applies the specified transform to the input value, which may be a graphic type or another transform. -#[node_macro::node(category("Math: Transform"))] -fn transform( +/// Applies the specified transform to each lane of the input wire, composing onto the lane's transform attribute. +#[node_macro::node(category("Math: Transform"), extent(transform_extent))] +fn transform( ctx: impl Ctx + DeriveCtx + ModifyFootprint, - #[implementations( - Context -> DAffine2, - Context -> DVec2, - Context -> List, - Context -> List, - Context -> List, - Context -> List>, - Context -> List>, - Context -> List, - Context -> List, - )] + content: impl Node, Output = (T, Attr)>, + #[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2, + #[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64, + #[widget(ParsedWidgetOverride::Custom = "transform_scale")] + #[default(1., 1.)] + scale: DVec2, + #[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2, +) -> Result<(T, Attr), Interrupt> { + let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); + let skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]); + let matrix = trs * skew; + + let transformed = ctx.modify_footprint(|footprint| footprint.apply_transform(&matrix)); + let (element, transform) = content.eval(&transformed.ctx())?; + + Ok((element, Attr(matrix * *transform))) +} + +fn transform_extent(content: ExtentIn<'_>, _translation: ValueIn<'_, DVec2>, _rotation: ValueIn<'_, f64>, _scale: ValueIn<'_, DVec2>, _skew: ValueIn<'_, DVec2>, level: LevelIn) -> GPoll { + content.at(level) +} + +/// The transform applied to a plain transform or point value. Registered under the same identifier +/// as the leveled `transform`, serving its value-typed rows. +#[node_macro::node(category(""))] +fn transform_value( + ctx: impl Ctx + DeriveCtx + ModifyFootprint, + #[implementations(Context -> DAffine2, Context -> DVec2)] content: impl Node, Output = T>, #[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64, @@ -45,71 +63,55 @@ fn transform( Ok(transform_target) } +pub use _transform_value_mod::transform_value_entries; + /// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform. /// Shear is represented jointly by rotation and scale, so resetting both will also remove any shear. #[node_macro::node(category("Math: Transform"))] fn reset_transform( _: impl Ctx, - #[implementations( - List, - List, - List>, - List>, - List, - List, - )] - mut content: List, + (element, transform): (T, Attr), #[default(true)] reset_translation: bool, reset_rotation: bool, reset_scale: bool, -) -> List { - for row_transform in content.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { - if reset_translation { - row_transform.translation = DVec2::ZERO; - } - - match (reset_rotation, reset_scale) { - (true, true) => row_transform.matrix2 = DMat2::IDENTITY, - (true, false) => { - let scale = row_transform.scale_magnitudes(); - row_transform.matrix2 = DMat2::from_diagonal(scale); - } - (false, true) => { - let rotation = row_transform.decompose_rotation(); - row_transform.matrix2 = DMat2::from_angle(rotation); - } - (false, false) => {} - } +) -> (T, Attr) { + let mut row_transform = *transform; + if reset_translation { + row_transform.translation = DVec2::ZERO; } - content + + match (reset_rotation, reset_scale) { + (true, true) => row_transform.matrix2 = DMat2::IDENTITY, + (true, false) => { + let scale = row_transform.scale_magnitudes(); + row_transform.matrix2 = DMat2::from_diagonal(scale); + } + (false, true) => { + let rotation = row_transform.decompose_rotation(); + row_transform.matrix2 = DMat2::from_angle(rotation); + } + (false, false) => {} + } + (element, Attr(row_transform)) } -/// Overwrites the transform of each item in the input `List` with the specified transform. +/// Overwrites the transform of each lane of the input wire with the specified transform. #[node_macro::node(category("Math: Transform"))] -fn replace_transform( - _: impl Ctx + InjectFootprint, - #[implementations( - List, - List, - List>, - List>, - List, - List, - )] - mut content: List, - transform: DAffine2, -) -> List { - for row_transform in content.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { - *row_transform = transform.transform(); - } - content +fn replace_transform(_: impl Ctx + InjectFootprint, (element, _content_transform): (T, Attr), transform: DAffine2) -> (T, Attr) { + (element, Attr(transform)) } // TODO: Figure out how this node should behave once #2982 is implemented. -/// Obtains the transform of the first item in the input `List`, if present. +/// Obtains the transform of the first lane of the input wire, if present. #[node_macro::node(category("Math: Transform"), path(core_types::vector))] -fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 { - content.attribute::(ATTR_TRANSFORM, 0).copied().unwrap_or_default() +fn extract_transform( + _: impl Ctx + ExtractIndex + InjectIndex + Copy, + #[implementations(Graphic, Vector, Raster, Raster, Color, GradientStops)] content: IList, +) -> DAffine2 { + match content.len() { + 0 => DAffine2::default(), + _ => content.lane(0).attr::(), + } } /// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform. diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index 6a67c878bf..4c3d4fd997 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -8,7 +8,7 @@ use vector_types::vector::VectorModification; /// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments. #[node_macro::node(category(""))] -fn path_modify(_ctx: impl Ctx, mut vector: List, modification: Box, node_path: List) -> List { +fn path_modify(_ctx: impl Ctx, mut vector: List, modification: Box, node_path: Vec) -> List { use core_types::list::Item; if vector.is_empty() { @@ -23,7 +23,7 @@ fn path_modify(_ctx: impl Ctx, mut vector: List, modification: Box = { let len = node_path.len(); - node_path.into_iter().take(len.saturating_sub(1)).collect() + node_path.into_iter().take(len.saturating_sub(1)).map(Item::new_from_element).collect() }; let existing: List = vector.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); vector.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, if existing.is_empty() { subgraph_path } else { existing }); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index a6f9457ae3..37524f260c 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -8,7 +8,7 @@ use core_types::list::{Item, ItemAttributeValues, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; -use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx}; +use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; @@ -155,8 +155,8 @@ where /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -fn fill( - _: impl Ctx, +fn fill( + _: impl Ctx + ExtractIndex + InjectIndex + Copy, /// The content with vector paths to apply the fill style to. #[implementations( List, List, List, List, List, List, @@ -166,16 +166,20 @@ fn fill( /// The fill to paint the path with. #[default(Color::BLACK)] #[implementations( - List, List, List, List, List>, List>, - List, List, List, List, List>, List>, + Graphic, Vector, Color, GradientStops, Raster, Raster, + Graphic, Vector, Color, GradientStops, Raster, Raster, )] - mut fill: F, - _backup_color: List, - _backup_gradient: List, + fill: IList

, + _backup_color: IList, + _backup_gradient: IList, _gradient_type: GradientType, _spread_method: GradientSpreadMethod, _transform: Option, -) -> V { +) -> V +where + List

: IntoGraphicList, +{ + let mut fill: List

= legacy_list_of(fill); if let Some(gradient) = (&mut fill as &mut dyn std::any::Any).downcast_mut::>() { if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_TYPE) { @@ -250,41 +254,21 @@ impl IntoF64Vec for String { /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] -fn stroke( - _: impl Ctx, +fn stroke( + _: impl Ctx + ExtractIndex + InjectIndex + Copy, /// The content with vector paths to apply the stroke style to. #[implementations( - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List, List, List, + List, List, List, List, List, List, + List, List, List, List, List, List, )] mut content: List, /// The stroke paint. #[default(Color::BLACK)] #[implementations( - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List>, List>, List>, - List>, List>, List>, - List, List, List, - List, List, List, - List, List, List, - List, List, List, - List>, List>, List>, - List>, List>, List>, + Graphic, Vector, Color, GradientStops, Raster, Raster, + Graphic, Vector, Color, GradientStops, Raster, Raster, )] - paint: P, + paint: IList

, /// The stroke thickness. #[unit(" px")] #[default(2.)] @@ -302,29 +286,16 @@ fn stroke( /// The order to paint the stroke on top of the fill, or the fill on top of the stroke. paint_order: PaintOrder, /// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed. - #[implementations( - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - List, f64, String, - )] - dash_lengths: L, + dash_lengths: IList, /// The phase offset distance from the starting point of the dash pattern. #[unit(" px")] dash_offset: f64, ) -> List where List: VectorListIterMut + Send, + List

: IntoGraphicList, { - let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); + let dash_lengths = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect(); let stroke = Stroke { weight, @@ -344,7 +315,7 @@ where vector.stroke = Some(stroke); }); - let paint = paint.into_graphic_list(); + let paint = legacy_list_of(paint).into_graphic_list(); content.for_each_vector_list_mut(|vector_list| { // Broadcast the same paint to every item, scanning the attribute column once instead of per index for slot in vector_list.iter_attribute_values_mut_or_default::>(ATTR_STROKE) { @@ -354,6 +325,14 @@ where content } +/// The transitional value bridge: a materialized level as the legacy list the +/// unconverted body consumes. +fn legacy_list_of(level: core_types::node::List<'_, T>) -> List { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(level.batch()) }; + graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type") +} + #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))] fn copy_to_points( _: impl Ctx,