Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo-artwork/isometric-fountain.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/valley-of-spires.graphite

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -904,9 +904,7 @@ macro_rules! known_table_row_types {
};
}

/// Override hook for [`Table::attribute_display_value`] that prefers `Display` over `Debug` for select
/// attribute types. The underlying storage is generic and can only see a `Debug` bound, so types whose
/// nicer `Display` rendering matters in the data panel are listed here explicitly.
/// Uses `Display` instead of `Debug` for attribute types that have a nicer human-readable format.
fn display_value_override(any: &dyn Any) -> Option<String> {
if let Some(value) = any.downcast_ref::<BlendMode>() {
return Some(value.to_string());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ impl<'a> ModifyInputsContext<'a> {
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(blend_node_id, 1);
let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
}

Expand All @@ -449,26 +449,45 @@ impl<'a> ModifyInputsContext<'a> {
return;
};
// Enable the `has_opacity` checkbox so the value is applied
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), false);
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 2), NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
NodeInput::value(TaggedValue::Bool(true), false),
false,
);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::OpacityInput::INDEX),
NodeInput::value(TaggedValue::F64(opacity * 100.), false),
false,
);
}

pub fn opacity_fill_set(&mut self, fill: f64) {
// Reuse the Opacity node if already present (saving a chain walk on slider drags), otherwise let the next call create it
// Reuse an existing Opacity node to avoid a redundant chain walk on slider drags
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
let existing = self.existing_proto_node_id(identifier.clone(), false);
let existed = existing.is_some();
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
return;
};
// Disable the opacity component on a freshly-created node so the slider only affects fill, mirroring the opacity-slider case
// (where the node's default `has_fill = false` already keeps fill out of the picture)
// Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
if !existed {
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 1), NodeInput::value(TaggedValue::Bool(false), false), false);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
NodeInput::value(TaggedValue::Bool(false), false),
false,
);
}
// Enable the `has_fill` checkbox so the value is applied
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), false);
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 4), NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasFillInput::INDEX),
NodeInput::value(TaggedValue::Bool(true), false),
false,
);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::FillInput::INDEX),
NodeInput::value(TaggedValue::F64(fill * 100.), false),
false,
);
}

/// Set the stops table on the 'Gradient Value' node, creating it if necessary.
Expand Down Expand Up @@ -570,7 +589,7 @@ impl<'a> ModifyInputsContext<'a> {
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(clip_node_id, 1);
let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
}

Expand Down
4 changes: 2 additions & 2 deletions node-graph/libraries/core-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
pub use table::{
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_SPREAD_METHOD,
ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME,
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
};
#[cfg(feature = "wasm")]
pub use tsify;
Expand Down
57 changes: 29 additions & 28 deletions node-graph/libraries/core-types/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,60 @@ use std::fmt::Debug;
// Standard attribute keys used across the data flow
// =====================================================================

/// Attribute key for a row's `DAffine2` transformation, applied when rendering and when accumulating
/// transforms through nested compositions.
/// Row's `DAffine2` transformation, composed multiplicatively through nested groups.
pub const ATTR_TRANSFORM: &str = "transform";

/// Attribute key for a row's `AlphaBlending` (blend mode + opacity + fill + clip), composed
/// multiplicatively through nested compositions.
pub const ATTR_ALPHA_BLENDING: &str = "alpha_blending";
/// Row's `BlendMode`, controlling how it composites with content beneath it.
pub const ATTR_BLEND_MODE: &str = "blend_mode";

/// Attribute key under which each row of an editor-aware layer stores a `Table<NodeId>` describing the
/// path (from the root document network) to the layer node that owns the row. Editor tools use this to
/// route clicks/selection back to the originating layer at any nesting depth.
/// Row's opacity multiplier (`f64`, implicit default `1.`).
/// Composed multiplicatively through nested groups. Affects content clipped to the row.
pub const ATTR_OPACITY: &str = "opacity";

/// Row's fill opacity multiplier (`f64`, implicit default `1.`).
/// Like opacity but does not affect content clipped to the row.
pub const ATTR_OPACITY_FILL: &str = "opacity_fill";

/// Whether a row inherits the alpha of the content beneath it (clipping mask).
pub const ATTR_CLIPPING_MASK: &str = "clipping_mask";

