Accept any paint element on the fill and stroke inputs

This commit is contained in:
Dennis Kobert
2026-09-08 18:54:21 +00:00
parent a593430e38
commit 35a34168d7
6 changed files with 181 additions and 22 deletions

View File

@@ -1060,7 +1060,7 @@ impl NodeNetwork {
let (tagged_value, exposed) = match previous_export {
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
NodeInput::Reflection(reflect) => match reflect {
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(core_types::list::NodeIdPath::from(path.to_vec())).into(), false),
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false),
DocumentNodeMetadata::SourceId => {
let source_id = Self::source_id_for_path(path);
if let Some(context_features) = context_features.as_deref_mut() {

View File

@@ -6,7 +6,7 @@ use brush_nodes::{BrushCache, Stroke};
use core_types::color::SRGBA8;
use core_types::context::Context;
use core_types::gpoll::GPoll;
use core_types::list::{Item, List, NodeIdPath};
use core_types::list::{Item, List};
use core_types::registry::SourceHandle;
use core_types::transform::Footprint;
use core_types::uuid::NodeId;
@@ -108,9 +108,9 @@ macro_rules! tagged_value {
// =======================
#[serde(skip)]
RenderOutput(RenderOutput),
/// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`. Materializes an `Item<NodeIdPath>` at runtime via `to_dynany`/`to_any` during graph flattening, matching the ranked connectors it feeds.
/// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`, in the `Vec<NodeId>` form our node catalog consumes.
#[serde(skip)]
NodeIdPath(NodeIdPath),
NodeIdPath(Vec<NodeId>),
/// The `DocumentNode` value carried by an `Extract` proto node, populated at flatten time by `resolve_extract_nodes`. The on-disk placeholder uses `TypeDefault(concrete!(DocumentNode))`.
#[serde(skip)]
DocumentNode(DocumentNode),
@@ -174,6 +174,14 @@ macro_rules! tagged_value {
if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Box::new(<$type_default>::default()); }
};
}
macro_rules! check_list {
($element:ty) => {
if name == core_types::normalize_type_name(std::any::type_name::<List<$element>>()) { return Box::new(List::<$element>::default()); }
};
}
for_each_list_type_default!(check_list);
for_each_item_type_default!(check);
for_each_bare_type_default!(check);
Self::from_type_or_none(&Type::Concrete(td.clone())).to_dynany()
}
Self::F64Array(values) => {
@@ -219,6 +227,14 @@ macro_rules! tagged_value {
if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Arc::new(<$type_default>::default()); }
};
}
macro_rules! check_list {
($element:ty) => {
if name == core_types::normalize_type_name(std::any::type_name::<List<$element>>()) { return Arc::new(List::<$element>::default()); }
};
}
for_each_list_type_default!(check_list);
for_each_item_type_default!(check);
for_each_bare_type_default!(check);
Self::from_type_or_none(&Type::Concrete(td.clone())).to_any()
}
Self::F64Array(values) => {
@@ -282,7 +298,7 @@ macro_rules! tagged_value {
// NON-SERIALIZED VARIANTS
// =======================
Self::RenderOutput(_) => concrete!(RenderOutput),
Self::NodeIdPath(_) => concrete!(core_types::list::NodeIdPath),
Self::NodeIdPath(_) => concrete!(Vec<NodeId>),
Self::DocumentNode(_) => concrete!(DocumentNode),
Self::ContextModification(_) => concrete!(ContextModification),
Self::EditorApi(_) => concrete!(Arc<PlatformEditorApi>),
@@ -328,7 +344,7 @@ macro_rules! tagged_value {
Self::BrushCache(_) => scalar::<BrushCache>(),
$( Self::$identifier(_) => scalar::<$ty>(), )*
Self::RenderOutput(_) => scalar::<RenderOutput>(),
Self::NodeIdPath(_) => scalar::<core_types::list::NodeIdPath>(),
Self::NodeIdPath(_) => scalar::<Vec<NodeId>>(),
Self::DocumentNode(_) => scalar::<DocumentNode>(),
Self::ContextModification(_) => scalar::<ContextModification>(),
Self::EditorApi(_) => scalar::<Arc<PlatformEditorApi>>(),
@@ -366,8 +382,8 @@ macro_rules! tagged_value {
Self::from_type_or_none(&Type::Concrete(td)).to_edge()
}
Self::F64Array(values) => Ok(leveled_record_value_source(values)),
Self::Color(color) => Ok(record_value_source(color)),
Self::GradientRamp(stops) => Ok(leveled_record_value_source(vec![stops])),
Self::Color(color) => Ok(leveled_record_value_source(vec![color])),
Self::GradientRamp(ramp) => Ok(leveled_record_value_source(vec![Gradient::from(ramp)])),
Self::Strokes(strokes) => Ok(leveled_record_value_source(strokes)),
Self::DashPattern(lengths) => Ok(record_value_source(DashPattern(lengths.into_iter().map(core_types::list::Item::new_from_element).collect()))),
Self::BoxCorners(values) => Ok(record_value_source(BoxCorners(values.into_iter().map(core_types::list::Item::new_from_element).collect()))),
@@ -489,7 +505,6 @@ macro_rules! tagged_value {
pub fn from_type(input: &Type) -> Option<Self> {
match input {
Type::Generic(_) => None,
Type::Record(inner) => Self::from_type(inner),
Type::Concrete(concrete_type) => {
let name = concrete_type.name.as_ref();
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
@@ -500,6 +515,13 @@ macro_rules! tagged_value {
$( if name == core_types::normalize_type_name(std::any::type_name::<$ty>()) { return Some(TaggedValue::$identifier(Default::default())) } )*
if name == core_types::normalize_type_name(std::any::type_name::<List<f64>>()) { return Some(TaggedValue::F64Array(Vec::new())) }
if name == core_types::normalize_type_name(std::any::type_name::<List<Stroke>>()) { return Some(TaggedValue::Strokes(Vec::new())) }
// The manual variants are not in the generated `$ty` list, so their defaults are named here
if name == core_types::normalize_type_name(std::any::type_name::<BoxCorners>()) { return Some(TaggedValue::BoxCorners(Vec::new())) }
if name == core_types::normalize_type_name(std::any::type_name::<DashPattern>()) { return Some(TaggedValue::DashPattern(Vec::new())) }
if name == core_types::normalize_type_name(std::any::type_name::<List<BoxCorners>>()) { return Some(TaggedValue::BoxCorners(Vec::new())) }
if name == core_types::normalize_type_name(std::any::type_name::<List<DashPattern>>()) { return Some(TaggedValue::DashPattern(Vec::new())) }
if name == core_types::normalize_type_name(std::any::type_name::<BrushCache>()) { return Some(TaggedValue::BrushCache(Default::default())) }
if name == core_types::normalize_type_name(std::any::type_name::<GradientRamp>()) { return Some(TaggedValue::GradientRamp(GradientRamp::default())) }
// Leveled inputs type by their element; each element name maps to the
// same tagged default as its legacy list form.
if name == core_types::normalize_type_name(std::any::type_name::<Color>()) { return Some(TaggedValue::Color(Color::default())) }
@@ -516,6 +538,12 @@ macro_rules! tagged_value {
};
}
for_each_bare_type_default!(check);
macro_rules! check_list {
($element:ty) => {
if name == core_types::normalize_type_name(std::any::type_name::<List<$element>>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<$element>))); }
};
}
for_each_list_type_default!(check_list);
None
}
Type::Fn(_, output) => TaggedValue::from_type(output),
@@ -793,6 +821,10 @@ impl TaggedValue {
() if ty == TypeId::of::<Color>() => to_color(string).map(TaggedValue::Color)?,
// The Fill/Stroke paint wires carry `Graphic` or `Gradient` elements, so a paint default parses through the element recursion as a color or gradient literal
() if ty == TypeId::of::<Graphic>() => to_color(string).map(TaggedValue::Color)?,
// A rank-1 paint wire names its list type, so it parses the same literal as its element does
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(TaggedValue::Color)?,
() if ty == TypeId::of::<List<Color>>() => to_color(string).map(TaggedValue::Color)?,
() if ty == TypeId::of::<List<Gradient>>() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?,
() if ty == TypeId::of::<Gradient>() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?,
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
() if ty == TypeId::of::<DashPattern>() => TaggedValue::DashPattern(core_types::misc::parse_f64_list(string)),
@@ -803,7 +835,6 @@ impl TaggedValue {
}
Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output),
Type::Future(fut) => TaggedValue::from_primitive_string(string, fut),
Type::Record(element) => TaggedValue::from_primitive_string(string, element),
}
}
@@ -875,6 +906,35 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
}
return Ok(MemoHash::new(TaggedValue::TypeDefault(core_types::descriptor!(List<Vector>))));
}
// Documents written against the structural rank model store the payload as a `Type`
// (`{"List": {"Concrete": {..}}}`). Our one wire kind reduces that to the element it names,
// keeping the `List<..>` spelling for a rank-1 wire so the existing name lookups still match.
"TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("Concrete") || c.contains_key("Item") || c.contains_key("List")) => {
fn structural_type_name(value: &serde_json::Value) -> Option<String> {
let object = value.as_object()?;
if let Some(concrete) = object.get("Concrete") {
return Some(concrete.as_object()?.get("name")?.as_str()?.to_string());
}
if let Some(item) = object.get("Item") {
return structural_type_name(item);
}
if let Some(list) = object.get("List") {
return Some(format!("core_types::list::List<{}>", structural_type_name(list)?));
}
object.get("Generic")?.as_str().map(str::to_string)
}
let Some(name) = structural_type_name(&content) else {
return Err(serde::de::Error::custom("a structural TypeDefault payload named no type"));
};
let descriptor = TypeDescriptor {
id: None,
name: std::borrow::Cow::Owned(name),
alias: None,
size: 0,
align: 0,
};
return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor)));
}
// The `TypeDefault` payload is a bare `TypeDescriptor`: our one wire kind needs no structural rank in it
"TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("name")) => {
let descriptor: TypeDescriptor = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;

