Skip to content
Open
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 frontend/src/components/floating-menus/NodeCatalog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
</script>

<LayoutCol class="node-catalog">
<TextInput placeholder="Search Nodes…" value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
<TextInput placeholder="Search Nodes…" value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} selectAllOnFocus={false} bind:this={nodeSearchInput} />
<div class="list-results" on:wheel|passive|stopPropagation>
{#each nodeCategories as nodeCategory}
<details open={nodeCategory[1].open}>
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/widgets/inputs/TextInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
// Sizing
export let minWidth = 0;
export let maxWidth = 0;
// Tooltips
// Tooltips
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Behavior
export let selectAllOnFocus = true;

let className = "";
export { className as class };
Expand All @@ -28,10 +30,10 @@
let self: FieldInput | undefined;
let editing = false;

function onTextFocused() {
function onTextFocused() {
editing = true;

self?.selectAllText(value);
if (selectAllOnFocus) self?.selectAllText(value);
}

// Called only when `value` is changed from the <input> element via user input and committed, either with the
Expand Down
2 changes: 2 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use std::fmt::Debug;
pub const ATTR_TRANSFORM: &str = "transform";
/// Item's `BlendMode`, controlling how it composites with content beneath it.
pub const ATTR_BLEND_MODE: &str = "blend_mode";
/// Item's SVG-compatible filter effects, applied in order during rendering.
pub const ATTR_FILTER_EFFECTS: &str = "filter_effects";
/// Item's opacity multiplier (`f64`, implicit default `1.`).
/// Composed multiplicatively through nested groups. Affects content clipped to the item.
pub const ATTR_OPACITY: &str = "opacity";
Expand Down
17 changes: 17 additions & 0 deletions node-graph/libraries/graphic-types/src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, dyn_any::DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SvgFilterEffect {
GaussianBlur {
std_deviation_x: f64,
std_deviation_y: f64,
},
}

impl Default for SvgFilterEffect {
fn default() -> Self {
Self::GaussianBlur {
std_deviation_x: 0.,
std_deviation_y: 0.,
}
}
}
2 changes: 2 additions & 0 deletions node-graph/libraries/graphic-types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub mod artboard;
pub mod graphic;
pub mod filter;

// Re-export all transitive dependencies so downstream crates only need to depend on graphic-types
pub use core_types;
pub use raster_types;
pub use vector_types;
pub use filter::SvgFilterEffect;

// Re-export commonly used types at the crate root
pub use artboard::Artboard;
Expand Down
33 changes: 31 additions & 2 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use core_types::bounds::RenderBoundingBox;
use core_types::color::Color;
use core_types::color::SRGBA8;
use core_types::consts::DEFAULT_FONT_SIZE;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
use core_types::list::{ATTR_FILL, ATTR_FILTER_EFFECTS, ATTR_STROKE, Item, List};
use core_types::math::quad::Quad;
use core_types::render_complexity::RenderComplexity;
use core_types::transform::Footprint;
Expand All @@ -21,6 +21,7 @@ use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource;
use graphic_types::SvgFilterEffect;
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
Expand Down Expand Up @@ -268,6 +269,29 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
}) + ")"
}

fn write_svg_filter_def(svg_defs: &mut String, effects: &[SvgFilterEffect]) -> Option<String> {
if effects.is_empty() {
return None;
}

let id = format!("filter-{}", generate_uuid());
write!(svg_defs, r#"<filter id="{id}" x="-50%" y="-50%" width="200%" height="200%" color-interpolation-filters="sRGB">"#).unwrap();

let mut input = "SourceGraphic".to_string();
for (index, effect) in effects.iter().enumerate() {
let result = format!("effect{index}");
match effect {
SvgFilterEffect::GaussianBlur { std_deviation_x, std_deviation_y } => {
write!(svg_defs, r#"<feGaussianBlur in="{input}" stdDeviation="{std_deviation_x} {std_deviation_y}" result="{result}"/>"#).unwrap();
}
}
input = result;
}

svg_defs.push_str("</filter>");
Some(id)
}

/// `(max, min)` factors by which a unit vector is stretched under `transform`'s linear part — the
/// principal and minor singular values, equal to the semi-axes of the ellipse a unit circle maps to.
/// Equivalent to `(max(sx, sy), min(sx, sy))` for axis-aligned scales, but accounts for shear.
Expand Down Expand Up @@ -835,6 +859,7 @@ impl Render for List<Graphic> {
for index in 0..self.len() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let filter_effects: Vec<SvgFilterEffect> = self.attribute_cloned_or_default(ATTR_FILTER_EFFECTS, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let element = self.element(index).unwrap();
Expand Down Expand Up @@ -1061,9 +1086,9 @@ impl Render for List<Vector> {
let Some(vector) = self.element(index) else { continue };
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let filter_effects: Vec<SvgFilterEffect> = self.attribute_cloned_or_default(ATTR_FILTER_EFFECTS, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);

// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
Expand Down Expand Up @@ -1161,6 +1186,10 @@ impl Render for List<Vector> {
attributes.push(ATTR_TRANSFORM, matrix);
}

if let Some(filter_id) = write_svg_filter_def(&mut attributes.0.svg_defs, &filter_effects) {
attributes.push("filter", format!("url(#{filter_id})"));
}

let defs = &mut attributes.0.svg_defs;
if let Some((ref id, mask_type, ref vector_item)) = push_id {
let mut svg = SvgRender::new();
Expand Down
45 changes: 44 additions & 1 deletion node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher};
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
use core_types::list::{ATTR_FILL, ATTR_FILTER_EFFECTS, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId;
Expand All @@ -15,6 +15,7 @@ use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::SvgFilterEffect;
use graphic_types::{Graphic, IntoGraphicList};
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
Expand Down Expand Up @@ -230,6 +231,48 @@ async fn fill<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send +
content
}

trait FilterListIterMut {
fn push_filter_effect(&mut self, effect: SvgFilterEffect);
}

impl FilterListIterMut for List<Vector> {
fn push_filter_effect(&mut self, effect: SvgFilterEffect) {
for effects in self.iter_attribute_values_mut_or_default::<Vec<SvgFilterEffect>>(ATTR_FILTER_EFFECTS) {
effects.push(effect.clone());
}
}
}

impl FilterListIterMut for List<Graphic> {
fn push_filter_effect(&mut self, effect: SvgFilterEffect) {
for graphic in self.iter_element_values_mut() {
let Some(vector_list) = graphic.as_vector_mut() else { continue };
vector_list.push_filter_effect(effect.clone());
}
}
}

#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn svg_gaussian_blur<T>(
_: impl Ctx,
#[implementations(List<Vector>, List<Graphic>)]
mut content: T,
#[range]
#[hard(0..)]
#[soft(..100)]
std_deviation: PixelLength,
) -> T
where
T: FilterListIterMut + 'n + Send,
{
content.push_filter_effect(SvgFilterEffect::GaussianBlur {
std_deviation_x: std_deviation,
std_deviation_y: std_deviation,
});
content
}


trait IntoF64Vec {
fn into_vec(self) -> Vec<f64>;
}
Expand Down