/// `Table<NodeId>` path from the root network to the layer node owning this row.
/// Used by editor tools to route clicks/selection back to the originating layer.
pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path";

/// Attribute key under which a row stores a `Table<Graphic>` snapshot of the upstream content that fed
/// into a destructive merge (Boolean Operation, Flatten Path, Morph, Rasterize, etc.). The renderer
/// recurses into this snapshot during metadata collection so the editor can still surface click targets
/// for the original child layers after their content has been collapsed into a single output.
/// `Table<Graphic>` snapshot of the upstream content that fed into a destructive merge
/// (Boolean Operation, Rasterize, etc.), so the editor can still surface click targets for
/// the original child layers after their content has been collapsed.
pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers";

/// Attribute key for the byte offset where a regex match begins in the input string, set by the
/// `regex_find_all` and `regex_capture` text nodes.
/// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes).
pub const ATTR_START: &str = "start";

/// Attribute key for the byte offset where a regex match ends in the input string, set by the
/// `regex_find_all` and `regex_capture` text nodes.
/// Byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes).
pub const ATTR_END: &str = "end";

/// Attribute key for a regex named-capture-group's name (empty for unnamed groups), set by the
/// `regex_capture` text node.
/// Regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node).
pub const ATTR_NAME: &str = "name";

/// Attribute key for a JSON value's type (`"string"`, `"number"`, `"object"`, etc.), set by the
/// `json_query_all` text node alongside each extracted value.
/// JSON value's type string (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'.
pub const ATTR_TYPE: &str = "type";

/// Attribute key for an artboard row's `DVec2` top-left corner location in document coordinates.
/// Artboard's `DVec2` top-left corner in document coordinates.
pub const ATTR_LOCATION: &str = "location";

/// Attribute key for an artboard row's `DVec2` width and height.
/// Artboard's `DVec2` width and height.
pub const ATTR_DIMENSIONS: &str = "dimensions";

/// Attribute key for an artboard row's `Color` background fill.
/// Artboard's `Color` background fill.
pub const ATTR_BACKGROUND: &str = "background";

/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard bounds.
/// Whether an artboard clips content to its bounds.
pub const ATTR_CLIP: &str = "clip";

/// Attribute key for a `Table<GradientStops>` row's `GradientSpreadMethod`, controlling the gradient's behavior
/// outside the start/end stops (`Pad` clamps to the boundary colors, `Reflect` mirrors, `Repeat` tiles).
/// Gradient's `GradientSpreadMethod` (`Pad`, `Reflect`, or `Repeat`).
pub const ATTR_SPREAD_METHOD: &str = "spread_method";

/// Attribute key for a `Table<GradientStops>` row's `GradientType`, choosing between a linear gradient (color
/// transitions along the gradient line) or a radial gradient (color transitions outward from the line's start).
/// Gradient's `GradientType` (`Linear` or `Radial`).
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";

// =====================
Expand Down
15 changes: 13 additions & 2 deletions node-graph/libraries/graphic-types/src/artboard.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::graphic::Graphic;
use core_types::blending::AlphaBlending;
use core_types::blending::BlendMode;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color};
Expand All @@ -22,6 +22,17 @@ use glam::{DAffine2, IVec2};
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Table<Graphic>>, D::Error> {
use serde::Deserialize;

/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}

