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} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not relevant to this PR (probably #4319). Please keep your branches separate to allow for reviewing small changes…

<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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this name should include SVG since gaussian blur is a standard filter that exists outside of the SVG specification. Other graphic types do not include SVG specific names.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guassian blur has only one input, so can get it implicity from the filter effect before. Other effects take several inputs, so you need to expand the data structure to take this into account.

GaussianBlur {
std_deviation_x: f64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The SvgFilterEffect::GaussianBlur variant stores raw f64 standard deviations with no constraints, but SVG feGaussianBlur requires non-negative numeric stdDeviation values. Because the type is public and serde-deserializable, invalid values (negative, NaN, infinite) can enter the data model from persisted documents or untrusted inputs. The SVG renderer then emits these raw values directly without validation, producing invalid SVG. Adding validation — for example by clamping to 0..=f64::MAX at render time, guarding deserialization, or introducing a constrained non-negative wrapper type — would prevent this invalid-state leak into exported output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/graphic-types/src/filter.rs, line 5:

<comment>The `SvgFilterEffect::GaussianBlur` variant stores raw `f64` standard deviations with no constraints, but SVG `feGaussianBlur` requires non-negative numeric `stdDeviation` values. Because the type is public and serde-deserializable, invalid values (negative, NaN, infinite) can enter the data model from persisted documents or untrusted inputs. The SVG renderer then emits these raw values directly without validation, producing invalid SVG. Adding validation — for example by clamping to `0..=f64::MAX` at render time, guarding deserialization, or introducing a constrained non-negative wrapper type — would prevent this invalid-state leak into exported output.</comment>

<file context>
@@ -0,0 +1,17 @@
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum SvgFilterEffect {
+	GaussianBlur {
+		std_deviation_x: f64,
+		std_deviation_y: f64,
+	},
</file context>

std_deviation_y: f64,
},
}

impl Default for SvgFilterEffect {
fn default() -> Self {
Self::GaussianBlur {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If more SvgFilterEffects are added, it doesn't make sense to have the default be a gaussian blur. Perhaps the default trait should not be implemented?

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be better in the vector-types crate rather than in graphic-types? This matches other vector graphics rendering related features such as gradients.


// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per code organisation, please put in the section below. This section is meant to "Re-export all transitive dependencies".


// 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> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The emitted <filter> uses a hard-coded region of x="-50%" y="-50%" width="200%" height="200%". Since filterUnits defaults to objectBoundingBox, these percentages are relative to the filtered element's bounding box. The filter region acts as a hard clipping rectangle for filter primitives, and feGaussianBlur with a large stdDeviation can extend well beyond it. The Gaussian blur node allows unbounded standard deviations (#[hard(0..)]), so a small object with a large blur (e.g., 10px wide with σ=100) would only receive ~5px of margin and be visibly clipped in the exported SVG. Consider computing the filter region from the blur radius and object bounds, or using filterUnits="userSpaceOnUse" with explicit coordinates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/renderer.rs, line 272:

<comment>The emitted `<filter>` uses a hard-coded region of `x="-50%" y="-50%" width="200%" height="200%"`. Since `filterUnits` defaults to `objectBoundingBox`, these percentages are relative to the filtered element's bounding box. The filter region acts as a hard clipping rectangle for filter primitives, and `feGaussianBlur` with a large `stdDeviation` can extend well beyond it. The Gaussian blur node allows unbounded standard deviations (`#[hard(0..)]`), so a small object with a large blur (e.g., 10px wide with σ=100) would only receive ~5px of margin and be visibly clipped in the exported SVG. Consider computing the filter region from the blur radius and object bounds, or using `filterUnits="userSpaceOnUse"` with explicit coordinates.</comment>

<file context>
@@ -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;
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Identical filter effects are emitted as separate SVG <filter> definitions for each vector path because a new UUID is generated on every call. Applying one blur to many paths will bloat the SVG <defs> with duplicate filters. Consider deduplicating by hashing the effect slice (or caching the ID), similar to how image data is already deduplicated elsewhere in SvgRender.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/renderer.rs, line 272:

<comment>Identical filter effects are emitted as separate SVG `<filter>` definitions for each vector path because a new UUID is generated on every call. Applying one blur to many paths will bloat the SVG `<defs>` with duplicate filters. Consider deduplicating by hashing the effect slice (or caching the ID), similar to how image data is already deduplicated elsewhere in `SvgRender`.</comment>

<file context>
@@ -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;
</file context>

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add comment to explain why the default expansion was not enough to avoid clipping.


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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to specify the in or result attributes if you just want to apply filter effects from top to bottom (which is what you are doing here anyway).

}
}
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);
Comment thread
sahilsahni18 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per #3779, all things must now be a List<T> for unknown reasons. Consult with Keavon if unsure.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the filter should be applied to the wrapping <g> tag in List<Graphic>::render_svg rather than individual paths, this variable is no longer needed here. Please remove it to avoid unused variable warnings.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Applying the filter at this level (inside List<Vector>::render_svg) means it targets individual <path> elements rather than the enclosing <g> group. This causes two issues:

  1. When a vector has separate fill and stroke paths (e.g., needs_separate_alignment_fill is true), only the stroke path receives the filter—the fill path remains unfiltered.
  2. If List<Graphic>::render_svg also reads ATTR_FILTER_EFFECTS for the wrapping <g>, the element gets double-filtered.

Consider moving the filter application to the <g> tag in List<Graphic>::render_svg so the entire vector element (all its paths) is filtered as a single unit, consistent with how opacity and blend mode are handled at the group level.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/renderer.rs, line 1189:

<comment>Applying the filter at this level (inside `List<Vector>::render_svg`) means it targets individual `<path>` elements rather than the enclosing `<g>` group. This causes two issues:

1. When a vector has separate fill and stroke paths (e.g., `needs_separate_alignment_fill` is true), only the stroke path receives the filter—the fill path remains unfiltered.
2. If `List<Graphic>::render_svg` also reads `ATTR_FILTER_EFFECTS` for the wrapping `<g>`, the element gets double-filtered.

Consider moving the filter application to the `<g>` tag in `List<Graphic>::render_svg` so the entire vector element (all its paths) is filtered as a single unit, consistent with how opacity and blend mode are handled at the group level.</comment>

<file context>
@@ -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})"));
+				}
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Gaussian blur filter visually expands content beyond the vector's geometric bounds, but BoundingBox/RenderBoundingBox calculations are not updated to account for ATTR_FILTER_EFFECTS. While the emitted <filter> sets a generous primitive subregion (x="-50%" y="-50%" width="200%" height="200%"), that only affects filtering within the element's local region; it does not expand the overall document viewBox or content bounds used for export sizing, thumbnail generation, or culling. If a blurred element sits near the edge of the computed bounds, the blur halo can be clipped. Consider inflating bounding box calculations by the blur radius (or maximum filter extent) when ATTR_FILTER_EFFECTS is present, so that export and layout logic accounts for the full visual footprint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/renderer.rs, line 1189:

