// TODO: split file use std::{ any::Any, cell::RefCell, sync::mpsc::{sync_channel, Receiver, SyncSender}, thread, }; use diaphragm_cairo_renderer::CairoRenderer; use diaphragm_core::{ colors::Color, core_shapes::{ CoreDrawable, CoreShape, Rectangle as CoreRectangle, StraightPath as CoreStraightPath, }, solving::VariableHandle, styles::Pattern, text::{FontDescription as CoreFontDescription, FontStyle, FontWeight, Text as CoreText}, types::{ Bool as CoreBool, Bounds, CoreShapeContext, Float as CoreFloat, Point2D as CorePoint2D, }, Runtime, }; use diaphragm_z3_solver::{z3, Z3Context}; use mlua::prelude::*; #[derive(Clone, Copy, Debug)] struct Float(CoreFloat); impl Float { fn new() -> Float { Float(runtime_thread_do(Box::new(|r| { r.solver_ctx().new_free_float() }))) } } impl LuaUserData for Float { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { macro_rules! float_method { ($name: expr, $op: ident) => { methods.add_method($name, |_lua, lhs: &Float, rhs: LuaValue| { let lhs = *lhs; let rhs = Float::try_from(rhs)?; Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().$op(lhs.0, rhs.0) })))) }); }; } float_method!("eq", float_eq); float_method!("ne", float_ne); float_method!("gt", float_gt); float_method!("ge", float_ge); float_method!("lt", float_lt); float_method!("le", float_le); methods.add_method("abs", |_lua, f: &Float, _: ()| { let f = *f; Ok(Float(runtime_thread_do(Box::new(move |r| { r.solver_ctx().float_abs(f.0) })))) }); } } impl TryFrom> for Float { type Error = LuaError; fn try_from(value: LuaValue) -> Result { match value { LuaValue::Integer(i) => Ok(Float(CoreFloat::Fixed(i as _))), LuaValue::Number(f) => Ok(Float(CoreFloat::Fixed(f))), // Create a new float from the borrow, since it might already be borrowed, with for ex. // f:eq(f) LuaValue::UserData(u) => Ok(Float(u.borrow::()?.0)), _ => Err(LuaError::FromLuaConversionError { from: value.type_name(), to: "Float", message: Some("Only int, float, or float() values are allowed".to_string()), }), } } } fn float(_: &Lua, _: ()) -> LuaResult { Ok(Float::new()) } fn float_max(_: &Lua, elems: Vec) -> LuaResult { let elems = elems.into_iter().map(|f| f.0).collect::>(); Ok(Float(runtime_thread_do(Box::new(move |r| { r.solver_ctx().float_max(&elems) })))) } fn float_min(_: &Lua, elems: Vec) -> LuaResult { let elems = elems.into_iter().map(|f| f.0).collect::>(); Ok(Float(runtime_thread_do(Box::new(move |r| { r.solver_ctx().float_min(&elems) })))) } #[derive(Clone, Copy, Debug)] struct Bool(CoreBool); impl LuaUserData for Bool { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { macro_rules! bool_method { ($name: expr, $op: ident) => { methods.add_method($name, |_lua, lhs: &Bool, rhs: LuaValue| { let lhs = *lhs; let rhs = Bool::try_from(rhs)?; Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().$op(lhs.0, rhs.0) })))) }); }; } bool_method!("eq", bool_eq); bool_method!("implies", bool_implies); methods.add_method("and", |_lua, lhs: &Bool, rhs: LuaValue| { let lhs = *lhs; let rhs = Bool::try_from(rhs)?; Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().bool_and(&[lhs.0, rhs.0]) })))) }); methods.add_method("or", |_lua, lhs: &Bool, rhs: LuaValue| { let lhs = *lhs; let rhs = Bool::try_from(rhs)?; Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().bool_or(&[lhs.0, rhs.0]) })))) }); methods.add_method("no", |_lua, b: &Bool, _: ()| { let b = *b; Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().bool_not(b.0) })))) }); } } impl TryFrom> for Bool { type Error = LuaError; fn try_from(value: LuaValue) -> Result { match value { LuaValue::Boolean(b) => Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().new_fixed_bool(b) })))), // Create a new bool from the borrow, since it might already be borrowed, with for ex. // b:eq(b) LuaValue::UserData(u) => Ok(Bool(u.borrow::()?.0)), _ => Err(LuaError::FromLuaConversionError { from: "Value", to: "Bool", message: Some("Only bool, or bool() values are allowed".to_string()), }), } } } #[derive(Debug, Clone)] struct Point2D(CorePoint2D); impl LuaUserData for Point2D { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { fields.add_field_method_get("x", |_, this| Ok(Float(this.0.x()))); fields.add_field_method_get("y", |_, this| Ok(Float(this.0.y()))); } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_method("eq", |_, this, other: LuaValue| { let x = this.0.x(); let y = this.0.y(); let other: Point2D = other.try_into()?; Ok(Bool(runtime_thread_do(Box::new(move |r| { let solver = r.solver_ctx(); let x_constraint = solver.float_eq(x, other.0.x()); let y_constraint = solver.float_eq(y, other.0.y()); solver.bool_and(&[x_constraint, y_constraint]) })))) }); methods.add_method("ne", |_, this, other: LuaValue| { let x = this.0.x(); let y = this.0.y(); let other: Point2D = other.try_into()?; Ok(Bool(runtime_thread_do(Box::new(move |r| { let solver = r.solver_ctx(); let x_constraint = solver.float_ne(x, other.0.x()); let y_constraint = solver.float_ne(y, other.0.y()); solver.bool_or(&[x_constraint, y_constraint]) })))) }); methods.add_method("man_dist", |_, this, other: LuaValue| { let x = this.0.x(); let y = this.0.y(); let other: Point2D = other.try_into()?; Ok(Float(runtime_thread_do(Box::new(move |r| { let solver = r.solver_ctx(); let signed_x_dist = solver.float_sub(&[x, other.0.x()]); let signed_y_dist = solver.float_sub(&[y, other.0.y()]); let x_dist = solver.float_abs(signed_x_dist); let y_dist = solver.float_abs(signed_y_dist); solver.float_add(&[x_dist, y_dist]) })))) }); } } impl TryFrom> for Point2D { type Error = LuaError; fn try_from(value: LuaValue<'_>) -> Result { match value { LuaValue::Table(table) => { // TODO: not really coherent with margin, which defaults to unconstrained float let x: Float = match table.get::<_, Option>("x")? { None => Float(CoreFloat::Fixed(0.)), Some(f) => f.try_into()?, }; let y: Float = match table.get::<_, Option>("y")? { None => Float(CoreFloat::Fixed(0.)), Some(f) => f.try_into()?, }; Ok(Point2D(CorePoint2D::new(x.0, y.0))) } // Create a new float from the borrow, since it might already be borrowed, with for ex. // f:eq(f) LuaValue::UserData(u) => Ok(Point2D(u.borrow::()?.0.clone())), _ => Err(LuaError::FromLuaConversionError { from: value.type_name(), to: "Point2D", message: Some("Only { x, y } tables are allowed".to_string()), }), } } } #[derive(Clone, Debug)] struct FontDescription(CoreFontDescription); impl LuaUserData for FontDescription { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { // fields.add_field_method_get("size", |_, this| Ok(Float(this.0.size))) fields.add_field_method_get("size", |_, this| Ok(Float(dbg!(this.0.size)))) } } const DEFAULT_FONT_FAMILY: &str = "serif"; impl Default for FontDescription { fn default() -> Self { Self(CoreFontDescription { family: DEFAULT_FONT_FAMILY.to_string(), style: FontStyle::Normal, weight: FontWeight::Normal, size: Float::new().0, }) } } fn font(_: &Lua, params: LuaTable) -> LuaResult { // TODO: better validation of the table // What happens when I mistype a param? // TODO: better error handling let family = params .get::<_, Option<_>>("family")? .unwrap_or_else(|| DEFAULT_FONT_FAMILY.to_string()); let style = match params.get::<_, Option>("style")?.as_deref() { Some("normal") | None => FontStyle::Normal, Some(_) => return Err(LuaError::RuntimeError("Unknown style".to_string())), }; let weight = match params.get::<_, Option>("weight")?.as_deref() { Some("normal") | None => FontWeight::Normal, Some(_) => return Err(LuaError::RuntimeError("Unknown weight".to_string())), }; let size = match params.get::<_, Option>("size")? { Some(f) => Float::try_from(f)?, None => Float::new(), }; Ok(FontDescription(CoreFontDescription { family, style, weight, size: size.0, })) } fn new_shape_context() -> CoreShapeContext { runtime_thread_do(Box::new(|r| CoreShapeContext::new(r.solver_ctx()))) } #[derive(Debug, Clone)] struct Drawable where T: Into, { shape: T, context: CoreShapeContext, } trait HasContext { fn get_context(&self) -> &CoreShapeContext; fn get_context_mut(&mut self) -> &mut CoreShapeContext; } fn add_bound_fields<'lua, T, F>(fields: &mut F) where T: LuaUserData, T: HasContext, F: LuaUserDataFields<'lua, T>, { macro_rules! bound_field { ($name: expr, $method: ident, $type: tt) => { fields.add_field_method_get($name, |_, this| { let bounds = this.get_context().bounds().clone(); Ok($type(runtime_thread_do(Box::new(move |r| { bounds.$method(r.solver_ctx()) })))) }); fields.add_field_method_set($name, |_, this, other: LuaValue| { let bounds = this.get_context().bounds().clone(); let other: $type = other.try_into()?; runtime_thread_do(Box::new(move |r| { let solver = r.solver_ctx(); let lhs = bounds.$method(solver); bound_field!(@check, solver, lhs, other, $type); })); Ok(()) }); }; (@check, $solver: ident, $lhs: ident, $rhs: ident, Float) => { let constraint = $solver.float_eq($lhs, $rhs.0); $solver.constrain(constraint); }; (@check, $solver: ident, $lhs: ident, $rhs: ident, Point2D) => { let x_constraint = $solver.float_eq($lhs.x(), $rhs.0.x()); let y_constraint = $solver.float_eq($lhs.y(), $rhs.0.y()); let constraint = $solver.bool_and(&[x_constraint, y_constraint]); $solver.constrain(constraint); }; } bound_field!("top", top, Float); bound_field!("left", left, Float); bound_field!("width", width, Float); bound_field!("height", height, Float); bound_field!("right", right, Float); bound_field!("bottom", bottom, Float); bound_field!("vert_center", vert_center, Float); bound_field!("horiz_center", horiz_center, Float); // TODO: two names: middle or center? bound_field!("top_left", top_left, Point2D); bound_field!("top_right", top_right, Point2D); bound_field!("bottom_left", bottom_left, Point2D); bound_field!("bottom_right", bottom_right, Point2D); bound_field!("middle_left", middle_left, Point2D); bound_field!("middle_right", middle_right, Point2D); bound_field!("top_middle", top_middle, Point2D); bound_field!("bottom_middle", bottom_middle, Point2D); bound_field!("center", center, Point2D); } fn add_context_fields<'lua, T, F>(fields: &mut F) where T: LuaUserData, T: HasContext, F: LuaUserDataFields<'lua, T>, { // Everything in top-level namespace, else we need struct with interior mutability? fields.add_field_method_get("stroke_color", |lua, this| { Ok(match this.get_context().stroke().pattern() { diaphragm_core::styles::Pattern::Solid(color) => { LuaValue::String(lua.create_string(&color.to_hex())?) } diaphragm_core::styles::Pattern::None => LuaValue::Nil, _ => todo!(), }) }); fields.add_field_method_set("stroke_color", |_, this, value: Option| { let context = this.get_context_mut(); if let Some(color) = value { context .stroke_mut() .set_pattern(Pattern::Solid(Color::from_hex(&color))); } else { context.stroke_mut().set_pattern(Pattern::None); } Ok(()) }); fields.add_field_method_get("stroke_width", |_, this| { Ok(this.get_context().stroke().line_width().map(Float)) }); fields.add_field_method_set("stroke_width", |_, this, value: LuaValue| { let other: Float = match value { LuaNil => { this.get_context_mut().stroke_mut().set_line_width(None); return Ok(()); } other => other.try_into()?, }; let stroke_width = match this.get_context().stroke().line_width() { Some(f) => f, None => { this.get_context_mut() .stroke_mut() .set_line_width(Some(other.0)); return Ok(()); } }; runtime_thread_do(Box::new(move |r| { let solver = r.solver_ctx(); let constraint = solver.float_eq(stroke_width, other.0); solver.constrain(constraint); })); Ok(()) }); fields.add_field_method_get("fill_color", |lua, this| { Ok(match this.get_context().fill().pattern() { diaphragm_core::styles::Pattern::Solid(color) => { LuaValue::String(lua.create_string(&color.to_hex())?) } diaphragm_core::styles::Pattern::None => LuaValue::Nil, _ => todo!(), }) }); fields.add_field_method_set("fill_color", |_, this, value: Option| { let context = this.get_context_mut(); if let Some(color) = value { context .fill_mut() .set_pattern(Pattern::Solid(Color::from_hex(&color))); } else { context.fill_mut().set_pattern(Pattern::None); } Ok(()) }); } impl> From> for CoreDrawable { fn from(value: Drawable) -> Self { CoreDrawable::new(value.shape.into(), value.context) } } #[derive(Clone, Debug)] struct Text(Drawable); impl LuaUserData for Text { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_method("draw", |_, this, _params: ()| { let drawable = this.0.clone().into(); runtime_thread_do(Box::new(|r| { r.add_drawable(drawable); })); Ok(()) }) } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { add_bound_fields(fields); } } impl HasContext for Text { fn get_context(&self) -> &CoreShapeContext { &self.0.context } fn get_context_mut(&mut self) -> &mut CoreShapeContext { &mut self.0.context } } fn text(_: &Lua, params: LuaTable) -> LuaResult { let content: String = params.get("content")?; let font = params .get::<_, Option>("font")? .unwrap_or_default(); let context = new_shape_context(); let content_2 = content.clone(); let font_2 = font.0.clone(); let context_2 = context.clone(); // TODO: this shouldn't be here, this should be an innate property of Text. Move the // Drawable struct in core? runtime_thread_do(Box::new(move |r| { let (text_width, text_height) = r.renderer().text_extents(&content_2, &font_2); let solver = r.solver_ctx(); // let scale = solver.float_div(font.0.size, CoreFloat::Fixed(height)); let calculated_width = solver.float_mul(&[CoreFloat::Fixed(text_width), font.0.size]); let bounds_width = context_2.bounds().width(solver); let width_constraint = solver.float_eq(bounds_width, calculated_width); solver.constrain(width_constraint); let calculated_height = solver.float_mul(&[CoreFloat::Fixed(text_height), font.0.size]); let bounds_height = context_2.bounds().height(solver); let height_constraint = solver.float_eq(bounds_height, calculated_height); solver.constrain(height_constraint); })); Ok(Text(Drawable { shape: CoreText { content, font: font.0, }, context, })) } #[derive(Clone, Debug)] struct Rectangle(Drawable); impl LuaUserData for Rectangle { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_method("draw", |_, this, _params: ()| { let drawable = this.0.clone().into(); runtime_thread_do(Box::new(|r| { r.add_drawable(drawable); })); Ok(()) }) } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { add_bound_fields(fields); add_context_fields(fields); } } impl HasContext for Rectangle { fn get_context(&self) -> &CoreShapeContext { &self.0.context } fn get_context_mut(&mut self) -> &mut CoreShapeContext { &mut self.0.context } } fn rectangle(_: &Lua, _params: ()) -> LuaResult { Ok(Rectangle(Drawable { shape: CoreRectangle {}, context: new_shape_context(), })) } #[derive(Debug, Clone)] struct StraightPath(Drawable); impl LuaUserData for StraightPath { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_method("draw", |_, this, _params: ()| { let drawable = this.0.clone().into(); runtime_thread_do(Box::new(|r| { r.add_drawable(drawable); })); Ok(()) }) } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { add_bound_fields(fields); add_context_fields(fields); } } impl HasContext for StraightPath { fn get_context(&self) -> &CoreShapeContext { &self.0.context } fn get_context_mut(&mut self) -> &mut CoreShapeContext { &mut self.0.context } } impl TryFrom> for StraightPath { type Error = LuaError; fn try_from(value: LuaValue) -> Result { match value { LuaValue::Table(t) => { let points = t .sequence_values::() .map(|el| Ok(Point2D::try_from(el?)?.0)) .collect::>>()?; Ok(StraightPath(Drawable { shape: CoreStraightPath::new(points), context: new_shape_context(), })) } // Create a new float from the borrow, since it might already be borrowed, with for ex. // f:eq(f) LuaValue::UserData(u) => Ok(StraightPath(u.borrow::()?.0.clone())), _ => Err(LuaError::FromLuaConversionError { from: value.type_name(), to: "StraightPath", message: Some("Only a list of points are allowed".to_string()), }), } } } // TODO: add bounds constraints before drawing fn straight_path(_: &Lua, params: LuaTable) -> LuaResult { let points: LuaValue = params.get("points")?; points.try_into() } // It just has a context for bounds, the rest is handled manually in Lua #[derive(Debug, Clone)] struct ComplexShape(CoreShapeContext); impl LuaUserData for ComplexShape { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { add_bound_fields(fields); } } impl HasContext for ComplexShape { fn get_context(&self) -> &CoreShapeContext { &self.0 } fn get_context_mut(&mut self) -> &mut CoreShapeContext { &mut self.0 } } fn complex_shape(_: &Lua, _params: ()) -> LuaResult { Ok(ComplexShape(new_shape_context())) } thread_local! { static SENDER: RefCell>> = RefCell::new(None); static REPLY: RefCell>> = RefCell::new(None); static REPLIER: RefCell>> = RefCell::new(None); } enum Message { Do(Box), SetBounds(Bounds), } fn runtime_thread_do(fun: Box T + Send>) -> T { SENDER.with(|sender| { sender .borrow_mut() .as_mut() .expect("Not currently drawing") .send(Message::Do(Box::new(|r| { let ret = fun(r); REPLIER.with(|replier| { replier .borrow_mut() .as_mut() .unwrap() .send(Reply::Any(Box::new(ret))) .unwrap(); }); }))) .unwrap(); }); REPLY.with(|reply| { let Reply::Any(any) = reply .borrow_mut() .as_mut() .expect("Not currently drawing") .recv() .unwrap(); *any.downcast().unwrap() }) } enum Reply { Any(Box), } fn constrain(_: &Lua, bool: Bool) -> LuaResult<()> { runtime_thread_do(Box::new(move |r| r.solver_ctx().constrain(bool.0))); Ok(()) } fn draw(lua: &Lua, params: LuaTable) -> LuaResult<()> { // So.... The Z3 stuff isn't Send and contains lifetimes, so we can't store them in global // variables or convert them to Lua. Solution: handle everything in a specific thread, and // communicate through a channel. thread::scope(|s| -> LuaResult<()> { let (message_sender, message_receiver) = sync_channel(1); let (reply_sender, reply_receiver) = sync_channel(1); SENDER.with(|f| { *f.borrow_mut() = Some(message_sender); }); REPLY.with(|f| { *f.borrow_mut() = Some(reply_receiver); }); s.spawn(move || { REPLIER.with(|f| { *f.borrow_mut() = Some(reply_sender.clone()); }); let z3_cfg = z3::Config::new(); let z3_ctx = z3::Context::new(&z3_cfg); let ctx = Z3Context::new(&z3_ctx); let cairo_renderer = CairoRenderer::new(); // TODO: we shouldn't need the renderer until the end let mut runtime = Runtime::new(Box::new(ctx), Box::new(cairo_renderer)); let mut bounds = None; for message in message_receiver { match message { Message::Do(fun) => { fun(&mut runtime); } Message::SetBounds(new_bounds) => bounds = Some(new_bounds), } } runtime.render(bounds.unwrap()); }); let _output: LuaTable = params.get("output")?; let figure = complex_shape(lua, ())?; let figure_bounds = figure.0.bounds().clone(); // TODO: sending bounds before callin `draw` prevents doing things like // figure = dia.rectangle.new(...) SENDER.with(|sender| { sender .borrow_mut() .as_mut() .expect("Not currently drawing") .send(Message::SetBounds(figure_bounds)) .unwrap() }); // TODO: this doesn't stop the runtime thread? let () = params.call_function("draw", figure).unwrap(); SENDER.with(|s| { *s.borrow_mut() = None; }); Ok(()) }) } #[mlua::lua_module] fn libdiaphragm(lua: &Lua) -> LuaResult { // TODO: the solver as a mutable global solves so much problem (pun not intended) let exports = lua.create_table()?; exports.set("float", lua.create_function(float)?)?; exports.set("text", lua.create_function(text)?)?; exports.set("font", lua.create_function(font)?)?; exports.set("rectangle", lua.create_function(rectangle)?)?; exports.set("straight_path", lua.create_function(straight_path)?)?; exports.set("complex_shape", lua.create_function(complex_shape)?)?; exports.set("float_max", lua.create_function(float_max)?)?; exports.set("float_min", lua.create_function(float_min)?)?; exports.set("constrain", lua.create_function(constrain)?)?; exports.set("draw", lua.create_function(draw)?)?; // Setting up metatables // --- let float_metatable = lua .create_userdata(Float(CoreFloat::Fixed(0.)))? .get_metatable()?; macro_rules! float_metamethod { ($method: ident, $op: ident) => { float_metatable.set( LuaMetaMethod::$method, lua.create_function(|_lua, (lhs, rhs): (Float, LuaValue)| { let rhs = Float::try_from(rhs)?; Ok(Float(runtime_thread_do(Box::new(move |r| { r.solver_ctx().$op(&[lhs.0, rhs.0]) })))) })?, )?; }; } float_metamethod!(Add, float_add); float_metamethod!(Sub, float_sub); float_metamethod!(Mul, float_mul); float_metatable.set( LuaMetaMethod::Div, lua.create_function(|_lua, (lhs, rhs): (Float, LuaValue)| { let rhs = Float::try_from(rhs)?; Ok(Float(runtime_thread_do(Box::new(move |r| { r.solver_ctx().float_div(lhs.0, rhs.0) })))) })?, )?; float_metatable.set( LuaMetaMethod::Unm, lua.create_function(|_lua, f: Float| { Ok(Float(runtime_thread_do(Box::new(move |r| { r.solver_ctx().float_neg(f.0) })))) })?, )?; // Not the operators `==`, `!=`, `<`, `<=`, `>`, and `>=` because Lua converts result to bool let bool_metatable = lua .create_userdata(Bool(CoreBool::new(VariableHandle::new(0))))? .get_metatable()?; bool_metatable.set( LuaMetaMethod::Shl, lua.create_function(|_lua, (lhs, rhs): (Bool, LuaValue)| { let rhs = Bool::try_from(rhs)?; Ok(Bool(runtime_thread_do(Box::new(move |r| { r.solver_ctx().bool_implies(lhs.0, rhs.0) })))) })?, )?; let point_metatable = lua .create_userdata(Point2D(CorePoint2D::new( CoreFloat::Fixed(0.), CoreFloat::Fixed(0.), )))? .get_metatable()?; macro_rules! point_metamethod { ($method: ident, $op: ident) => { point_metatable.set( LuaMetaMethod::$method, lua.create_function(|_lua, (lhs, rhs): (Point2D, LuaValue)| { let rhs = Point2D::try_from(rhs)?; Ok(Point2D(runtime_thread_do(Box::new(move |r| { let solver = r.solver_ctx(); let new_x = solver.$op(&[lhs.0.x(), rhs.0.x()]); let new_y = solver.$op(&[lhs.0.y(), rhs.0.y()]); CorePoint2D::new(new_x, new_y) })))) })?, )?; }; } point_metamethod!(Add, float_add); point_metamethod!(Sub, float_sub); Ok(exports) }