/// Pre-migration shape of the artboard's stored data: the struct that used to live as the element
/// of `Table<Artboard>`. Kept as a private type so we can deserialize legacy documents into the new
/// `Table<Table<Graphic>>` (element = `content`, other fields → row attributes).
Expand All @@ -48,7 +59,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
alpha_blending: Vec<LegacyAlphaBlending>,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
93 changes: 57 additions & 36 deletions node-graph/libraries/graphic-types/src/graphic.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use core_types::blending::AlphaBlending;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::ops::TableConvert;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, Color};
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
use glam::DAffine2;
use raster_types::{CPU, GPU, Raster};
Expand Down Expand Up @@ -130,47 +130,57 @@ impl From<Table<GradientStops>> for Graphic {
/// Deeply flattens a `Table<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`Table`s composes transforms and opacity.
fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) -> Table<T> {
fn compose_alpha_blending(parent: AlphaBlending, child: AlphaBlending) -> AlphaBlending {
AlphaBlending {
blend_mode: child.blend_mode,
opacity: parent.opacity * child.opacity,
fill: child.fill,
clip: child.clip,
}
}

fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) {
for current_graphic_row in current_graphic_table.into_iter() {
let layer_path: Table<NodeId> = current_graphic_row.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default(ATTR_TRANSFORM);
let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default(ATTR_ALPHA_BLENDING);
let current_opacity: f64 = current_graphic_row.attribute_cloned_or(ATTR_OPACITY, 1.);
let current_fill: f64 = current_graphic_row.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);

match current_graphic_row.into_element() {
// Recurse into nested `Table<Graphic>` items, composing the parent's transform onto each child
// Compose the parent's transform, opacity, and fill onto each child row
Graphic::Graphic(mut sub_table) => {
for index in 0..sub_table.len() {
let child_transform: DAffine2 = sub_table.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let child_alpha_blending: AlphaBlending = sub_table.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index);
// Identity default means a missing column still composes correctly
for v in sub_table.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*v = current_transform * *v;
}

sub_table.set_attribute(ATTR_TRANSFORM, index, current_transform * child_transform);
sub_table.set_attribute(ATTR_ALPHA_BLENDING, index, compose_alpha_blending(current_alpha_blending, child_alpha_blending));
// f64 defaults to 0, but opacity/fill default to 1, so missing columns must be set rather than multiplied
if let Some(values) = sub_table.iter_attribute_values_mut::<f64>(ATTR_OPACITY) {
for v in values {
*v *= current_opacity;
}
} else {
for v in sub_table.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY) {
*v = current_opacity;
}
}
if let Some(values) = sub_table.iter_attribute_values_mut::<f64>(ATTR_OPACITY_FILL) {
for v in values {
*v *= current_fill;
}
} else {
for v in sub_table.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY_FILL) {
*v = current_fill;
}
}

flatten_recursive(output, sub_table, extract_variant);
}
// Try to extract the target variant; if it matches, push its items with composed transform and opacity
// Extract the target variant and push its items with composed transform, opacity, and fill
other => {
if let Some(typed_table) = extract_variant(other) {
for row in typed_table.into_iter() {
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let row_alpha_blending: AlphaBlending = row.attribute_cloned_or_default(ATTR_ALPHA_BLENDING);
let (element, mut attributes) = row.into_parts();
for mut item in typed_table.into_iter() {
let row_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let row_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.);
let row_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);

attributes.insert(ATTR_TRANSFORM, current_transform * row_transform);
attributes.insert(ATTR_ALPHA_BLENDING, compose_alpha_blending(current_alpha_blending, row_alpha_blending));
attributes.insert(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
item.set_attribute(ATTR_TRANSFORM, current_transform * row_transform);
item.set_attribute(ATTR_OPACITY, current_opacity * row_opacity);
item.set_attribute(ATTR_OPACITY_FILL, current_fill * row_fill);
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());

output.push(TableRow::from_parts(element, attributes));
output.push(item);
}
}
}
Expand Down Expand Up @@ -321,8 +331,9 @@ impl Graphic {

pub fn had_clip_enabled(&self) -> bool {
fn all_clipped<T>(table: &Table<T>) -> bool {
table.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING).all(|a| a.clip)
table.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
}

match self {
Graphic::Vector(table) => all_clipped(table),
Graphic::Graphic(table) => all_clipped(table),
Expand All @@ -335,12 +346,11 @@ impl Graphic {

pub fn can_reduce_to_clip_path(&self) -> bool {
match self {
Graphic::Vector(vector) => vector
.iter_element_values()
.zip(vector.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING))
.all(|(element, alpha_blending)| {
(alpha_blending.opacity > 1. - f32::EPSILON) && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
}),
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
opacity > 1. - f64::EPSILON && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
}),
_ => false,
}
}
Expand Down Expand Up @@ -474,12 +484,23 @@ impl<T: Clone> OmitIndex for Table<T> {
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
use serde::Deserialize;

/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}

#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldGraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>,
transform: DAffine2,
alpha_blending: AlphaBlending,
alpha_blending: LegacyAlphaBlending,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand All @@ -502,7 +523,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
alpha_blending: Vec<LegacyAlphaBlending>,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
Loading
Loading