port to egui (WIP)
This commit is contained in:
125
src/chart_manager.rs
Normal file
125
src/chart_manager.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use crate::misc::{add_asterisks, Cache, Function};
|
||||
|
||||
// Manages Chart generation and caching of values
|
||||
pub struct ChartManager {
|
||||
function: Function,
|
||||
min_x: f64,
|
||||
max_x: f64,
|
||||
num_interval: usize,
|
||||
resolution: usize,
|
||||
back_cache: Cache<Vec<(f64, f64)>>,
|
||||
front_cache: Cache<(Vec<(f64, f64, f64)>, f64)>,
|
||||
}
|
||||
|
||||
impl ChartManager {
|
||||
pub fn new(
|
||||
func_str: String, min_x: f64, max_x: f64, num_interval: usize,
|
||||
resolution: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
function: Function::from_string(func_str),
|
||||
min_x,
|
||||
max_x,
|
||||
num_interval,
|
||||
resolution,
|
||||
back_cache: Cache::new_empty(),
|
||||
front_cache: Cache::new_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw(
|
||||
&mut self
|
||||
) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) {
|
||||
let absrange = (self.max_x - self.min_x).abs();
|
||||
let data: Vec<(f64, f64)> = match self.back_cache.is_valid() {
|
||||
true => self.back_cache.get().clone(),
|
||||
false => {
|
||||
let output: Vec<(f64, f64)> = (1..=self.resolution)
|
||||
.map(|x| ((x as f64 / self.resolution as f64) * absrange) + self.min_x)
|
||||
.map(|x| (x, self.function.run(x)))
|
||||
.collect();
|
||||
self.back_cache.set(output.clone());
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_data: Vec<(f64, f64)> = data
|
||||
.iter()
|
||||
.map(|(x, y)| (*x, *y))
|
||||
.collect();
|
||||
|
||||
let (rect_data, area): (Vec<(f64, f64, f64)>, f64) = match self.front_cache.is_valid() {
|
||||
true => self.front_cache.get().clone(),
|
||||
false => {
|
||||
let step = absrange / (self.num_interval as f64);
|
||||
let output: (Vec<(f64, f64, f64)>, f64) = self.integral_rectangles(step);
|
||||
self.front_cache.set(output.clone());
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
(filtered_data, rect_data, area)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update(
|
||||
&mut self, func_str_new: String, min_x: f64, max_x: f64, num_interval: usize, resolution: usize,
|
||||
) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) {
|
||||
let func_str: String = add_asterisks(func_str_new);
|
||||
let update_func: bool = !self.function.str_compare(func_str.clone());
|
||||
|
||||
let underlying_update = update_func
|
||||
| (min_x != self.min_x)
|
||||
| (max_x != self.max_x);
|
||||
|
||||
if underlying_update | (self.resolution != resolution) {
|
||||
self.back_cache.invalidate();
|
||||
}
|
||||
|
||||
if underlying_update | (num_interval != self.num_interval) {
|
||||
self.front_cache.invalidate();
|
||||
}
|
||||
|
||||
if update_func {
|
||||
self.function = Function::from_string(func_str);
|
||||
}
|
||||
|
||||
self.min_x = min_x;
|
||||
self.max_x = max_x;
|
||||
self.num_interval = num_interval;
|
||||
self.resolution = resolution;
|
||||
|
||||
self.draw()
|
||||
}
|
||||
|
||||
// Creates and does the math for creating all the rectangles under the graph
|
||||
#[inline]
|
||||
fn integral_rectangles(&self, step: f64) -> (Vec<(f64, f64, f64)>, f64) {
|
||||
let data2: Vec<(f64, f64, f64)> = (0..self.num_interval)
|
||||
.map(|e| {
|
||||
let x: f64 = ((e as f64) * step) + self.min_x;
|
||||
|
||||
// Makes sure rectangles are properly handled on x values below 0
|
||||
let x2: f64 = match x > 0.0 {
|
||||
true => x + step,
|
||||
false => x - step,
|
||||
};
|
||||
|
||||
let tmp1: f64 = self.function.run(x);
|
||||
let tmp2: f64 = self.function.run(x2);
|
||||
|
||||
// Chooses the y value who's absolute value is the smallest
|
||||
let y: f64 = match tmp2.abs() > tmp1.abs() {
|
||||
true => tmp1,
|
||||
false => tmp2,
|
||||
};
|
||||
|
||||
(x, x2, y)
|
||||
})
|
||||
.filter(|(_, _, y)| !y.is_nan())
|
||||
.collect();
|
||||
let area: f64 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas
|
||||
(data2, area)
|
||||
}
|
||||
}
|
||||
94
src/egui_app.rs
Normal file
94
src/egui_app.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use eframe::{egui, epi};
|
||||
use egui::{plot::{HLine, Line, Plot, Value, Values, Text}, Pos2};
|
||||
use crate::chart_manager::ChartManager;
|
||||
use meval::Expr;
|
||||
use crate::misc::{add_asterisks, Cache, Function};
|
||||
use egui::{Color32, ColorImage, Ui};
|
||||
use emath::Rect;
|
||||
use epaint::{Rounding, RectShape, Stroke};
|
||||
use egui::widgets::plot::{Bar, BarChart};
|
||||
|
||||
pub struct MathApp {
|
||||
func_str: String,
|
||||
min_x: f64,
|
||||
max_x: f64,
|
||||
num_interval: usize,
|
||||
resolution: usize,
|
||||
chart_manager: ChartManager,
|
||||
}
|
||||
|
||||
impl Default for MathApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
func_str: "x^2".to_string(),
|
||||
min_x: -10.0,
|
||||
max_x: 10.0,
|
||||
num_interval: 100,
|
||||
resolution: 10000,
|
||||
chart_manager: ChartManager::new("x^2".to_string(), -10.0, 10.0, 100, 10000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl epi::App for MathApp {
|
||||
fn name(&self) -> &str {
|
||||
"eframe template"
|
||||
}
|
||||
|
||||
/// Called once before the first frame.
|
||||
fn setup(
|
||||
&mut self,
|
||||
_ctx: &egui::Context,
|
||||
_frame: &epi::Frame,
|
||||
_storage: Option<&dyn epi::Storage>,
|
||||
) { }
|
||||
|
||||
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
|
||||
let Self {
|
||||
func_str,
|
||||
min_x,
|
||||
max_x,
|
||||
num_interval,
|
||||
resolution,
|
||||
chart_manager
|
||||
} = self;
|
||||
|
||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||
ui.heading("Side Panel");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Function: ");
|
||||
ui.text_edit_singleline(func_str);
|
||||
});
|
||||
|
||||
ui.add(egui::Slider::new(min_x, -1000.0..=1000.0).text("Min X"));
|
||||
ui.add(egui::Slider::new(max_x, *min_x..=1000.0).text("Max X"));
|
||||
|
||||
ui.add(egui::Slider::new(num_interval, 0..=usize::MAX).text("Interval"));
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let (filtered_data, rect_data, area) = chart_manager.update(self.func_str.clone(), self.min_x, self.max_x, self.num_interval, self.resolution);
|
||||
|
||||
let filtered_data_values = filtered_data.iter().map(|(x, y)| Value::new(*x, *y)).collect();
|
||||
|
||||
let curve = Line::new(Values::from_values(filtered_data_values)).color(Color32::RED);
|
||||
let bars = rect_data.iter().map(|(_, x2, y)| Bar::new(*x2, *y)).collect();
|
||||
let barchart = BarChart::new(bars).color(Color32::BLUE);
|
||||
|
||||
// ui.label("Graph:");
|
||||
ui.label(format!("Area: {:.10}", area));
|
||||
Plot::new("plot")
|
||||
.view_aspect(1.0)
|
||||
.include_y(0)
|
||||
.show(ui, |plot_ui| {
|
||||
plot_ui.line(curve);
|
||||
plot_ui.bar_chart(barchart);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
267
src/lib.rs
267
src/lib.rs
@@ -1,18 +1,22 @@
|
||||
#![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen
|
||||
#![allow(clippy::type_complexity)] // Clippy, my types are fine.
|
||||
|
||||
use meval::Expr;
|
||||
use plotters::prelude::*;
|
||||
use plotters_canvas::CanvasBackend;
|
||||
use std::panic;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
mod misc;
|
||||
use crate::misc::{add_asterisks, Cache, ChartOutput, DrawResult, Function};
|
||||
mod egui_app;
|
||||
mod chart_manager;
|
||||
use std::panic;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use eframe::{epi, egui};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
// Use `js_namespace` here to bind `console.log(..)` instead of just
|
||||
@@ -21,8 +25,9 @@ extern "C" {
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn init() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
|
||||
log("Initializing...");
|
||||
|
||||
// See performance in browser profiler!
|
||||
@@ -36,244 +41,8 @@ pub fn init() {
|
||||
log("Initialized console_error_panic_hook!");
|
||||
|
||||
log("Finished initializing!");
|
||||
}
|
||||
|
||||
// Manages Chart generation and caching of values
|
||||
#[wasm_bindgen]
|
||||
pub struct ChartManager {
|
||||
function: Function,
|
||||
min_x: f32,
|
||||
max_x: f32,
|
||||
min_y: f32,
|
||||
max_y: f32,
|
||||
num_interval: usize,
|
||||
resolution: usize,
|
||||
back_cache: Cache<Vec<(f32, f32)>>,
|
||||
front_cache: Cache<(Vec<(f32, f32, f32)>, f32)>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ChartManager {
|
||||
pub fn new(
|
||||
func_str: String, min_x: f32, max_x: f32, min_y: f32, max_y: f32, num_interval: usize,
|
||||
resolution: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
function: Function::from_string(func_str),
|
||||
min_x,
|
||||
max_x,
|
||||
min_y,
|
||||
max_y,
|
||||
num_interval,
|
||||
resolution,
|
||||
back_cache: Cache::new_empty(),
|
||||
front_cache: Cache::new_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
|
||||
pub fn test_func(function_string: String) -> String {
|
||||
// Factorials do not work, and it would be really difficult to make them work
|
||||
if function_string.contains('!') {
|
||||
return "Factorials are unsupported".to_string();
|
||||
}
|
||||
|
||||
let new_func_str: String = add_asterisks(function_string);
|
||||
let expr_result = new_func_str.parse();
|
||||
let expr_error = match &expr_result {
|
||||
Ok(_) => "".to_string(),
|
||||
Err(error) => format!("{}", error),
|
||||
};
|
||||
if !expr_error.is_empty() {
|
||||
return expr_error;
|
||||
}
|
||||
|
||||
let expr: Expr = expr_result.unwrap();
|
||||
let func_result = expr.bind("x");
|
||||
let func_error = match &func_result {
|
||||
Ok(_) => "".to_string(),
|
||||
Err(error) => format!("{}", error),
|
||||
};
|
||||
if !func_error.is_empty() {
|
||||
return func_error;
|
||||
}
|
||||
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw(
|
||||
&mut self, element: HtmlCanvasElement, dark_mode: bool,
|
||||
) -> DrawResult<(impl Fn((i32, i32)) -> Option<(f32, f32)>, f32)> {
|
||||
log("Drawing...");
|
||||
let backend = CanvasBackend::with_canvas_object(element).unwrap();
|
||||
let root = backend.into_drawing_area();
|
||||
let font: FontDesc = ("sans-serif", 20.0).into();
|
||||
|
||||
if dark_mode {
|
||||
root.fill(&RGBColor(28, 28, 28))?;
|
||||
} else {
|
||||
root.fill(&WHITE)?;
|
||||
}
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.margin(20.0)
|
||||
.caption(format!("y={}", self.function.get_string()), font)
|
||||
.x_label_area_size(30.0)
|
||||
.y_label_area_size(30.0)
|
||||
.build_cartesian_2d(self.min_x..self.max_x, self.min_y..self.max_y)?;
|
||||
|
||||
if dark_mode {
|
||||
chart
|
||||
.configure_mesh()
|
||||
.x_labels(3)
|
||||
.y_labels(3)
|
||||
.light_line_style(&RGBColor(254, 254, 254))
|
||||
.draw()?;
|
||||
} else {
|
||||
chart.configure_mesh().x_labels(3).y_labels(3).draw()?;
|
||||
}
|
||||
|
||||
let absrange = (self.max_x - self.min_x).abs();
|
||||
let data: Vec<(f32, f32)> = match self.back_cache.is_valid() {
|
||||
true => self.back_cache.get().clone(),
|
||||
false => {
|
||||
log("Updating back_cache");
|
||||
let output: Vec<(f32, f32)> = (1..=self.resolution)
|
||||
.map(|x| ((x as f32 / self.resolution as f32) * absrange) + self.min_x)
|
||||
.map(|x| (x, self.function.run(x)))
|
||||
.collect();
|
||||
self.back_cache.set(output.clone());
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_data: Vec<(f32, f32)> = data
|
||||
.iter()
|
||||
.filter(|(_, y)| &self.min_y <= y && y <= &self.max_y)
|
||||
.map(|(x, y)| (*x, *y))
|
||||
.collect();
|
||||
chart.draw_series(LineSeries::new(filtered_data, &RED))?;
|
||||
|
||||
let (rect_data, area): (Vec<(f32, f32, f32)>, f32) = match self.front_cache.is_valid() {
|
||||
true => self.front_cache.get().clone(),
|
||||
false => {
|
||||
log("Updating front_cache");
|
||||
let step = absrange / (self.num_interval as f32);
|
||||
let output: (Vec<(f32, f32, f32)>, f32) = self.integral_rectangles(step);
|
||||
self.front_cache.set(output.clone());
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
if self.num_interval <= 200 {
|
||||
// Draw rectangles
|
||||
chart.draw_series(
|
||||
rect_data
|
||||
.iter()
|
||||
.map(|(x1, x2, y)| Rectangle::new([(*x2, *y), (*x1, 0.0)], &BLUE)),
|
||||
)?;
|
||||
} else {
|
||||
// Save resources by not graphing rectangles and using an AreaSeries when you can no longer see the rectangles
|
||||
let capped_data: Vec<(f32, f32)> = data
|
||||
.iter()
|
||||
.map(|(x, y)| {
|
||||
if y.is_nan() {
|
||||
return (*x, 0.0);
|
||||
}
|
||||
|
||||
let new_y: &f32 = if y > &self.max_y {
|
||||
&self.max_y
|
||||
} else if &self.min_y > y {
|
||||
&self.min_y
|
||||
} else {
|
||||
y
|
||||
};
|
||||
|
||||
(*x, *new_y)
|
||||
})
|
||||
.collect();
|
||||
chart.draw_series(AreaSeries::new(capped_data, 0.0, &BLUE))?;
|
||||
}
|
||||
|
||||
root.present()?;
|
||||
log("Finished Drawing!");
|
||||
Ok((chart.into_coord_trans(), area))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update(
|
||||
&mut self, canvas: HtmlCanvasElement, func_str_new: String, min_x: f32, max_x: f32,
|
||||
min_y: f32, max_y: f32, num_interval: usize, resolution: usize, dark_mode: bool,
|
||||
) -> Result<ChartOutput, JsValue> {
|
||||
let func_str: String = add_asterisks(func_str_new);
|
||||
let update_func: bool = !self.function.str_compare(func_str.clone());
|
||||
|
||||
let underlying_update = update_func
|
||||
| (min_x != self.min_x)
|
||||
| (max_x != self.max_x)
|
||||
| (min_y != self.min_y)
|
||||
| (max_y != self.max_y);
|
||||
|
||||
if underlying_update | (self.resolution != resolution) {
|
||||
self.back_cache.invalidate();
|
||||
}
|
||||
|
||||
if underlying_update | (num_interval != self.num_interval) {
|
||||
self.front_cache.invalidate();
|
||||
}
|
||||
|
||||
if update_func {
|
||||
self.function = Function::from_string(func_str);
|
||||
}
|
||||
|
||||
self.min_x = min_x;
|
||||
self.max_x = max_x;
|
||||
self.min_y = min_y;
|
||||
self.max_y = max_y;
|
||||
self.num_interval = num_interval;
|
||||
self.resolution = resolution;
|
||||
|
||||
let draw_output = self
|
||||
.draw(canvas, dark_mode)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let map_coord = draw_output.0;
|
||||
|
||||
let chart_output = ChartOutput {
|
||||
convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x, y))),
|
||||
area: draw_output.1,
|
||||
};
|
||||
|
||||
Ok(chart_output)
|
||||
}
|
||||
|
||||
// Creates and does the math for creating all the rectangles under the graph
|
||||
#[inline]
|
||||
fn integral_rectangles(&self, step: f32) -> (Vec<(f32, f32, f32)>, f32) {
|
||||
let data2: Vec<(f32, f32, f32)> = (0..self.num_interval)
|
||||
.map(|e| {
|
||||
let x: f32 = ((e as f32) * step) + self.min_x;
|
||||
|
||||
// Makes sure rectangles are properly handled on x values below 0
|
||||
let x2: f32 = match x > 0.0 {
|
||||
true => x + step,
|
||||
false => x - step,
|
||||
};
|
||||
|
||||
let tmp1: f32 = self.function.run(x);
|
||||
let tmp2: f32 = self.function.run(x2);
|
||||
|
||||
// Chooses the y value who's absolute value is the smallest
|
||||
let y: f32 = match tmp2.abs() > tmp1.abs() {
|
||||
true => tmp1,
|
||||
false => tmp2,
|
||||
};
|
||||
|
||||
(x, x2, y)
|
||||
})
|
||||
.filter(|(_, _, y)| !y.is_nan())
|
||||
.collect();
|
||||
let area: f32 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas
|
||||
(data2, area)
|
||||
}
|
||||
|
||||
log("Starting App...");
|
||||
let app = egui_app::MathApp::default();
|
||||
eframe::start_web(canvas_id, Box::new(app))
|
||||
}
|
||||
|
||||
16
src/main.rs
Normal file
16
src/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use eframe::{epi, egui};
|
||||
mod egui_app;
|
||||
mod misc;
|
||||
mod chart_manager;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
let app = egui_app::MathApp::default();
|
||||
let options = eframe::NativeOptions {
|
||||
// Let's show off that we support transparent windows
|
||||
transparent: true,
|
||||
drag_and_drop_support: true,
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(Box::new(app), options);
|
||||
}
|
||||
63
src/misc.rs
63
src/misc.rs
@@ -1,7 +1,4 @@
|
||||
use meval::Expr;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub type DrawResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
/*
|
||||
EXTREMELY Janky function that tries to put asterisks in the proper places to be parsed. This is so cursed. But it works, and I hopefully won't ever have to touch it again.
|
||||
@@ -80,6 +77,36 @@ pub fn add_asterisks(function_in: String) -> String {
|
||||
output_string.replace('π', "pi") // π -> pi
|
||||
}
|
||||
|
||||
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
|
||||
pub fn test_func(function_string: String) -> String {
|
||||
// Factorials do not work, and it would be really difficult to make them work
|
||||
if function_string.contains('!') {
|
||||
return "Factorials are unsupported".to_string();
|
||||
}
|
||||
|
||||
let new_func_str: String = add_asterisks(function_string);
|
||||
let expr_result = new_func_str.parse();
|
||||
let expr_error = match &expr_result {
|
||||
Ok(_) => "".to_string(),
|
||||
Err(error) => format!("{}", error),
|
||||
};
|
||||
if !expr_error.is_empty() {
|
||||
return expr_error;
|
||||
}
|
||||
|
||||
let expr: Expr = expr_result.unwrap();
|
||||
let func_result = expr.bind("x");
|
||||
let func_error = match &func_result {
|
||||
Ok(_) => "".to_string(),
|
||||
Err(error) => format!("{}", error),
|
||||
};
|
||||
if !func_error.is_empty() {
|
||||
return func_error;
|
||||
}
|
||||
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub struct Function {
|
||||
function: Box<dyn Fn(f64) -> f64>,
|
||||
func_str: String,
|
||||
@@ -96,41 +123,13 @@ impl Function {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn run(&self, x: f32) -> f32 { (self.function)(x as f64) as f32 }
|
||||
pub fn run(&self, x: f64) -> f64 { (self.function)(x) }
|
||||
|
||||
pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string }
|
||||
|
||||
pub fn get_string(&self) -> String { self.func_str.clone() }
|
||||
}
|
||||
|
||||
/// Result of screen to chart coordinates conversion.
|
||||
#[wasm_bindgen]
|
||||
pub struct Point {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Point {
|
||||
#[inline]
|
||||
pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ChartOutput {
|
||||
pub(crate) convert: Box<dyn Fn((i32, i32)) -> Option<(f32, f32)>>,
|
||||
pub(crate) area: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ChartOutput {
|
||||
pub fn get_area(&self) -> f32 { self.area }
|
||||
|
||||
pub fn coord(&self, x: i32, y: i32) -> Option<Point> {
|
||||
(self.convert)((x, y)).map(|(x, y)| Point::new(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cache<T> {
|
||||
backing_data: Option<T>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user