<comment>The Gaussian blur filter visually expands content beyond the vector's geometric bounds, but `BoundingBox`/`RenderBoundingBox` calculations are not updated to account for `ATTR_FILTER_EFFECTS`. While the emitted `<filter>` sets a generous primitive subregion (`x="-50%" y="-50%" width="200%" height="200%"`), that only affects filtering within the element's local region; it does not expand the overall document `viewBox` or content bounds used for export sizing, thumbnail generation, or culling. If a blurred element sits near the edge of the computed bounds, the blur halo can be clipped. Consider inflating bounding box calculations by the blur radius (or maximum filter extent) when `ATTR_FILTER_EFFECTS` is present, so that export and layout logic accounts for the full visual footprint.</comment>

<file context>
@@ -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})"));
+				}
</file context>

attributes.push("filter", format!("url(#{filter_id})"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When zooming the viewport, the amount of blur changes. This is not desirable.

}
Comment on lines +1189 to +1191

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Applying the filter directly to individual <path> elements inside List<Vector>::render_svg is problematic because:

  1. If a vector has separate fill and stroke paths (e.g., when needs_separate_alignment_fill is true), only the stroke path gets the filter, leaving the fill path unfiltered.
  2. It leads to double-filtering if the parent group <g> tag also applies the filter.

Since every graphic element is already wrapped in a <g> tag by List<Graphic>::render_svg, we should apply the filter to the <g> tag there instead of here. This ensures the entire vector element (including all its paths) is filtered as a single unit.

Please remove this block and instead apply the filter to the <g> tag in List<Graphic>::render_svg.


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());
}
}
}
Comment on lines +246 to +253

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Currently, FilterListIterMut for List<Graphic> is implemented by pushing the filter down to the child vectors. This has two major drawbacks:

  1. Non-vector graphics (such as Text, Raster, or nested Groups) inside the List<Graphic> will not receive the filter.
  2. The filter_effects attribute is never set on the List<Graphic> itself, meaning List<Graphic>::render_svg will always see an empty filter list for the group.

Instead, we should set the ATTR_FILTER_EFFECTS attribute on the List<Graphic> itself, so the filter is applied to the entire group/layer as a single unit. This matches how other layer-level attributes (like opacity and blend mode) are handled.

impl FilterListIterMut for List<Graphic> {
	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());
		}
	}
}


#[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,
Comment on lines +269 to +270

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not allow users to set the x and y to different values?

});
content
}


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