From 83cfd0225a1835b0c52c3252ba6b6fb96d7b0741 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 20 Jul 2026 16:37:43 -0700 Subject: [PATCH] Reshape the node catalog: rename the list access nodes, split Text to Vector, reinstate Upload Texture as a proto node, and delete the vestigial debug nodes --- .../document/document_message_handler.rs | 6 +- .../node_graph/document_node_definitions.rs | 78 +----------- .../messages/portfolio/document_migration.rs | 103 +++++++++++----- .../wgpu-executor/src/texture_conversion.rs | 13 -- node-graph/nodes/gcore/src/debug.rs | 25 ---- node-graph/nodes/graphic/src/graphic.rs | 43 +------ .../nodes/gstd/src/platform_application_io.rs | 15 ++- node-graph/nodes/gstd/src/text.rs | 17 ++- node-graph/nodes/text/src/to_path.rs | 113 ++++++++++-------- node-graph/nodes/vector/src/vector_nodes.rs | 2 +- tools/node-docs/src/page_node.rs | 6 +- 11 files changed, 178 insertions(+), 243 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index bb2ccd3604..77827e2c1b 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2758,7 +2758,7 @@ impl DocumentMessageHandler { } let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist"); - let index_elements_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::index_elements::IDENTIFIER).expect("Index Elements node should exist"); + let item_at_index_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::item_at_index::IDENTIFIER).expect("Item at Index node should exist"); let mut resulting_layers: Vec = Vec::new(); @@ -2791,7 +2791,7 @@ impl DocumentMessageHandler { if has_fill && has_stroke { let (existing_index, new_index) = (0_f64, 1_f64); - let existing_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]); + let existing_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]); let existing_index_id = NodeId::new(); self.network_interface.insert_node(existing_index_id, existing_index_template, &[]); self.network_interface.move_node_to_chain_start(&existing_index_id, layer, &[], false); @@ -2813,7 +2813,7 @@ impl DocumentMessageHandler { self.network_interface.set_display_name(&new_layer_id, original_name, &[]); } - let new_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]); + let new_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]); let new_index_id = NodeId::new(); self.network_interface.insert_node(new_index_id, new_index_template, &[]); self.network_interface.move_node_to_chain_start(&new_index_id, new_layer, &[], false); 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 9eb80d36a4..91717b1e37 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 @@ -488,7 +488,7 @@ fn document_node_definitions() -> HashMap HashMap>), 0), NodeInput::scope(platform_application_io::wgpu_executor::IDENTIFIER)], - call_argument: generic!(T), - implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_conversion::upload_texture::IDENTIFIER), - ..Default::default() - }, - DocumentNode { - call_argument: generic!(T), - inputs: vec![NodeInput::node(NodeId(0), 0)], - implementation: DocumentNodeImplementation::ProtoNode(memo::memoize::IDENTIFIER), - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), - ..Default::default() - }), - inputs: vec![NodeInput::type_default(descriptor!(List>), true)], - ..Default::default() - }, - persistent_node_metadata: DocumentNodePersistentMetadata { - output_names: vec!["Texture".to_string()], - network_metadata: Some(NodeNetworkMetadata { - persistent_metadata: NodeNetworkPersistentMetadata { - node_metadata: [ - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, 0)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)), - ..Default::default() - }, - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), - ..Default::default() - }, - ..Default::default() - }), - ..Default::default() - }, - }, - description: Cow::Borrowed("TODO"), - properties: None, - }, DocumentNodeDefinition { identifier: "Extract", category: "", @@ -1407,13 +1335,13 @@ fn document_node_definitions() -> HashMap, preserving each item's start/end/name attributes DocumentNode { inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)], - implementation: DocumentNodeImplementation::ProtoNode(graphic::omit_element::IDENTIFIER), + implementation: DocumentNodeImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER), ..Default::default() }, ] diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index aa44760650..c869409b52 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -93,10 +93,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::animation::animation_time::IDENTIFIER, aliases: &["graphene_core::animation::AnimationTimeNode"], }, - NodeReplacement { - node: graphene_std::debug::clone::IDENTIFIER, - aliases: &["graphene_core::ops::CloneNode"], - }, NodeReplacement { node: graphene_std::extract_xy::extract_xy::IDENTIFIER, aliases: &["graphene_core::ops::ExtractXyNode"], @@ -112,6 +108,14 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ "graphene_core::transform_nodes::FreezeRealTimeNode", "graphene_core::vector::SubpathSegmentLengthsNode", "core_types::vector::SubpathSegmentLengthsNode", + // The deleted debug Option trio degrades to a passthrough of its single input (audit resolution 8) + "graphene_core::ops::SizeOfNode", + "graphene_core::debug::SizeOfNode", + "graphene_core::ops::SomeNode", + "graphene_core::debug::SomeNode", + "graphene_core::ops::UnwrapNode", + "graphene_core::debug::UnwrapNode", + "graphene_core::debug::UnwrapOptionNode", ], }, NodeReplacement { @@ -126,18 +130,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::animation::real_time::IDENTIFIER, aliases: &["graphene_core::animation::RealTimeNode"], }, - NodeReplacement { - node: graphene_std::debug::size_of::IDENTIFIER, - aliases: &["graphene_core::ops::SizeOfNode"], - }, - NodeReplacement { - node: graphene_std::debug::some::IDENTIFIER, - aliases: &["graphene_core::ops::SomeNode"], - }, - NodeReplacement { - node: graphene_std::debug::unwrap_option::IDENTIFIER, - aliases: &["graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"], - }, // ================================ // graphic // ================================ @@ -167,13 +159,19 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ aliases: &["graphene_core::graphic::FlattenVectorNode", "graphene_core::graphic_element::FlattenVectorNode"], }, NodeReplacement { - node: graphene_std::graphic::index_elements::IDENTIFIER, + node: graphene_std::graphic::item_at_index::IDENTIFIER, aliases: &[ "graphene_core::graphic_element::IndexNode", "graphene_core::graphic::IndexNode", "graphene_core::graphic::IndexElementsNode", + "graphic_nodes::graphic::IndexElementsNode", + "graphic_nodes::graphic::ExtractElementNode", ], }, + NodeReplacement { + node: graphene_std::graphic::remove_at_index::IDENTIFIER, + aliases: &["graphic_nodes::graphic::OmitElementNode"], + }, NodeReplacement { node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER, aliases: &[ @@ -777,7 +775,7 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ aliases: &["graphene_core::vector::ClosePathNode"], }, NodeReplacement { - node: graphene_std::vector::count_elements::IDENTIFIER, + node: graphene_std::vector::list_length::IDENTIFIER, aliases: &["graphene_core::vector::CountElementsNode"], }, NodeReplacement { @@ -1876,6 +1874,25 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path); } + // The Text to Vector node's runtime `separate_glyphs` toggle became the dedicated "Text to Vector Glyphs" node, leaving Text to Vector as a plain + // string-to-compound-path converter. A 2-input Text to Vector is the old toggled shape: a `true` toggle routes to Text to Vector Glyphs, otherwise + // the node stays Text to Vector; either way the toggle input is dropped and the string wire is preserved. + if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text_to_vector::IDENTIFIER) && inputs_count == 2 { + let separate_glyphs = matches!(node.inputs.get(1).and_then(|input| input.as_value()), Some(TaggedValue::Bool(true))); + let target = if separate_glyphs { + graphene_std::text::text_to_vector_glyphs::IDENTIFIER + } else { + graphene_std::text::text_to_vector::IDENTIFIER + }; + + let mut node_template = resolve_proto_node_type(target)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut node_template); + document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + if let Some(string_input) = node.inputs.first() { + document.network_interface.set_input(&InputConnector::node(*node_id, 0), string_input.clone(), network_path); + } + } + // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 8 { let mut template: NodeTemplate = legacy_text_node_template()?; @@ -2406,14 +2423,14 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], return None; }; - // Create Count Elements node: counts content `List` items → N - let Some(count_elements_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::count_elements::IDENTIFIER)) else { - log::error!("Could not get count_elements node from definition when upgrading morph"); + // Create List Length node: counts content `List` items → N + let Some(list_length_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::list_length::IDENTIFIER)) else { + log::error!("Could not get list_length node from definition when upgrading morph"); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); return None; }; - let count_elements_template = count_elements_def.default_node_template(); - let count_elements_id = NodeId::new(); + let list_length_template = list_length_def.default_node_template(); + let list_length_id = NodeId::new(); // Create Subtract node: N → N-1 let Some(subtract_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::subtract::IDENTIFIER)) else { @@ -2435,10 +2452,10 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], let divide_id = NodeId::new(); // Insert and position nodes - document.network_interface.insert_node(count_elements_id, count_elements_template, network_path); + document.network_interface.insert_node(list_length_id, list_length_template, network_path); document .network_interface - .shift_absolute_node_position(&count_elements_id, morph_position + IVec2::new(-21, 2), network_path); + .shift_absolute_node_position(&list_length_id, morph_position + IVec2::new(-21, 2), network_path); document.network_interface.insert_node(subtract_id, subtract_template, network_path); document.network_interface.shift_absolute_node_position(&subtract_id, morph_position + IVec2::new(-14, 2), network_path); @@ -2447,12 +2464,12 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.shift_absolute_node_position(÷_id, morph_position + IVec2::new(-7, 1), network_path); // Wire: content source → Count Elements input 0 - document.network_interface.set_input(&InputConnector::node(count_elements_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(list_length_id, 0), old_inputs[0].clone(), network_path); // Wire: Count Elements output → Subtract input 0 (minuend) document .network_interface - .set_input(&InputConnector::node(subtract_id, 0), NodeInput::node(count_elements_id, 0), network_path); + .set_input(&InputConnector::node(subtract_id, 0), NodeInput::node(list_length_id, 0), network_path); // Wire: old progression → Divide input 0 (numerator) document.network_interface.set_input(&InputConnector::node(divide_id, 0), old_inputs[1].clone(), network_path); @@ -2745,6 +2762,31 @@ fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, ne } } + // The removed Attach Attribute node (merged into Write Attribute per audit resolution 6) degrades to a passthrough of its + // content: its eager whole-list source input cannot be mechanically rewired as Write Attribute's lazy per-item value producer. + if let Some(DefinitionIdentifier::ProtoNode(identifier)) = document.network_interface.reference(node_id, network_path) + && identifier.as_str().ends_with("::AttachAttributeNode") + { + let mut node_template = resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut node_template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + if let Some(content) = old_inputs.first() { + document.network_interface.set_input(&InputConnector::node(*node_id, 0), content.clone(), network_path); + } + } + + // The Upload Texture node's old wrapper-network form maps onto its proto node form, which draws the executor from scope + if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path) + && name == "Upload Texture" + { + let mut node_template = resolve_proto_node_type(graphene_std::platform_application_io::upload_texture::IDENTIFIER)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut node_template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + if let Some(content) = old_inputs.first() { + document.network_interface.set_input(&InputConnector::node(*node_id, 0), content.clone(), network_path); + } + } + Some(()) } @@ -2752,6 +2794,13 @@ fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, ne mod tests { use super::*; + // The removed-definition blocks above abort silently via `?` if their swap target ever leaves the catalog + #[test] + fn removed_definition_swap_targets_resolve() { + assert!(resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).is_some()); + assert!(resolve_proto_node_type(graphene_std::platform_application_io::upload_texture::IDENTIFIER).is_some()); + } + #[test] fn a_written_attribute_no_longer_resets_the_layer_definitions() { // The node resolves natively again, so a document carrying it keeps diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index c4e8eefca0..a7481914f5 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -1,6 +1,5 @@ use crate::WgpuExecutorHandle; use core_types::Color; -use core_types::Ctx; use core_types::color::SRGBA8; use core_types::list::{Item, List}; use core_types::ops::{Convert, ConvertAsync}; @@ -250,15 +249,3 @@ impl ConvertAsync, WgpuExecutorHandle> for Raster { Box::pin(async move { converter.convert(&device).await.expect("Failed to download texture data") }) } } - -/// Uploads an raster texture from the CPU to the GPU. This is now deprecated and the Convert node should be used in the future. -/// -/// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue. -#[node_macro::node(category(""))] -pub fn upload_texture>, WgpuExecutorHandle>>( - _: impl Ctx, - #[implementations(List>, List>)] input: T, - executor: WgpuExecutorHandle, -) -> List> { - input.convert(Footprint::DEFAULT, executor) -} diff --git a/node-graph/nodes/gcore/src/debug.rs b/node-graph/nodes/gcore/src/debug.rs index 88b1802caf..4558ccf281 100644 --- a/node-graph/nodes/gcore/src/debug.rs +++ b/node-graph/nodes/gcore/src/debug.rs @@ -1,6 +1,5 @@ use core_types::Ctx; use glam::{DAffine2, DVec2}; -use raster_types::{CPU, Raster}; /// Meant for debugging purposes, not general use. Logs the input value to the console and passes it through unchanged. #[node_macro::node(category("Debug"), name("Log to Console"))] @@ -9,27 +8,3 @@ fn log_to_console(_: impl Ctx, #[implementations(bool, f64, log::debug!("{value:#?}"); value } - -/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes. -#[node_macro::node(category("Debug"))] -fn size_of(_: impl Ctx, ty: core_types::Type) -> Option { - ty.size() -} - -/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option. -#[node_macro::node(category("Debug"))] -fn some(_: impl Ctx, #[implementations(f64, f32, u32, u64, String)] input: T) -> Option { - Some(input) -} - -/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None. -#[node_macro::node(category("Debug"))] -fn unwrap_option(_: impl Ctx, #[implementations(Option, Option, Option, Option)] input: Option) -> T { - input.unwrap_or_default() -} - -/// Clones the element out of its record input. -#[node_macro::node(category("Debug"))] -fn clone(_: impl Ctx, #[implementations(Raster, f64)] value: &T) -> T { - value.clone() -} diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 2f9a416797..e14c38fdb7 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -25,43 +25,10 @@ fn resolve_index(index: f64, total: u64) -> Option { } } -/// Returns a one-lane level holding the item at the specified index with its -/// attributes, or an empty level when the index is out of range. -#[node_macro::node(category("General"), extent(index_elements_extent))] -pub fn index_elements( - ctx: impl Ctx + ModifyIndex + Copy, - /// The list of data. - list: impl Node, Output = T>, - /// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item. - index: SignedInteger, -) -> Result { - let total = match list.extent(ctx, Level::Total) { - GPoll::Final(Extent::Exactly(count)) => count as u64, - GPoll::Pending => return Err(Interrupt::Pending), - _ => return Err(GraphError::new("index elements over a non-exact extent").into()), - }; - let Some(source) = resolve_index(index, total) else { - return Err(GraphError::new("index elements addressed its empty selection").into()); - }; - let mut shifted = *ctx; - shifted.set_index(source); - list.eval(&shifted) -} - -fn index_elements_extent(list: ExtentIn<'_>, index: ValueIn<'_, f64>, level: LevelIn) -> GPoll { - match level.top() { - true => index.get().zip(list.at(level)).map(|(index, extent)| match extent { - Extent::Exactly(count) => Extent::Exactly(resolve_index(index, count as u64).is_some() as usize), - _ => Extent::Exactly(1), - }), - false => list.at(level), - } -} - -/// Returns the list with the element at the specified index removed. +/// Returns the list with the item at the specified index removed. /// If no value exists at that index, the list is returned unchanged. -#[node_macro::node(category("General"), extent(omit_element_extent))] -pub fn omit_element( +#[node_macro::node(category("General"), name("Remove at Index"), extent(omit_element_extent))] +pub fn remove_at_index( ctx: impl Ctx + ModifyIndex + Copy, /// The list of data. list: impl Node, Output = T>, @@ -96,8 +63,8 @@ fn omit_element_extent(list: ExtentIn<'_>, index: ValueIn<'_, f64>, level: Level /// Returns the bare element (without the item's attributes) at the specified index in a `List`. /// Use this when downstream nodes want just the inner value rather than a `List` containing a single item. /// If no value exists at that index, the element type's default is returned. -#[node_macro::node(category("General"))] -pub fn extract_element( +#[node_macro::node(category("General"), name("Item at Index"))] +pub fn item_at_index( _: impl Ctx, /// The `List` of data to extract from. #[implementations(String, f64, NodeId, Color, Gradient, Vector, Raster, Graphic, Artboard)] diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index f14c1c16de..ac72a1dabd 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -6,15 +6,14 @@ use canvas_utils::{Canvas, CanvasHandle}; use core_types::attribute::{Attr, OwnedAttr, Transform}; use core_types::color::SRGBA8; use core_types::gpoll::GPoll; -#[cfg(target_family = "wasm")] use core_types::list::List; #[cfg(target_family = "wasm")] use core_types::ATTR_TRANSFORM; #[cfg(target_family = "wasm")] use core_types::math::bbox::Bbox; +use core_types::ops::Convert; use core_types::runtime::SourceFuture; -#[cfg(target_family = "wasm")] use core_types::transform::Footprint; use core_types::{Color, Ctx}; pub use graph_craft::application_io::resource::{Resource, ResourceHash}; @@ -31,7 +30,7 @@ use graphic_types::Vector; #[cfg(target_family = "wasm")] use graphic_types::markers::EditorMergedLayers; use graphic_types::raster_types::Image; -use graphic_types::raster_types::{CPU, Raster}; +use graphic_types::raster_types::{CPU, GPU, Raster}; #[cfg(target_family = "wasm")] use graphic_types::vector_types::gradient::Gradient; #[cfg(target_family = "wasm")] @@ -308,3 +307,13 @@ pub fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: A pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc) -> Option<::wgpu_executor::WgpuExecutorHandle> { editor_api.application_io.as_ref()?.gpu_executor_arc().map(::wgpu_executor::WgpuExecutorHandle) } + +/// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it. +#[node_macro::node(category("Debug"), memoize)] +pub fn upload_texture>, ::wgpu_executor::WgpuExecutorHandle>>( + _: impl Ctx, + #[implementations(List>)] content: T, + #[scope(wgpu_executor::IDENTIFIER)] executor: ::wgpu_executor::WgpuExecutorHandle, +) -> List> { + content.convert(Footprint::DEFAULT, executor) +} diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 7d44390289..24fb5bf6cf 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -90,15 +90,24 @@ fn text( list } -/// Converts a styled `String[]` into vector geometry. +/// Converts styled text into vector compound paths. #[node_macro::node(category("Text"), name("Text to Vector"))] fn text_to_vector( _: impl Ctx, /// A styled list of text strings produced by the **Text** node (or any other `String[]` source). #[implementations(List)] strings: List, - /// Whether to split every letterform into its own vector item. Otherwise, a single vector compound path is produced. - separate_glyphs: bool, ) -> List { - shape_text_list(&strings, separate_glyphs) + shape_text_list(&strings, false) +} + +/// Splits styled text into a separate vector item for each of its glyphs (letterforms). +#[node_macro::node(category("Text"), name("Text to Vector Glyphs"))] +fn text_to_vector_glyphs( + _: impl Ctx, + /// A styled list of text strings produced by the **Text** node (or any other `String[]` source). + #[implementations(List)] + strings: List, +) -> List { + shape_text_list(&strings, true) } diff --git a/node-graph/nodes/text/src/to_path.rs b/node-graph/nodes/text/src/to_path.rs index 5dd27a6608..9098dd0297 100644 --- a/node-graph/nodes/text/src/to_path.rs +++ b/node-graph/nodes/text/src/to_path.rs @@ -2,7 +2,7 @@ use super::TypesettingConfig; use super::text_context::TextContext; use crate::markers::{ATTR_FONT, ATTR_TEXT_ALIGN}; use core_types::blending::BlendMode; -use core_types::list::List; +use core_types::list::{Item, List}; use core_types::uuid::NodeId; use core_types::{ ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, @@ -23,60 +23,71 @@ pub fn lines_clipping(text: &str, font: &Resource, typesetting: TypesettingConfi TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, typesetting)) } -/// Shapes each string item of a styled `List` into vector geometry, reading its font and typesetting -/// from the item's attributes (as set by the 'Text' node) and re-applying its transform and blending -/// attributes onto the produced paths. With `separate_glyphs`, each glyph becomes its own item. +/// Shapes a single styled string item into vector geometry, reading its font and typesetting from the item's +/// attributes (as set by the 'Text' node) and re-applying its transform and blending attributes onto the produced +/// paths. With `separate_glyphs`, each glyph becomes its own item; otherwise a single compound path is produced. +pub fn shape_text_item(item: &Item, separate_glyphs: bool) -> List { + let text = item.element(); + if text.is_empty() { + return List::new(); + } + + // Use fallback font when none is explicitly attached. + let font: Resource = { + let font: Resource = item.attribute_cloned_or_default(ATTR_FONT); + if font.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { font } + }; + + let defaults = TypesettingConfig::default(); + let typesetting = TypesettingConfig { + font_size: item.attribute_cloned_or(ATTR_FONT_SIZE, defaults.font_size), + line_height_ratio: item.attribute_cloned_or(ATTR_LINE_HEIGHT, defaults.line_height_ratio), + letter_spacing: item.attribute_cloned_or(ATTR_LETTER_SPACING, defaults.letter_spacing), + letter_tilt: item.attribute_cloned_or(ATTR_LETTER_TILT, defaults.letter_tilt), + max_width: item.attribute_cloned_or::>(ATTR_MAX_WIDTH, defaults.max_width), + max_height: item.attribute_cloned_or::>(ATTR_MAX_HEIGHT, defaults.max_height), + align: item.attribute_cloned_or(ATTR_TEXT_ALIGN, defaults.align), + }; + + let vectors = to_path(text, &font, typesetting, separate_glyphs); + let transform = item.attribute_cloned_or_default::(ATTR_TRANSFORM); + let layer_path = item.attribute::>(ATTR_EDITOR_LAYER_PATH).cloned(); + let blend_mode = item.attribute::(ATTR_BLEND_MODE).copied(); + let opacity = item.attribute::(ATTR_OPACITY).copied(); + let opacity_fill = item.attribute::(ATTR_OPACITY_FILL).copied(); + + let mut result = List::new(); + for mut produced in vectors.into_iter() { + if transform != DAffine2::IDENTITY { + let local = produced.attribute_cloned_or_default::(ATTR_TRANSFORM); + produced.set_attribute(ATTR_TRANSFORM, transform * local); + } + if let Some(layer_path) = &layer_path { + produced.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + } + if let Some(blend_mode) = blend_mode { + produced.set_attribute(ATTR_BLEND_MODE, blend_mode); + } + if let Some(opacity) = opacity { + produced.set_attribute(ATTR_OPACITY, opacity); + } + if let Some(opacity_fill) = opacity_fill { + produced.set_attribute(ATTR_OPACITY_FILL, opacity_fill); + } + result.push(produced); + } + + result +} + +/// Shapes each string item of a styled `List` into vector geometry, flattening the per-item results. pub fn shape_text_list(strings: &List, separate_glyphs: bool) -> List { let mut result = List::new(); for index in 0..strings.len() { - let Some(text) = strings.element(index) else { continue }; - if text.is_empty() { - continue; - } - - // Use fallback font when none is explicitly attached. - let font: Resource = { - let f: Resource = strings.attribute_cloned_or_default(ATTR_FONT, index); - if f.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { f } - }; - - let defaults = TypesettingConfig::default(); - let typesetting = TypesettingConfig { - font_size: strings.attribute_cloned_or(ATTR_FONT_SIZE, index, defaults.font_size), - line_height_ratio: strings.attribute_cloned_or(ATTR_LINE_HEIGHT, index, defaults.line_height_ratio), - letter_spacing: strings.attribute_cloned_or(ATTR_LETTER_SPACING, index, defaults.letter_spacing), - letter_tilt: strings.attribute_cloned_or(ATTR_LETTER_TILT, index, defaults.letter_tilt), - max_width: strings.attribute_cloned_or::>(ATTR_MAX_WIDTH, index, defaults.max_width), - max_height: strings.attribute_cloned_or::>(ATTR_MAX_HEIGHT, index, defaults.max_height), - align: strings.attribute_cloned_or(ATTR_TEXT_ALIGN, index, defaults.align), - }; - - let vectors = to_path(text, &font, typesetting, separate_glyphs); - let transform = strings.attribute_cloned_or_default::(ATTR_TRANSFORM, index); - let layer_path = strings.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, index); - let blend_mode = strings.attribute::(ATTR_BLEND_MODE, index).copied(); - let opacity = strings.attribute::(ATTR_OPACITY, index).copied(); - let opacity_fill = strings.attribute::(ATTR_OPACITY_FILL, index).copied(); - - for mut item in vectors.into_iter() { - if transform != DAffine2::IDENTITY { - let local = item.attribute_cloned_or_default::(ATTR_TRANSFORM); - item.set_attribute(ATTR_TRANSFORM, transform * local); - } - if !layer_path.is_empty() { - item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); - } - if let Some(blend_mode) = blend_mode { - item.set_attribute(ATTR_BLEND_MODE, blend_mode); - } - if let Some(opacity) = opacity { - item.set_attribute(ATTR_OPACITY, opacity); - } - if let Some(opacity_fill) = opacity_fill { - item.set_attribute(ATTR_OPACITY_FILL, opacity_fill); - } - result.push(item); + let Some(item) = strings.clone_item(index) else { continue }; + for produced in shape_text_item(&item, separate_glyphs).into_iter() { + result.push(produced); } } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 99505b89b6..9f5014ff96 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -3668,7 +3668,7 @@ fn point_inside(_: impl Ctx, source: IList, point: DVec2) -> bool { // TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs. // TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.) #[node_macro::node(category("General"), path(graphene_core::vector))] -fn count_elements(_: impl Ctx, #[implementations(Graphic, Artboard, Vector, Raster, Color, Gradient, String)] content: IList) -> f64 { +fn list_length(_: impl Ctx, #[implementations(Graphic, Artboard, Vector, Raster, Color, Gradient, String)] content: IList) -> f64 { content.len() as f64 } diff --git a/tools/node-docs/src/page_node.rs b/tools/node-docs/src/page_node.rs index 673928c6e5..d14028f81f 100644 --- a/tools/node-docs/src/page_node.rs +++ b/tools/node-docs/src/page_node.rs @@ -178,11 +178,11 @@ fn write_inputs(page: &mut std::fs::File, valid_input_types: &[Vec"#); + // Compare against the typed default's debug form so the swatch tracks the `Gradient` representation + let black_to_white_gradient = value::TaggedValue::Gradient(Default::default()).to_debug_string(); let default_value = match default_value { "Color::BLACK" => render_color("black"), - "Gradient([(0.0, Color { red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0 }), (1.0, Color { red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0 })])" => { - render_color("linear-gradient(to right, black, white)") - } + gradient if gradient == black_to_white_gradient => render_color("linear-gradient(to right, black, white)"), _ => format!("`{default_value}{}`", field.unit.unwrap_or_default()), };