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

This commit is contained in:
Keavon Chambers
2026-07-20 16:37:43 -07:00
committed by Dennis Kobert
parent 194c3d0a72
commit 83cfd0225a
11 changed files with 178 additions and 243 deletions

View File

@@ -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<NodeId> = 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);

View File

@@ -488,7 +488,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
// 1: Count Elements (number of subpaths)
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector::count_elements::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(vector::list_length::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(0), 0)],
..Default::default()
},
@@ -1289,78 +1289,6 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
),
properties: None,
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
identifier: "Upload Texture",
category: "Debug",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::import(concrete!(List<Raster<CPU>>), 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<Raster<CPU>>), 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<DefinitionIdentifier, DocumentNodeDefi
// Node 1: extract_element at index 0, extracts the whole match as a bare String (drops the item's start/end/name attributes since the unwrapped String can't carry them)
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extract_element::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER),
..Default::default()
},
// Node 2: omit_element at index 0, returns the capture group items as a List<String>, 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()
},
]

View File

@@ -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(&divide_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

View File

@@ -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<Raster<CPU>, WgpuExecutorHandle> for Raster<GPU> {
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<T: Convert<List<Raster<GPU>>, WgpuExecutorHandle>>(
_: impl Ctx,
#[implementations(List<Raster<CPU>>, List<Raster<GPU>>)] input: T,
executor: WgpuExecutorHandle,
) -> List<Raster<GPU>> {
input.convert(Footprint::DEFAULT, executor)
}

View File

@@ -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<T: std::fmt::Debug>(_: 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<usize> {
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<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String)] input: T) -> Option<T> {
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<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<u32>, Option<u64>, Option<String>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Clones the element out of its record input.
#[node_macro::node(category("Debug"))]
fn clone<T: Clone>(_: impl Ctx, #[implementations(Raster<CPU>, f64)] value: &T) -> T {
value.clone()
}

View File

@@ -25,43 +25,10 @@ fn resolve_index(index: f64, total: u64) -> Option<u64> {
}
}
/// 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<T>(
ctx: impl Ctx + ModifyIndex + Copy,
/// The list of data.
list: impl Node<Context<'_>, 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<T, Interrupt> {
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<Extent> {
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<T>(
#[node_macro::node(category("General"), name("Remove at Index"), extent(omit_element_extent))]
pub fn remove_at_index<T>(
ctx: impl Ctx + ModifyIndex + Copy,
/// The list of data.
list: impl Node<Context<'_>, 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<T: Clone + Default + Send + Sync + CacheHash + 'static>(
#[node_macro::node(category("General"), name("Item at Index"))]
pub fn item_at_index<T: Clone + Default + Send + Sync + CacheHash + 'static>(
_: impl Ctx,
/// The `List` of data to extract from.
#[implementations(String, f64, NodeId, Color, Gradient, Vector, Raster<CPU>, Graphic, Artboard)]

View File

@@ -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<PlatformEditorApi>) -> 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<T: Convert<List<Raster<GPU>>, ::wgpu_executor::WgpuExecutorHandle>>(
_: impl Ctx,
#[implementations(List<Raster<CPU>>)] content: T,
#[scope(wgpu_executor::IDENTIFIER)] executor: ::wgpu_executor::WgpuExecutorHandle,
) -> List<Raster<GPU>> {
content.convert(Footprint::DEFAULT, executor)
}

View File

@@ -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<String>)]
strings: List<String>,
/// Whether to split every letterform into its own vector item. Otherwise, a single vector compound path is produced.
separate_glyphs: bool,
) -> List<Vector> {
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<String>)]
strings: List<String>,
) -> List<Vector> {
shape_text_list(&strings, true)
}

View File

@@ -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<String>` 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<String>, separate_glyphs: bool) -> List<Vector> {
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::<Option<f64>>(ATTR_MAX_WIDTH, defaults.max_width),
max_height: item.attribute_cloned_or::<Option<f64>>(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::<DAffine2>(ATTR_TRANSFORM);
let layer_path = item.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH).cloned();
let blend_mode = item.attribute::<BlendMode>(ATTR_BLEND_MODE).copied();
let opacity = item.attribute::<f64>(ATTR_OPACITY).copied();
let opacity_fill = item.attribute::<f64>(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::<DAffine2>(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<String>` into vector geometry, flattening the per-item results.
pub fn shape_text_list(strings: &List<String>, separate_glyphs: bool) -> List<Vector> {
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::<Option<f64>>(ATTR_MAX_WIDTH, index, defaults.max_width),
max_height: strings.attribute_cloned_or::<Option<f64>>(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::<DAffine2>(ATTR_TRANSFORM, index);
let layer_path = strings.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, index);
let blend_mode = strings.attribute::<BlendMode>(ATTR_BLEND_MODE, index).copied();
let opacity = strings.attribute::<f64>(ATTR_OPACITY, index).copied();
let opacity_fill = strings.attribute::<f64>(ATTR_OPACITY_FILL, index).copied();
for mut item in vectors.into_iter() {
if transform != DAffine2::IDENTITY {
let local = item.attribute_cloned_or_default::<DAffine2>(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);
}
}

View File

@@ -3668,7 +3668,7 @@ fn point_inside(_: impl Ctx, source: IList<Vector>, 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<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(Graphic, Artboard, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<T>) -> f64 {
fn list_length<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(Graphic, Artboard, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<T>) -> f64 {
content.len() as f64
}

View File

@@ -178,11 +178,11 @@ fn write_inputs(page: &mut std::fs::File, valid_input_types: &[Vec<core_types::T
let default_value = default_value.trim_end_matches('.').trim_end_matches(".0"); // Display whole-number floats as integers
let render_color = |color| format!(r#"<span style="padding-right: 100px; border: 2px solid var(--color-fog); background: {color}"></span>"#);
// 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()),
};