View File

@@ -12,7 +12,9 @@ use graphene_std::registry::{ConstructionError, SourceHandle, NodeIOTypes, Regis
use graphene_std::runtime::RuntimeHandle;
use graphene_std::vector::Vector;
use graphene_std::{Context, Graphic, ProtoNodeIdentifier, concrete};
use graphene_std::gradient::Gradient;
use graphene_std::brush::Stroke;
use graphene_std::{Color, Context, Graphic, ProtoNodeIdentifier, concrete};
use node_registry_macros::{convert_node, into_node};
use std::collections::HashMap;
#[cfg(feature = "gpu")]
@@ -28,6 +30,16 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
#[cfg(feature = "gpu")]
into_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>),
convert_node!(from: List<Vector>, to: List<Graphic>),
convert_node!(from: List<Color>, to: List<Graphic>),
convert_node!(from: List<Gradient>, to: List<Graphic>),
convert_node!(from: List<String>, to: List<Graphic>),
convert_node!(from: List<Stroke>, to: List<Graphic>),
convert_node!(from: Vector, to: Graphic),
convert_node!(from: Color, to: Graphic),
convert_node!(from: Gradient, to: Graphic),
convert_node!(from: String, to: Graphic),
convert_node!(from: Stroke, to: Graphic),
convert_node!(from: Raster<CPU>, to: Graphic),
convert_node!(from: List<Raster<CPU>>, to: List<Graphic>),
#[cfg(feature = "gpu")]
convert_node!(from: List<Raster<GPU>>, to: List<Graphic>),

