diff --git a/Cargo.lock b/Cargo.lock index f8c06aa04a..70fe465199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6476,6 +6476,7 @@ dependencies = [ "graphic-types", "kurbo", "log", + "math-parser", "node-macro", "qrcodegen", "rand", diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c68bc5ac56..2f35e034cd 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -457,6 +457,10 @@ pub fn get_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::spiral::IDENTIFIER)) } +pub fn get_function_plot_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::function_plot::IDENTIFIER)) +} + pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) } diff --git a/editor/src/messages/tool/common_functionality/shapes/function_plot.rs b/editor/src/messages/tool/common_functionality/shapes/function_plot.rs new file mode 100644 index 0000000000..b70fa5b3ea --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/function_plot.rs @@ -0,0 +1,63 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct FunctionPlot; + +impl FunctionPlot { + pub fn create_node() -> NodeTemplate { + let node_type = resolve_proto_node_type(graphene_std::vector::plot_nodes::function_plot::IDENTIFIER).expect("Function Plot node can't be found"); + node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + viewport: &ViewportMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, viewport, center, lock_ratio) { + let Some(node_id) = graph_modification_utils::get_function_plot_id(layer, &document.network_interface) else { + return; + }; + + // We want viewport zoom independent size and 1 scalefactor + let document_to_viewport = document + .navigation_handler + .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); + + let start = document_to_viewport.inverse().transform_point2(start); + let end = document_to_viewport.inverse().transform_point2(end); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false), + }); + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_translation(start.midpoint(end)), + transform_in: TransformIn::Local, + skip_rerender: false, + }); + } + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index 4d74b15ba5..6a3a7f903b 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -2,6 +2,7 @@ pub mod arc_shape; pub mod arrow_shape; pub mod circle_shape; pub mod ellipse_shape; +pub mod function_plot; pub mod grid_shape; pub mod line_shape; pub mod polygon_shape; diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 5f0d6da016..9a69b648b4 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -35,6 +35,7 @@ pub enum ShapeType { Spiral, Grid, Arrow, + FunctionPlot, Line, // KEEP THIS AT THE END Rectangle, // KEEP THIS AT THE END Ellipse, // KEEP THIS AT THE END @@ -50,6 +51,7 @@ impl ShapeType { ShapeType::Spiral, ShapeType::Grid, ShapeType::Arrow, + ShapeType::FunctionPlot, ShapeType::Line, // KEEP THIS AT THE END ShapeType::Rectangle, // KEEP THIS AT THE END ShapeType::Ellipse, // KEEP THIS AT THE END @@ -70,6 +72,7 @@ impl ShapeType { Self::Spiral => "Spiral", Self::Grid => "Grid", Self::Arrow => "Arrow", + Self::FunctionPlot => "Function Plot", Self::Line => "Line", // KEEP THIS AT THE END Self::Rectangle => "Rectangle", // KEEP THIS AT THE END Self::Ellipse => "Ellipse", // KEEP THIS AT THE END diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 710b0def8d..570e5dcf10 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -15,6 +15,7 @@ use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::shapes::arc_shape::Arc; use crate::messages::tool::common_functionality::shapes::arrow_shape::Arrow; use crate::messages::tool::common_functionality::shapes::circle_shape::Circle; +use crate::messages::tool::common_functionality::shapes::function_plot::FunctionPlot; use crate::messages::tool::common_functionality::shapes::grid_shape::Grid; use crate::messages::tool::common_functionality::shapes::line_shape::LineToolData; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; @@ -30,6 +31,7 @@ use crate::messages::tool::utility_types::DocumentToolData; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::renderer::Quad; +use graphene_std::vector::function_plot; use graphene_std::vector::misc::{ArcType, GridType, SpiralType}; use graphene_std::vector::style::FillChoice; use graphene_std::{Color, NodeInputDecleration}; @@ -212,6 +214,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance { } .into() }), + MenuListEntry::new("FunctionPlot").label("Function Plot").on_commit(move |_| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::FunctionPlot), + } + .into() + }), ]]; DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_instance() } @@ -347,6 +355,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data: (spiral::IDENTIFIER, ShapeType::Spiral), (grid::IDENTIFIER, ShapeType::Grid), (arrow::IDENTIFIER, ShapeType::Arrow), + (function_plot::IDENTIFIER, ShapeType::FunctionPlot), ] .into_iter() .find_map(|(id, shape)| layer_view.upstream_node_id_from_name(&proto(id)).map(|_| shape)) else { @@ -430,7 +439,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data: changed = true; } } - ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle => {} + ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle | ShapeType::FunctionPlot => {} } changed @@ -1116,7 +1125,15 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { + ShapeType::Polygon + | ShapeType::Star + | ShapeType::Circle + | ShapeType::Arc + | ShapeType::Spiral + | ShapeType::Grid + | ShapeType::Rectangle + | ShapeType::Ellipse + | ShapeType::FunctionPlot => { tool_data.data.start(document, input, viewport); } ShapeType::Arrow | ShapeType::Line => { @@ -1139,6 +1156,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns), ShapeType::Grid => Grid::create_node(tool_options.grid_type), ShapeType::Arrow => Arrow::create_node(tool_options.arrow_shaft_width, tool_options.arrow_head_width, tool_options.arrow_head_length), + ShapeType::FunctionPlot => FunctionPlot::create_node(), ShapeType::Line => Line::create_node(), ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), @@ -1150,7 +1168,15 @@ impl Fsm for ShapeToolFsmState { let defered_responses = &mut VecDeque::new(); match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { + ShapeType::Polygon + | ShapeType::Star + | ShapeType::Circle + | ShapeType::Arc + | ShapeType::Spiral + | ShapeType::Grid + | ShapeType::Rectangle + | ShapeType::Ellipse + | ShapeType::FunctionPlot => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -1212,6 +1238,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Spiral => Spiral::update_shape(document, input, viewport, layer, tool_data, responses), ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses), ShapeType::Arrow => Arrow::update_shape(document, input, viewport, layer, tool_data, modifier, responses), + ShapeType::FunctionPlot => FunctionPlot::update_shape(document, input, viewport, layer, tool_data, modifier, responses), ShapeType::Line => Line::update_shape(document, input, viewport, layer, tool_data, modifier, responses), ShapeType::Rectangle => Rectangle::update_shape(document, input, viewport, layer, tool_data, modifier, responses), ShapeType::Ellipse => Ellipse::update_shape(document, input, viewport, layer, tool_data, modifier, responses), @@ -1464,6 +1491,11 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Boundary"), + HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], ShapeType::Line => vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), @@ -1488,7 +1520,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Spiral => HintGroup(vec![]), + ShapeType::Spiral | ShapeType::FunctionPlot => HintGroup(vec![]), ShapeType::Grid => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Arrow => HintGroup(vec![ HintInfo::keys([Key::Shift], "15° Increments"), diff --git a/libraries/math-parser/src/constants.rs b/libraries/math-parser/src/constants.rs index f2ca33c646..1d54f8052d 100644 --- a/libraries/math-parser/src/constants.rs +++ b/libraries/math-parser/src/constants.rs @@ -64,7 +64,7 @@ lazy_static! { ); map.insert( - "invsin", + "asin", Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))), [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))), @@ -73,7 +73,7 @@ lazy_static! { ); map.insert( - "invcos", + "acos", Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))), [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))), @@ -82,7 +82,7 @@ lazy_static! { ); map.insert( - "invtan", + "atan", Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))), [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))), @@ -91,7 +91,7 @@ lazy_static! { ); map.insert( - "invcsc", + "acsc", Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))), [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))), @@ -100,7 +100,7 @@ lazy_static! { ); map.insert( - "invsec", + "asec", Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))), [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))), @@ -109,7 +109,7 @@ lazy_static! { ); map.insert( - "invcot", + "acot", Box::new(|values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real((PI / 2. - real).atan()))), [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex((Complex::new(PI / 2., 0.) - complex).atan()))), @@ -117,6 +117,125 @@ lazy_static! { }), ); + map.insert( + "ln", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ln()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.ln()))), + _ => None, + }), + ); + + map.insert( + "log", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.log10()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.log10()))), + _ => None, + }), + ); + + map.insert( + "exp", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.exp()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.exp()))), + _ => None, + }), + ); + + map.insert( + "abs", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.norm()))), + _ => None, + }), + ); + + map.insert( + "floor", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.floor()))), + _ => None, + }), + ); + + map.insert( + "ceil", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.ceil()))), + _ => None, + }), + ); + + map.insert( + "round", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.round()))), + _ => None, + }), + ); + + map.insert( + "min", + Box::new(|values| match values { + [Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(a.min(*b)))), + _ => None, + }), + ); + + map.insert( + "max", + Box::new(|values| match values { + [Value::Number(Number::Real(a)), Value::Number(Number::Real(b))] => Some(Value::Number(Number::Real(a.max(*b)))), + _ => None, + }), + ); + + map.insert( + "atan2", + Box::new(|values| match values { + [Value::Number(Number::Real(y)), Value::Number(Number::Real(x))] => Some(Value::Number(Number::Real(y.atan2(*x)))), + _ => None, + }), + ); + + map.insert( + "sign", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.signum()))), + _ => None, + }), + ); + + map.insert( + "sinh", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sinh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sinh()))), + _ => None, + }), + ); + + map.insert( + "cosh", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cosh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cosh()))), + _ => None, + }), + ); + + map.insert( + "tanh", + Box::new(|values| match values { + [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tanh()))), + [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tanh()))), + _ => None, + }), + ); + map }; } diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index 87594c253b..316682506c 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -24,6 +24,7 @@ repeat-nodes = { workspace = true } dyn-any = { workspace = true } glam = { workspace = true } kurbo = { workspace = true } +math-parser = { workspace = true } rand = { workspace = true } rustc-hash = { workspace = true } log = { workspace = true } diff --git a/node-graph/nodes/vector/src/lib.rs b/node-graph/nodes/vector/src/lib.rs index 28ff413ea1..2645cbc391 100644 --- a/node-graph/nodes/vector/src/lib.rs +++ b/node-graph/nodes/vector/src/lib.rs @@ -1,5 +1,6 @@ pub mod generator_nodes; pub mod merge_qr_squares; +pub mod plot_nodes; pub mod vector_modification_nodes; mod vector_nodes; @@ -10,6 +11,7 @@ extern crate log; pub use core_types as gcore; pub use generator_nodes::*; pub use graphic_types; +pub use plot_nodes::*; pub use vector_modification_nodes::*; pub use vector_nodes::*; pub use vector_types; diff --git a/node-graph/nodes/vector/src/plot_nodes.rs b/node-graph/nodes/vector/src/plot_nodes.rs new file mode 100644 index 0000000000..1abb2f43cc --- /dev/null +++ b/node-graph/nodes/vector/src/plot_nodes.rs @@ -0,0 +1,588 @@ +use core_types::Ctx; +use core_types::list::{Item, List}; +use glam::DVec2; +use graphic_types::Vector; +use log::warn; +use math_parser::ast; +use math_parser::context::{EvalContext, NothingMap, ValueProvider}; +use math_parser::value::Value; +use std::collections::{HashMap, HashSet}; +use vector_types::subpath; + +const TOLERANCE_FACTOR: f64 = 0.005; + +#[derive(Debug, Clone, Copy)] +enum SamplePoint { + Valid { parameter: f64, coord: DVec2 }, + + Invalid { parameter: f64 }, +} + +impl SamplePoint { + pub fn parameter(&self) -> f64 { + match self { + SamplePoint::Valid { parameter, .. } => *parameter, + SamplePoint::Invalid { parameter } => *parameter, + } + } + + pub fn coord(&self) -> Option { + match self { + SamplePoint::Valid { coord, .. } => Some(*coord), + SamplePoint::Invalid { .. } => None, + } + } + + pub fn is_valid(&self) -> bool { + matches!(self, SamplePoint::Valid { .. }) + } +} + +type CurveSegment = Vec; + +#[derive(Debug)] +struct CurveBounds { + x_min: f64, + x_max: f64, + y_min: f64, + y_max: f64, +} + +#[derive(Debug, Clone, Copy)] +struct PlotTransform { + scale: f64, + x_center: f64, + y_center: f64, +} + +struct PlotContext<'a> { + values: &'a HashMap, +} + +impl ValueProvider for PlotContext<'_> { + fn get_value(&self, name: &str) -> Option { + self.values.get(name).map(|v| Value::from_f64(*v)) + } +} + +/// Plots a parametric curve. Please note that at most one variable is currently supported in the expressions. +/// +/// You can use the following mathematical operators: +/// • **Addition**: x + y +/// • **Subtraction**: x - y +/// • **Multiplication**: x * y or 2x +/// • **Division**: x / y +/// • **Modulo**: x % y +/// • **Power**: x ^ y +/// +/// You can use the following trigonometric functions: +/// • **sin(x)**: sine +/// • **cos(x)**: cosine +/// • **tan(x)**: tangent +/// • **csc(x)**: cosecant, 1 / sin(x) +/// • **sec(x)**: secant, 1 / cos(x) +/// • **cot(x)**: cotangent, 1 / tan(x) +/// +/// You can use the following inverse trigonometric functions: +/// • **asin(x)**: arcsine +/// • **acos(x)**: arccosine +/// • **atan(x)**: arctangent +/// • **acsc(x)**: inverse cosecant, asin(1 / x) +/// • **asec(x)**: inverse secant, acos(1 / x) +/// • **acot(x)**: inverse cotangent, atan(π/2 - x) +/// +/// You can use the following mathematical functions: +/// • **sqrt(x)**: square root +/// • **abs(x)**: absolute value +/// • **exp(x)**: exponential function, e^x +/// • **ln(x)**: natural logarithm +/// • **log(x)**: base-10 logarithm +/// • **floor(x)**: round down to the nearest integer +/// • **ceil(x)**: round up to the nearest integer +/// • **round(x)**: round to the nearest integer +/// • **sign(x)**: sign function (-1, 0, or 1) +/// +/// You can use the following multi-argument functions: +/// • **min(a, b)**: minimum of two values +/// • **max(a, b)**: maximum of two values +/// • **atan2(y, x)**: four-quadrant arctangent +/// +/// You can use the following hyperbolic functions: +/// • **sinh(x)**: hyperbolic sine +/// • **cosh(x)**: hyperbolic cosine +/// • **tanh(x)**: hyperbolic tangent +/// +/// You can use the following mathematical constants: +/// • **pi**: π ≈ 3.141592653589793 +/// • **tau**: τ = 2π +/// • **e**: Euler's number, ≈ 2.718281828459045 +/// • **phi**: Golden ratio, ≈ 1.618033988749895 +/// • **G**: Standard gravitational acceleration +/// +#[node_macro::node(category("Vector: Shape"))] +fn function_plot( + _: impl Ctx, + _primary: (), + /// Width of the plot's bounding box + #[unit(" px")] + #[default(100)] + width: f64, + /// Height of the plot's bounding box + #[unit(" px")] + #[default(100)] + height: f64, + /// A math expression for x(t). For y = f(x) functions simply enter x. + #[default(sin(t + pi / 2))] + x_expression: String, + /// A math expression for y(t). For y = f(x) functions simply enter f(x). + #[default(sin(2 * t))] + y_expression: String, + /// Minimum value for the parameter to evaluate. + #[default(0.)] + parameter_min_value: f64, + /// Maximum value for the parameter to evaluate. + #[default(6.28318530718)] + parameter_max_value: f64, + /// Level of sampling detail + #[default(7)] + level_of_detail: u32, + /// Discontinuity detection sensitivity + #[default(0.3)] + discontinuity_sensitivity: f64, + /// Auto close + #[default(true)] + auto_close: bool, + /// Plot Axes + #[default(true)] + plot_axes: bool, +) -> List { + let mut plots = List::new(); + let (x_node, y_node, variable_name) = match parse_plot_expressions(&x_expression, &y_expression) { + Some(result) => result, + None => return plots, + }; + + let (sample_points, bounds) = match sample_plot_curve(&x_node, &y_node, &variable_name, parameter_min_value, parameter_max_value, level_of_detail) { + Some(result) => result, + None => return plots, + }; + + let plot_transform = calculate_plot_transform(&bounds, width, height); + + let segments = detect_curve_segments(sample_points, discontinuity_sensitivity); + + for mut segment_anchors in segments { + fit_plot_to_bounds(&mut segment_anchors, &plot_transform); + + let mut closed = false; + + if auto_close && let (Some(first), Some(last)) = (segment_anchors.first(), segment_anchors.last()) { + let distance = first.distance(*last); + + closed = distance < 1.; + if closed { + segment_anchors.pop(); + } + } + + let shape = Vector::from_subpath(subpath::Subpath::from_anchors(segment_anchors, closed)); + + if plots.is_empty() { + plots = List::new_from_element(shape); + } else { + plots.push(Item::new_from_element(shape)); + } + } + + if plot_axes { + let axes = build_plot_axes(&bounds, &plot_transform); + for axis in axes { + let shape = Vector::from_subpath(subpath::Subpath::from_anchors(axis, false)); + + if plots.is_empty() { + plots = List::new_from_element(shape); + } else { + plots.push(Item::new_from_element(shape)); + } + } + } + + plots +} + +fn sample_plot_curve(x_node: &ast::Node, y_node: &ast::Node, variable_name: &str, parameter_min_value: f64, parameter_max_value: f64, sample_count: u32) -> Option<(Vec, CurveBounds)> { + let point_start = evaluate_expressions(parameter_min_value, x_node, y_node, variable_name)?; + + let point_end = evaluate_expressions(parameter_max_value, x_node, y_node, variable_name)?; + + let mut bounds = CurveBounds { + x_min: f64::INFINITY, + x_max: f64::NEG_INFINITY, + y_min: f64::INFINITY, + y_max: f64::NEG_INFINITY, + }; + + if point_start.is_valid() { + update_bounds(&point_start, &mut bounds); + } + + if point_end.is_valid() { + update_bounds(&point_end, &mut bounds); + } + + let mut sample_points = Vec::::new(); + + sample_interval(point_start, point_end, &mut sample_points, &mut bounds, x_node, y_node, variable_name, 0, 4, sample_count); + + sample_points.push(point_end); + + Some((sample_points, bounds)) +} + +#[allow(clippy::too_many_arguments)] +fn sample_interval( + begin_point: SamplePoint, + end_point: SamplePoint, + sample_points: &mut Vec, + bounds: &mut CurveBounds, + x_node: &ast::Node, + y_node: &ast::Node, + variable_name: &str, + current_depth: u32, + min_depth: u32, + max_depth: u32, +) { + if current_depth >= max_depth { + update_bounds(&begin_point, bounds); + sample_points.push(begin_point); + return; + } + + let t_begin = begin_point.parameter(); + let t_end = end_point.parameter(); + let t_mid = t_begin + (t_end - t_begin) * 0.50; + let Some(point_mid) = evaluate_expressions(t_mid, x_node, y_node, variable_name) else { + return; + }; + + if current_depth > min_depth && begin_point.is_valid() && end_point.is_valid() { + let t_quarter = t_begin + (t_end - t_begin) * 0.25; + let t_q3 = t_begin + (t_end - t_begin) * 0.75; + + let Some(point_q1) = evaluate_expressions(t_quarter, x_node, y_node, variable_name) else { + return; + }; + + let Some(point_q3) = evaluate_expressions(t_q3, x_node, y_node, variable_name) else { + return; + }; + + let Some(begin_coord) = begin_point.coord() else { return }; + let Some(end_coord) = end_point.coord() else { return }; + + let segment_length = begin_coord.distance(end_coord); + let tolerance = segment_length * TOLERANCE_FACTOR; + + let segment_q1 = begin_coord + (end_coord - begin_coord) * 0.25; + let segment_mid = (begin_coord + end_coord) * 0.5; + let segment_q3 = begin_coord + (end_coord - begin_coord) * 0.75; + + let mut max_error: f64 = 0.; + + if let Some(q1) = point_q1.coord() { + max_error = max_error.max(q1.distance(segment_q1)); + } + if let Some(mid) = point_mid.coord() { + max_error = max_error.max(mid.distance(segment_mid)); + } + if let Some(q3) = point_q3.coord() { + max_error = max_error.max(q3.distance(segment_q3)); + } + + if max_error < tolerance { + update_bounds(&begin_point, bounds); + sample_points.push(begin_point); + return; + } + } + + sample_interval(begin_point, point_mid, sample_points, bounds, x_node, y_node, variable_name, current_depth + 1, min_depth, max_depth); + + sample_interval(point_mid, end_point, sample_points, bounds, x_node, y_node, variable_name, current_depth + 1, min_depth, max_depth); +} + +fn evaluate_expressions(t: f64, x_node: &ast::Node, y_node: &ast::Node, variable_name: &str) -> Option { + let mut values = HashMap::::new(); + values.insert(variable_name.to_string(), t); + + let context = EvalContext::new(PlotContext { values: &values }, NothingMap); + + let x_value = match x_node.eval(&context) { + Ok(value) => value, + Err(e) => { + warn!("Expression evaluation error: {e:?}"); + return None; + } + }; + + let y_value = match y_node.eval(&context) { + Ok(value) => value, + Err(e) => { + warn!("Expression evaluation error: {e:?}"); + return None; + } + }; + + let point = match (x_value.as_real().filter(|v| v.is_finite()), y_value.as_real().filter(|v| v.is_finite())) { + (Some(x), Some(y)) => SamplePoint::Valid { + parameter: t, + coord: DVec2::new(x, y), + }, + + _ => SamplePoint::Invalid { parameter: t }, + }; + + Some(point) +} + +fn update_bounds(point: &SamplePoint, bounds: &mut CurveBounds) { + let Some(coord) = point.coord() else { + return; + }; + + bounds.x_min = bounds.x_min.min(coord.x); + bounds.x_max = bounds.x_max.max(coord.x); + + bounds.y_min = bounds.y_min.min(coord.y); + bounds.y_max = bounds.y_max.max(coord.y); +} + +fn detect_curve_segments(curve_points: Vec, discontinuity_sensitivity: f64) -> Vec { + let mut segments: Vec> = Vec::new(); + let mut segment = Vec::::new(); + + let mut previous_right_continuity = Some(true); + + for i in 0..curve_points.len() { + let current_point = &curve_points[i]; + if !current_point.is_valid() { + continue; + } + + let mut push_to_last_segment = false; + let mut continue_segment = true; + if i < curve_points.len() - 1 { + continue_segment = curve_points[i + 1].is_valid(); + } + + if continue_segment { + let mut left_continuity = Some(true); + let mut right_continuity = Some(true); + + if i > 1 { + let prev_prev_point = &curve_points[i - 2]; + let prev_point = &curve_points[i - 1]; + left_continuity = extrapolate_continuity(current_point, prev_prev_point, prev_point, discontinuity_sensitivity); + } + + if i < curve_points.len() - 2 { + let next_point = &curve_points[i + 1]; + let next_next_point = &curve_points[i + 2]; + + right_continuity = extrapolate_continuity(current_point, next_point, next_next_point, discontinuity_sensitivity); + } + + if right_continuity == Some(false) { + continue_segment = false; + } + + if previous_right_continuity == Some(false) && left_continuity == Some(true) { + push_to_last_segment = true; + } + + previous_right_continuity = right_continuity; + } + + if push_to_last_segment && !segments.is_empty() { + segments.last_mut().unwrap().push(current_point.coord().unwrap()); + } else { + segment.push(current_point.coord().unwrap()); + } + + if !continue_segment { + if segment.len() > 1 { + segments.push(segment); + } + + segment = Vec::new(); + } + } + + if segment.len() > 1 { + segments.push(segment); + } + + segments +} + +fn extrapolate_continuity(point: &SamplePoint, neighbor1: &SamplePoint, neighbor2: &SamplePoint, discontinuity_sensitivity: f64) -> Option { + if !point.is_valid() || !neighbor1.is_valid() || !neighbor2.is_valid() { + return None; + } + let current_coord = point.coord().unwrap(); + + let left_limit_x = extrapolate_limit( + DVec2 { + x: point.parameter(), + y: current_coord.x, + }, + DVec2 { + x: neighbor1.parameter(), + y: neighbor1.coord().unwrap().x, + }, + DVec2 { + x: neighbor2.parameter(), + y: neighbor2.coord().unwrap().x, + }, + ); + + let left_limit_y = extrapolate_limit( + DVec2 { + x: point.parameter(), + y: current_coord.y, + }, + DVec2 { + x: neighbor1.parameter(), + y: neighbor1.coord().unwrap().y, + }, + DVec2 { + x: neighbor2.parameter(), + y: neighbor2.coord().unwrap().y, + }, + ); + + let segment_length = current_coord.distance(neighbor2.coord().unwrap()); + let tolerance = segment_length * discontinuity_sensitivity; + + if (left_limit_x - current_coord.x).abs() > tolerance || (left_limit_y - current_coord.y).abs() > tolerance { + return Some(false); + } + + Some(true) +} + +fn extrapolate_limit(p_coord: DVec2, p1_coord: DVec2, p2_coord: DVec2) -> f64 { + if (p2_coord.x - p1_coord.x).abs() > f64::EPSILON { + (p_coord.x - p1_coord.x) * (p2_coord.y - p1_coord.y) / (p2_coord.x - p1_coord.x) + p1_coord.y + } else { + p2_coord.y + (p2_coord.y - p1_coord.y) + } +} + +fn calculate_plot_transform(bounds: &CurveBounds, width: f64, height: f64) -> PlotTransform { + let x_scale = width / (bounds.x_max - bounds.x_min).max(f64::EPSILON); + let y_scale = height / (bounds.y_max - bounds.y_min).max(f64::EPSILON); + + let scale = x_scale.min(y_scale); + + let x_center = (bounds.x_min + bounds.x_max) / 2.; + let y_center = (bounds.y_min + bounds.y_max) / 2.; + + PlotTransform { scale, x_center, y_center } +} + +fn fit_plot_to_bounds(anchor_positions: &mut [DVec2], plot_transform: &PlotTransform) { + for anchor_position in anchor_positions { + *anchor_position = fit_point_to_bounds(*anchor_position, plot_transform); + } +} + +fn fit_point_to_bounds(point: DVec2, plot_transform: &PlotTransform) -> DVec2 { + DVec2::new((point.x - plot_transform.x_center) * plot_transform.scale, (plot_transform.y_center - point.y) * plot_transform.scale) +} + +fn build_plot_axes(bounds: &CurveBounds, plot_transform: &PlotTransform) -> Vec { + let axis_x = if bounds.x_min <= 0. && bounds.x_max >= 0. { 0. } else { (bounds.x_min + bounds.x_max) / 2. }; + + let axis_y = if bounds.y_min <= 0. && bounds.y_max >= 0. { 0. } else { (bounds.y_min + bounds.y_max) / 2. }; + + let horizontal = vec![ + fit_point_to_bounds(DVec2::new(bounds.x_min, axis_y), plot_transform), + fit_point_to_bounds(DVec2::new(bounds.x_max, axis_y), plot_transform), + ]; + + let vertical = vec![ + fit_point_to_bounds(DVec2::new(axis_x, bounds.y_min), plot_transform), + fit_point_to_bounds(DVec2::new(axis_x, bounds.y_max), plot_transform), + ]; + + vec![horizontal, vertical] +} + +fn parse_plot_expressions(x_expression: &str, y_expression: &str) -> Option<(ast::Node, ast::Node, String)> { + let (x_node, x_vars) = parse_expression(x_expression)?; + let (y_node, y_vars) = parse_expression(y_expression)?; + + let variable_name = match (x_vars.iter().next(), y_vars.iter().next()) { + (Some(x), Some(y)) => { + if x != y { + warn!("X and Y expressions must use the same variable"); + return None; + } + x.clone() + } + (Some(x), None) => x.clone(), + (None, Some(y)) => y.clone(), + (None, None) => { + warn!("At least one variable is required"); + return None; + } + }; + + Some((x_node, y_node, variable_name)) +} + +fn parse_expression(expression: &str) -> Option<(ast::Node, HashSet)> { + let (node, _unit) = match ast::Node::try_parse_from_str(expression) { + Ok(expr) => expr, + Err(e) => { + warn!("Invalid expression: `{expression}`\n{e:?}"); + return None; + } + }; + + let mut variables = HashSet::new(); + collect_variables(&node, &mut variables); + + if variables.len() > 1 { + warn!("Currently at most one variable is supported"); + return None; + } + + Some((node, variables)) +} + +fn collect_variables(node: &ast::Node, vars: &mut HashSet) { + match node { + ast::Node::Var(name) => { + vars.insert(name.clone()); + } + + ast::Node::Lit(_) => {} + + ast::Node::UnaryOp { expr, .. } => { + collect_variables(expr, vars); + } + + ast::Node::BinOp { lhs, rhs, .. } => { + collect_variables(lhs, vars); + collect_variables(rhs, vars); + } + + ast::Node::FnCall { expr, .. } => { + for arg in expr { + collect_variables(arg, vars); + } + } + } +}