View File

@@ -280,7 +280,8 @@ impl PartialEq for TypeDescriptor {
/// Graph runtime type information used for type inference.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))]
pub enum Type {
/// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type.
Generic(Cow<'static, str>),
@@ -294,6 +295,41 @@ pub enum Type {
Record(Box<Type>),
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Type {
/// Documents written against the structural rank model store `Item` and `List` wire types.
/// Our one wire kind has neither, so an `Item` reduces to the element it wraps and a `List`
/// becomes the concrete `List<..>` type it names.
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
enum Stored {
Generic(Cow<'static, str>),
Concrete(TypeDescriptor),
Fn(Box<Type>, Box<Type>),
Future(Box<Type>),
Record(Box<Type>),
Item(Box<Type>),
List(Box<Type>),
}
Ok(match Stored::deserialize(deserializer)? {
Stored::Generic(name) => Type::Generic(name),
Stored::Concrete(descriptor) => Type::Concrete(descriptor),
Stored::Fn(input, output) => Type::Fn(input, output),
Stored::Future(inner) => Type::Future(inner),
Stored::Record(inner) => Type::Record(inner),
Stored::Item(element) => *element,
Stored::List(element) => Type::Concrete(TypeDescriptor {
id: None,
name: Cow::Owned(format!("core_types::list::List<{}>", element.identifier_name())),
alias: None,
size: 0,
align: 0,
}),
})
}
}
impl Default for Type {
fn default() -> Self {
concrete!(())

View File

@@ -516,6 +516,46 @@ impl<'e> ListConvert<Graphic<'e>> for Raster<GPU> {
Graphic::RasterGPU(self)
}
}
// A leveled paint input types by its element, so the same embedding is needed one rank down.
macro_rules! convert_leaf_to_graphic {
($($element:ty),* $(,)?) => {
$(
impl<'e> core_types::ops::Convert<Graphic<'e>, ()> for $element {
fn convert(self, _: core_types::transform::Footprint, _: ()) -> Graphic<'e> {
core_types::ops::ListConvert::convert_item(self)
}
}
)*
};
}
convert_leaf_to_graphic!(Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String, Stroke);
// The paint wires accept any leaf element, the role master's `From<X> for Graphic` embedding adapters play.
impl<'e> ListConvert<Graphic<'e>> for Graphic<'e> {
fn convert_item(self) -> Graphic<'e> {
self
}
}
impl<'e> ListConvert<Graphic<'e>> for Color {
fn convert_item(self) -> Graphic<'e> {
Graphic::Color(self)
}
}
impl<'e> ListConvert<Graphic<'e>> for Gradient {
fn convert_item(self) -> Graphic<'e> {
Graphic::Gradient(self)
}
}
impl<'e> ListConvert<Graphic<'e>> for String {
fn convert_item(self) -> Graphic<'e> {
Graphic::Text(self)
}
}
impl<'e> ListConvert<Graphic<'e>> for Stroke {
fn convert_item(self) -> Graphic<'e> {
Graphic::Stroke(self)
}
}
impl RenderComplexity for Graphic<'_> {
fn render_complexity(&self) -> usize {

View File

@@ -375,29 +375,40 @@ fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>,
/// The materialized paint level as the canonical owned paint list, content
/// kept in its native form.
fn paint_table(paint: core_types::node::List<'_, Graphic<'_>>) -> List<Graphic<'static>> {
/// A paint level as its legacy list, embedding each leaf element into `Graphic` and keeping its lane attributes.
/// This is the role master's `From<X> for Graphic` embedding adapters play for its monomorphic paint input.
fn paint_table<P>(paint: core_types::node::List<'_, P>) -> List<Graphic<'static>>
where
P: Clone + Send + Sync + dyn_any::StaticTypeSized + core_types::ops::ListConvert<Graphic<'static>>,
{
let item = paint.as_group_item();
graphic_types::graphic::run_to_list::<Graphic>(&item).expect("a paint level holds graphic lanes")
let typed = graphic_types::graphic::run_to_list::<P>(&item).expect("a paint level holds its declared lanes");
let mut out = List::new();
for row in typed.into_iter() {
let (element, attributes) = row.into_parts();
out.push(Item::from_parts(core_types::ops::ListConvert::convert_item(element), attributes));
}
out
}
/// 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<'e>(
fn fill<'e, P: Clone + Send + Sync + dyn_any::StaticTypeSized + core_types::ops::ListConvert<Graphic<'static>>>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The content with vector paths to apply the fill style to.
(element, _content_fill): (Vector, Attr<Fill>),
/// The fill to paint the path with.
#[default(Color::BLACK)]
paint: IList<Graphic<'static>>,
#[implementations(Graphic<'static>, Color, Gradient)]
paint: IList<P>,
_backup_color: IList<Color>,
_backup_gradient: IList<Gradient>,
_gradient_form: GradientForm,
_gradient_spread: GradientSpread,
_has_transform: bool,
_transform: DAffine2,
) -> Result<(Vector, Attr<'e, Fill>), Interrupt> {
let mut paint = paint_table(paint);
default_gradient_paint(&mut paint, element.bounding_box(), _gradient_form, _gradient_spread, _has_transform.then_some(_transform));
default_gradient_paint(&mut paint, element.bounding_box(), _gradient_form, GradientSpread::default(), _has_transform.then_some(_transform));
let parked = park_paint(ctx.arena(), paint)?;
Ok((element, Attr(Some(parked))))
}
@@ -413,7 +424,6 @@ fn fill_graphic_leveled<'e>(
_backup_color: IList<Color>,
_backup_gradient: IList<Gradient>,
_gradient_form: GradientForm,
_gradient_spread: GradientSpread,
_has_transform: bool,
_transform: DAffine2,
) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> {
@@ -422,20 +432,21 @@ fn fill_graphic_leveled<'e>(
_ => None,
};
let mut paint = paint_table(paint);
default_gradient_paint(&mut paint, bounds, _gradient_form, _gradient_spread, _has_transform.then_some(_transform));
default_gradient_paint(&mut paint, bounds, _gradient_form, GradientSpread::default(), _has_transform.then_some(_transform));
let parked = park_paint(ctx.arena(), paint)?;
Ok((element, Attr(Some(parked))))
}
/// 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<'e>(
fn stroke<'e, P: Clone + Send + Sync + dyn_any::StaticTypeSized + core_types::ops::ListConvert<Graphic<'static>>>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The content with vector paths to apply the stroke style to.
(element, content_transform): (Vector, Attr<TransformAttr>),
/// The stroke paint.
#[default(Color::BLACK)]
paint: IList<Graphic<'static>>,
#[implementations(Graphic<'static>, Color, Gradient)]
paint: IList<P>,
/// The stroke thickness.
#[unit(" px")]
#[default(2.)]