From 7ba9c7a78248f0130734bed1cdff055bdfd4242e Mon Sep 17 00:00:00 2001 From: Henning Meyer Date: Sun, 15 Mar 2026 21:06:48 +0000 Subject: [PATCH] Replace hlua with piccolo in luascad for WASM compatibility - Swap hlua (Lua 5.2 C bindings) for piccolo 0.3 (pure-Rust Lua VM) - Consolidate lobject, lobject_vector, printbuffer, sandbox into luascad.rs - LObject stored as static piccolo UserData with shared methods metatable - Sandbox via Lua::core() (base/coroutine/math/string/table, no I/O) - print/build capture via Arc> instead of mpsc channels - All 31 integration tests pass unchanged Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 89 +++++-- luascad/Cargo.toml | 2 +- luascad/lib.rs | 12 +- luascad/lobject.rs | 318 ---------------------- luascad/lobject_vector.rs | 104 -------- luascad/luascad.rs | 541 +++++++++++++++++++++++++++++++++++--- luascad/printbuffer.rs | 43 --- luascad/sandbox.rs | 41 --- 8 files changed, 579 insertions(+), 571 deletions(-) delete mode 100644 luascad/lobject.rs delete mode 100644 luascad/lobject_vector.rs delete mode 100644 luascad/printbuffer.rs delete mode 100644 luascad/sandbox.rs diff --git a/Cargo.lock b/Cargo.lock index c639fb9..7bf9dc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,6 +823,30 @@ dependencies = [ "slab", ] +[[package]] +name = "gc-arena" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd70cf88a32937834aae9614ff2569b5d9467fa0c42c5d7762fd94a8de88266" +dependencies = [ + "allocator-api2", + "gc-arena-derive", + "hashbrown 0.14.5", + "sptr", +] + +[[package]] +name = "gc-arena-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c612a69f5557a11046b77a7408d2836fe77077f842171cd211c5ef504bd3cddd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "gdk" version = "0.18.2" @@ -1273,6 +1297,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1321,16 +1355,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hlua" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c03a4912d51870b3ad70456a312f0527debce842f6230096938564bcf13bea" -dependencies = [ - "libc", - "lua52-sys", -] - [[package]] name = "image" version = "0.25.10" @@ -1603,17 +1627,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lua52-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2905d3eea022e5a4d8723d95ba261cd366572c9a43e429dfc089088a8c0fe91" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "matrixmultiply" version = "0.3.10" @@ -2236,6 +2249,21 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "piccolo" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003bf52de285e1ff1adcbc6572588db3849988ea660a2d55af3a2ffbc81f597f" +dependencies = [ + "ahash", + "allocator-api2", + "anyhow", + "gc-arena", + "hashbrown 0.14.5", + "rand 0.8.5", + "thiserror 1.0.69", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -2916,6 +2944,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2961,6 +2995,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3191,9 +3236,9 @@ dependencies = [ name = "truescad_luascad" version = "0.6.0" dependencies = [ - "hlua", "implicit3d", "nalgebra", + "piccolo", ] [[package]] diff --git a/luascad/Cargo.toml b/luascad/Cargo.toml index b043329..2a569f1 100644 --- a/luascad/Cargo.toml +++ b/luascad/Cargo.toml @@ -9,6 +9,6 @@ name = "truescad_luascad" path = "lib.rs" [dependencies] -hlua = "0.4" +piccolo = "0.3" implicit3d = "0.16" nalgebra = "0.34" diff --git a/luascad/lib.rs b/luascad/lib.rs index 2d39d3b..251f150 100644 --- a/luascad/lib.rs +++ b/luascad/lib.rs @@ -1,14 +1,8 @@ -#[macro_use] -extern crate hlua; pub extern crate implicit3d; -pub mod lobject; -pub mod lobject_vector; -pub mod luascad; -pub mod printbuffer; -pub mod sandbox; +mod luascad; -pub use self::luascad::eval; +pub use luascad::{eval, EvalResult}; type Float = f64; -const EPSILON: f64 = std::f64::EPSILON; +const EPSILON: f64 = f64::EPSILON; diff --git a/luascad/lobject.rs b/luascad/lobject.rs deleted file mode 100644 index b5b9cb1..0000000 --- a/luascad/lobject.rs +++ /dev/null @@ -1,318 +0,0 @@ -use super::{Float, EPSILON}; -use hlua; -use implicit3d::{ - Bender, BoundingBox, Cone, Cylinder, Intersection, Mesh, NormalPlane, Object, PlaneNegX, - PlaneNegY, PlaneNegZ, PlaneX, PlaneY, PlaneZ, Sphere, Twister, -}; -use nalgebra as na; -use std::sync::mpsc; - -#[derive(Clone, Debug)] -pub struct LObject { - pub o: Option>>, -} - -pub const INFINITY: Float = 1e10; -pub const NEG_INFINITY: Float = -1e10; - -implement_lua_push!(LObject, |mut metatable| { - { - let mut index = metatable.empty_array("__index"); - - index.set( - "translate", - hlua::function4(|o: &mut LObject, x: Float, y: Float, z: Float| o.translate(x, y, z)), - ); - index.set( - "rotate", - hlua::function4(|o: &mut LObject, x: Float, y: Float, z: Float| o.rotate(x, y, z)), - ); - index.set( - "scale", - hlua::function4(|o: &mut LObject, x: Float, y: Float, z: Float| o.scale(x, y, z)), - ); - index.set("clone", hlua::function1(|o: &mut LObject| o.clone())); - } - metatable.set( - "__tostring", - hlua::function1(|o: &mut LObject| format!("{:#?}", o)), - ); -}); - -implement_lua_read!(LObject); - -impl LObject { - pub fn as_object(&self) -> Option>> { - self.o.clone() - } - fn add_aliases(lua: &mut hlua::Lua, env_name: &str) { - lua.execute::<()>(&format!( - r#" - function Box (x, y, z, smooth) - if type(x) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then - error("all arguments must be numbers") - end - s = 0 - if type(smooth) == "number" then - s = smooth - end - return __Box(x, y, z, s) - end - function Cylinder (arg) - if type(arg.l) ~= "number" then - error("l must be a valid number") - end - if type(arg.r) == "number" then - r1 = arg.r - r2 = arg.r - elseif type(arg.r1) == "number" and type(arg.r2) == "number" then - r1 = arg.r1 - r2 = arg.r2 - else - error("specify either r or r1 and r2") - end - s = 0 - if type(arg.s) == "number" then - s = arg.s - end - return __Cylinder(arg.l, r1, r2, s) - end - function Plane3Points (a,b,c) - if type(a) ~= "table" or type(b) ~= "table" or type(c) ~= "table" or - #a ~= 3 or #b ~= 3 or #c ~= 3 then - error("all three arguments must be tables of len 3") - end - for i=1,3 do - if type(a[i]) ~= "number" or type(b[i]) ~= "number" or type(c[i]) ~= "number" then - error("all table elements must be numbers") - end - end - return __Plane3Points(a[1], a[2], a[3], - b[1], b[2], b[3], - c[1], c[2], c[3]) - end - function PlaneHessian (n,p) - if type(n) ~= "table" or #n ~= 3 or - type(n[1]) ~= "number" or type(n[2]) ~= "number" or type(n[3]) ~= "number" then - error("first argument (normal) must be a table of 3 numbers") - end - if type(p) ~= "number" then - error("second argument must be a number (p in hessian form)") - end - return __PlaneHessian(n[1], n[2], n[3], p) - end - {env}.Box = Box; - {env}.Cylinder = Cylinder; - {env}.Plane3Points = Plane3Points; - {env}.PlaneHessian = PlaneHessian; - "#, - env = env_name - )) - .unwrap(); - } - pub fn export_factories(lua: &mut hlua::Lua, env_name: &str, console: mpsc::Sender) { - { - let mut env = lua.get::, _>(env_name).unwrap(); - - macro_rules! one_param_object { - ( $x:ident ) => { - env.set( - stringify!($x), - hlua::function1(move |d_lua: hlua::AnyLuaValue| { - let mut d = 0.; - if let hlua::AnyLuaValue::LuaNumber(v) = d_lua { - d = v; - } - LObject { - o: Some(Box::new($x::new(d))), - } - }), - ); - }; - } - - one_param_object!(PlaneX); - one_param_object!(PlaneY); - one_param_object!(PlaneZ); - one_param_object!(PlaneNegX); - one_param_object!(PlaneNegY); - one_param_object!(PlaneNegZ); - env.set( - "Sphere", - hlua::function1(|radius: Float| LObject { - o: Some(Box::new(Sphere::new(radius))), - }), - ); - env.set( - "iCylinder", - hlua::function1(|radius: Float| LObject { - o: Some(Box::new(Cylinder::new(radius))), - }), - ); - env.set( - "iCone", - hlua::function1(|slope: Float| LObject { - o: Some(Box::new(Cone::new(slope, 0.))), - }), - ); - env.set( - "Bend", - hlua::function2(|o: &LObject, width: Float| LObject { - o: if let Some(obj) = o.as_object() { - Some(Box::new(Bender::new(obj, width))) - } else { - None - }, - }), - ); - env.set( - "Twist", - hlua::function2(|o: &LObject, height: Float| LObject { - o: if let Some(obj) = o.as_object() { - Some(Box::new(Twister::new(obj, height))) - } else { - None - }, - }), - ); - env.set( - "Mesh", - hlua::function1(move |filename: String| LObject { - o: match Mesh::try_new(&filename) { - Ok(mesh) => { - console - .send( - "Warning: Mesh support is currently horribly inefficient!" - .to_string(), - ) - .unwrap(); - Some(Box::new(mesh)) - } - Err(e) => { - console - .send(format!("Could not read mesh: {:}", e)) - .unwrap(); - None - } - }, - }), - ); - } - lua.set( - "__Box", - hlua::function4(|x: Float, y: Float, z: Float, smooth: Float| LObject { - o: Some( - Intersection::from_vec( - vec![ - Box::new(PlaneX::new(x / 2.0)), - Box::new(PlaneY::new(y / 2.0)), - Box::new(PlaneZ::new(z / 2.0)), - Box::new(PlaneNegX::new(x / 2.0)), - Box::new(PlaneNegY::new(y / 2.0)), - Box::new(PlaneNegZ::new(z / 2.0)), - ], - smooth, - ) - .unwrap(), - ), - }), - ); - lua.set( - "__PlaneHessian", - hlua::function4(|nx: Float, ny: Float, nz: Float, p: Float| LObject { - o: Some(Box::new(NormalPlane::from_normal_and_p( - na::Vector3::new(nx, ny, nz), - p, - ))), - }), - ); - lua.set( - "__Plane3Points", - hlua::function9( - |ax: Float, - ay: Float, - az: Float, - bx: Float, - by: Float, - bz: Float, - cx: Float, - cy: Float, - cz: Float| { - LObject { - o: Some(Box::new(NormalPlane::from_3_points( - &na::Point3::new(ax, ay, az), - &na::Point3::new(bx, by, bz), - &na::Point3::new(cx, cy, cz), - ))), - } - }, - ), - ); - lua.set( - "__Cylinder", - hlua::function4( - |length: Float, radius1: Float, radius2: Float, smooth: Float| { - let mut conie; - if (radius1 - radius2).abs() < EPSILON { - conie = Box::new(Cylinder::new(radius1)) as Box>; - } else { - let slope = (radius2 - radius1).abs() / length; - let offset = if radius1 < radius2 { - -radius1 / slope - length * 0.5 - } else { - radius2 / slope + length * 0.5 - }; - conie = Box::new(Cone::new(slope, offset)); - let rmax = radius1.max(radius2); - let conie_box = BoundingBox::new( - &na::Point3::new(-rmax, -rmax, NEG_INFINITY), - &na::Point3::new(rmax, rmax, INFINITY), - ); - conie.set_bbox(&conie_box); - } - LObject { - o: Some( - Intersection::from_vec( - vec![ - conie, - Box::new(PlaneZ::new(length / 2.0)), - Box::new(PlaneNegZ::new(length / 2.0)), - ], - smooth, - ) - .unwrap(), - ), - } - }, - ), - ); - LObject::add_aliases(lua, env_name); - } - fn translate(&mut self, x: Float, y: Float, z: Float) -> LObject { - LObject { - o: if let Some(ref obj) = self.o { - Some(obj.clone().translate(&na::Vector3::new(x, y, z))) - } else { - None - }, - } - } - fn rotate(&mut self, x: Float, y: Float, z: Float) -> LObject { - LObject { - o: if let Some(ref obj) = self.o { - Some(obj.clone().rotate(&na::Vector3::new(x, y, z))) - } else { - None - }, - } - } - fn scale(&mut self, x: Float, y: Float, z: Float) -> LObject { - LObject { - o: if let Some(ref obj) = self.o { - Some(obj.clone().scale(&na::Vector3::new(x, y, z))) - } else { - None - }, - } - } -} diff --git a/luascad/lobject_vector.rs b/luascad/lobject_vector.rs deleted file mode 100644 index cad2382..0000000 --- a/luascad/lobject_vector.rs +++ /dev/null @@ -1,104 +0,0 @@ -use super::Float; -use hlua; -use implicit3d::{Intersection, Object, Union}; -use crate::lobject::LObject; - -pub struct LObjectVector { - pub v: Option>>>, -} - -implement_lua_push!(LObjectVector, |mut metatable| { - let mut index = metatable.empty_array("__index"); - index.set( - "push", - hlua::function2(|v: &mut LObjectVector, o: &mut LObject| { - v.push(o.as_object()); - }), - ); -}); - -implement_lua_read!(LObjectVector); - -impl LObjectVector { - pub fn new(o: Option>>) -> LObjectVector { - LObjectVector { - v: if let Some(o) = o { Some(vec![o]) } else { None }, - } - } - pub fn export_factories(lua: &mut hlua::Lua, env_name: &str) { - lua.set( - "__new_object_vector", - hlua::function1(|o: &LObject| LObjectVector::new(o.as_object())), - ); - lua.set( - "__new_union", - hlua::function2(|o: &LObjectVector, smooth: Float| LObject { - o: if let Some(ref v) = o.v { - Some(Union::from_vec(v.clone(), smooth).unwrap()) - } else { - None - }, - }), - ); - lua.set( - "__new_intersection", - hlua::function2(|o: &LObjectVector, smooth: Float| LObject { - o: if let Some(ref v) = o.v { - Some(Intersection::from_vec(v.clone(), smooth).unwrap()) - } else { - None - }, - }), - ); - lua.set( - "__new_difference", - hlua::function2(|o: &LObjectVector, smooth: Float| LObject { - o: if let Some(ref v) = o.v { - Some(Intersection::difference_from_vec(v.clone(), smooth).unwrap()) - } else { - None - }, - }), - ); - lua.execute::<()>(&format!( - " - function __array_to_ov(lobjects) - ov = __new_object_vector(lobjects[1]) - for i = 2, #lobjects do - ov:push(lobjects[i]) - end - return ov - end - - function Union(lobjects, smooth) - smooth = smooth or 0 - return __new_union(__array_to_ov(lobjects), smooth) - end - - function Intersection(lobjects, smooth) - smooth = smooth or 0 - return __new_intersection(__array_to_ov(lobjects), smooth) - end - - function Difference(lobjects, smooth) - smooth = smooth or 0 - return __new_difference(__array_to_ov(lobjects), smooth) - end - - {env}.Union = Union; - {env}.Intersection = Intersection; - {env}.Difference = Difference;", - env = env_name - )) - .unwrap(); - } - pub fn push(&mut self, o: Option>>) { - if let Some(o) = o { - if let Some(ref mut v) = self.v { - v.push(o); - } - } else { - self.v = None - } - } -} diff --git a/luascad/luascad.rs b/luascad/luascad.rs index 9324220..d52a577 100644 --- a/luascad/luascad.rs +++ b/luascad/luascad.rs @@ -1,41 +1,516 @@ -use super::Float; -use hlua; -use hlua::{Lua, LuaError}; -use crate::lobject::LObject; -use crate::lobject_vector::LObjectVector; -use crate::printbuffer; -use crate::sandbox; +use std::sync::{Arc, Mutex}; -pub const USER_FUNCTION_NAME: &str = "__luscad_user_function__"; -pub const SANDBOX_ENV_NAME: &str = "__luascad_sandbox_env__"; +use piccolo::{ + Callback, CallbackReturn, Closure, Context, Executor, IntoValue, Lua, MetaMethod, Table, + UserData, Value, +}; -pub type EvalResult = Result<(String, Option>>), LuaError>; +use implicit3d::{ + BoundingBox, Bender, Cone, Cylinder, Intersection, Mesh, NormalPlane, Object, PlaneNegX, + PlaneNegY, PlaneNegZ, PlaneX, PlaneY, PlaneZ, Sphere, Twister, Union, +}; +use nalgebra as na; +use crate::{Float, EPSILON}; + +pub type EvalResult = Result<(String, Option>>), piccolo::StaticError>; + +/// Lua-visible wrapper around an implicit3d object. +pub struct LObject(pub Option>>); + +impl LObject { + fn as_object(&self) -> Option>> { + self.0.as_ref().map(|o| o.clone_box()) + } +} + +// ── internal helpers ────────────────────────────────────────────────────────── + +/// Wrap an LObject as piccolo static UserData and attach the shared methods metatable. +fn wrap_object(ctx: Context<'_>, obj: LObject) -> Value<'_> { + let ud = UserData::new_static(&ctx, obj); + if let Value::Table(mt) = ctx.get_global("__lobj_mt") { + ud.set_metatable(&ctx, Some(mt)); + } + ud.into() +} + +/// Read an integer-keyed Lua array table and collect implicit3d objects out of it. +fn objects_from_table<'gc>( + ctx: Context<'gc>, + table: Table<'gc>, +) -> Result>>, piccolo::Error<'gc>> { + let mut objects = Vec::new(); + let len = table.length() as usize; + for i in 1..=len { + let val = table.get(ctx, i as i64); + match val { + Value::UserData(ud) => { + let obj = ud + .downcast_static::() + .map_err(|_| "expected LObject in list".into_value(ctx))?; + match &obj.0 { + Some(o) => objects.push(o.clone_box()), + None => { + return Err("nil object in list".into_value(ctx).into()); + } + } + } + _ => return Err("expected LObject in list".into_value(ctx).into()), + } + } + Ok(objects) +} + +// ── setup functions ─────────────────────────────────────────────────────────── + +/// Register a shared `__lobj_mt` metatable that provides :translate/:rotate/:scale/:clone. +fn setup_methods_metatable(ctx: Context<'_>) { + let methods = Table::new(&ctx); + + methods + .set( + ctx, + "translate", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let (x, y, z): (Float, Float, Float) = stack.consume(ctx)?; + let obj = ud.downcast_static::()?; + let new_obj = LObject(obj.0.as_ref().map(|o| o.clone_box().translate(&na::Vector3::new(x, y, z)))); + stack.replace(ctx, wrap_object(ctx, new_obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + methods + .set( + ctx, + "rotate", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let (x, y, z): (Float, Float, Float) = stack.consume(ctx)?; + let obj = ud.downcast_static::()?; + let new_obj = LObject(obj.0.as_ref().map(|o| o.clone_box().rotate(&na::Vector3::new(x, y, z)))); + stack.replace(ctx, wrap_object(ctx, new_obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + methods + .set( + ctx, + "scale", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let (x, y, z): (Float, Float, Float) = stack.consume(ctx)?; + let obj = ud.downcast_static::()?; + let new_obj = LObject(obj.0.as_ref().map(|o| o.clone_box().scale(&na::Vector3::new(x, y, z)))); + stack.replace(ctx, wrap_object(ctx, new_obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + methods + .set( + ctx, + "clone", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let obj = ud.downcast_static::()?; + let new_obj = LObject(obj.0.as_ref().map(|o| o.clone_box())); + stack.replace(ctx, wrap_object(ctx, new_obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + let metatable = Table::new(&ctx); + metatable.set(ctx, MetaMethod::Index, methods).unwrap(); + ctx.set_global("__lobj_mt", metatable).unwrap(); +} + +/// Register custom `print` that appends to `buffer` instead of writing to stdout. +fn setup_print(ctx: Context<'_>, buffer: Arc>) { + ctx.set_global( + "print", + Callback::from_fn(&ctx, move |_ctx, _, mut stack| { + let mut parts = Vec::new(); + for i in 0..stack.len() { + let s = match stack.get(i) { + Value::String(s) => { + std::str::from_utf8(s.as_bytes()).unwrap_or("?").to_string() + } + Value::Number(n) => n.to_string(), + Value::Integer(n) => n.to_string(), + Value::Boolean(b) => b.to_string(), + Value::Nil => "nil".to_string(), + other => other.type_name().to_string(), + }; + parts.push(s); + } + buffer.lock().unwrap().push_str(&(parts.join("\t") + "\n")); + stack.clear(); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); +} + +/// Register all geometry factory functions and boolean operations as Lua globals. +fn setup_factories(ctx: Context<'_>, console: Arc>) { + macro_rules! plane_factory { + ($name:literal, $T:ident) => { + ctx.set_global( + $name, + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let d: Float = stack.consume(ctx)?; + stack.replace(ctx, wrap_object(ctx, LObject(Some(Box::new($T::new(d)))))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + }; + } + + plane_factory!("PlaneX", PlaneX); + plane_factory!("PlaneY", PlaneY); + plane_factory!("PlaneZ", PlaneZ); + plane_factory!("PlaneNegX", PlaneNegX); + plane_factory!("PlaneNegY", PlaneNegY); + plane_factory!("PlaneNegZ", PlaneNegZ); + + ctx.set_global( + "Sphere", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let r: Float = stack.consume(ctx)?; + stack.replace(ctx, wrap_object(ctx, LObject(Some(Box::new(Sphere::new(r)))))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "iCylinder", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let r: Float = stack.consume(ctx)?; + stack.replace(ctx, wrap_object(ctx, LObject(Some(Box::new(Cylinder::new(r)))))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "iCone", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let slope: Float = stack.consume(ctx)?; + stack.replace( + ctx, + wrap_object(ctx, LObject(Some(Box::new(Cone::new(slope, 0.0))))), + ); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + // __Box(x, y, z, smooth) — called by the Lua Box() wrapper + ctx.set_global( + "__Box", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (x, y, z, smooth): (Float, Float, Float, Float) = stack.consume(ctx)?; + let obj = Intersection::from_vec( + vec![ + Box::new(PlaneX::new(x / 2.0)) as Box>, + Box::new(PlaneY::new(y / 2.0)), + Box::new(PlaneZ::new(z / 2.0)), + Box::new(PlaneNegX::new(x / 2.0)), + Box::new(PlaneNegY::new(y / 2.0)), + Box::new(PlaneNegZ::new(z / 2.0)), + ], + smooth, + ) + .unwrap(); + stack.replace(ctx, wrap_object(ctx, LObject(Some(obj)))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + // __Cylinder(length, r1, r2, smooth) — called by the Lua Cylinder() wrapper + ctx.set_global( + "__Cylinder", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (length, radius1, radius2, smooth): (Float, Float, Float, Float) = + stack.consume(ctx)?; + let conie: Box> = if (radius1 - radius2).abs() < EPSILON { + Box::new(Cylinder::new(radius1)) + } else { + let slope = (radius2 - radius1).abs() / length; + let offset = if radius1 < radius2 { + -radius1 / slope - length * 0.5 + } else { + radius2 / slope + length * 0.5 + }; + let mut c: Box> = Box::new(Cone::new(slope, offset)); + let rmax = radius1.max(radius2); + c.set_bbox(&BoundingBox::new( + &na::Point3::new(-rmax, -rmax, -1e10), + &na::Point3::new(rmax, rmax, 1e10), + )); + c + }; + let obj = Intersection::from_vec( + vec![conie, Box::new(PlaneZ::new(length / 2.0)), Box::new(PlaneNegZ::new(length / 2.0))], + smooth, + ) + .unwrap(); + stack.replace(ctx, wrap_object(ctx, LObject(Some(obj)))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + // __PlaneHessian(nx, ny, nz, p) — called by the Lua PlaneHessian() wrapper + ctx.set_global( + "__PlaneHessian", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (nx, ny, nz, p): (Float, Float, Float, Float) = stack.consume(ctx)?; + let plane = NormalPlane::from_normal_and_p(na::Vector3::new(nx, ny, nz), p); + stack.replace(ctx, wrap_object(ctx, LObject(Some(Box::new(plane))))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + // __Plane3Points(ax,ay,az, bx,by,bz, cx,cy,cz) — called by the Lua Plane3Points() wrapper + ctx.set_global( + "__Plane3Points", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (ax, ay, az, bx, by, bz, cx, cy, cz): ( + Float, Float, Float, + Float, Float, Float, + Float, Float, Float, + ) = stack.consume(ctx)?; + let plane = NormalPlane::from_3_points( + &na::Point3::new(ax, ay, az), + &na::Point3::new(bx, by, bz), + &na::Point3::new(cx, cy, cz), + ); + stack.replace(ctx, wrap_object(ctx, LObject(Some(Box::new(plane))))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "Bend", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let width: Float = stack.consume(ctx)?; + let obj = ud.downcast_static::()?; + let new_obj = LObject( + obj.0 + .as_ref() + .map(|o| Box::new(Bender::new(o.clone_box(), width)) as Box>), + ); + stack.replace(ctx, wrap_object(ctx, new_obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "Twist", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let height: Float = stack.consume(ctx)?; + let obj = ud.downcast_static::()?; + let new_obj = LObject( + obj.0 + .as_ref() + .map(|o| Box::new(Twister::new(o.clone_box(), height)) as Box>), + ); + stack.replace(ctx, wrap_object(ctx, new_obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "Mesh", + Callback::from_fn(&ctx, move |ctx, _, mut stack| { + let filename: piccolo::String = stack.consume(ctx)?; + let filename_str = std::str::from_utf8(filename.as_bytes()) + .unwrap_or("") + .to_string(); + let obj = match Mesh::try_new(&filename_str) { + Ok(mesh) => { + console + .lock() + .unwrap() + .push_str("Warning: Mesh support is currently horribly inefficient!\n"); + LObject(Some(Box::new(mesh))) + } + Err(e) => { + console + .lock() + .unwrap() + .push_str(&format!("Could not read mesh: {e}\n")); + LObject(None) + } + }; + stack.replace(ctx, wrap_object(ctx, obj)); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + // Union / Intersection / Difference take (table, smooth?) + ctx.set_global( + "Union", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (table, smooth): (Table, Option) = stack.consume(ctx)?; + let objects = objects_from_table(ctx, table)?; + let obj = Union::from_vec(objects, smooth.unwrap_or(0.0)) + .ok_or_else(|| "Union requires at least one object".into_value(ctx))?; + stack.replace(ctx, wrap_object(ctx, LObject(Some(obj)))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "Intersection", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (table, smooth): (Table, Option) = stack.consume(ctx)?; + let objects = objects_from_table(ctx, table)?; + let obj = Intersection::from_vec(objects, smooth.unwrap_or(0.0)) + .ok_or_else(|| "Intersection requires at least one object".into_value(ctx))?; + stack.replace(ctx, wrap_object(ctx, LObject(Some(obj)))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global( + "Difference", + Callback::from_fn(&ctx, |ctx, _, mut stack| { + let (table, smooth): (Table, Option) = stack.consume(ctx)?; + let objects = objects_from_table(ctx, table)?; + let obj = Intersection::difference_from_vec(objects, smooth.unwrap_or(0.0)) + .ok_or_else(|| "Difference requires at least one object".into_value(ctx))?; + stack.replace(ctx, wrap_object(ctx, LObject(Some(obj)))); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); +} + +/// Lua helper functions that wrap the low-level `__Box`, `__Cylinder`, etc. callbacks. +const LUA_ALIASES: &str = r#" +function Box(x, y, z, smooth) + if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then + error("all arguments must be numbers") + end + local s = 0 + if type(smooth) == "number" then s = smooth end + return __Box(x, y, z, s) +end + +function Cylinder(arg) + if type(arg.l) ~= "number" then error("l must be a valid number") end + local r1, r2 + if type(arg.r) == "number" then + r1, r2 = arg.r, arg.r + elseif type(arg.r1) == "number" and type(arg.r2) == "number" then + r1, r2 = arg.r1, arg.r2 + else + error("specify either r or r1 and r2") + end + local s = 0 + if type(arg.s) == "number" then s = arg.s end + return __Cylinder(arg.l, r1, r2, s) +end + +function Plane3Points(a, b, c) + if type(a) ~= "table" or type(b) ~= "table" or type(c) ~= "table" or + #a ~= 3 or #b ~= 3 or #c ~= 3 then + error("all three arguments must be tables of len 3") + end + for i = 1, 3 do + if type(a[i]) ~= "number" or type(b[i]) ~= "number" or type(c[i]) ~= "number" then + error("all table elements must be numbers") + end + end + return __Plane3Points(a[1],a[2],a[3], b[1],b[2],b[3], c[1],c[2],c[3]) +end + +function PlaneHessian(n, p) + if type(n) ~= "table" or #n ~= 3 or + type(n[1]) ~= "number" or type(n[2]) ~= "number" or type(n[3]) ~= "number" then + error("first argument (normal) must be a table of 3 numbers") + end + if type(p) ~= "number" then + error("second argument must be a number (p in hessian form)") + end + return __PlaneHessian(n[1], n[2], n[3], p) +end +"#; + +// ── public API ──────────────────────────────────────────────────────────────── + +/// Evaluate a Lua script in a sandboxed environment with all geometry primitives available. +/// +/// Returns `(print_output, Option)`. pub fn eval(script: &str) -> EvalResult { - let mut result = None; - let print_output; + // Lua::core() loads: base, coroutine, math, string, table — no I/O, no require, no load + let mut lua = Lua::core(); + + let print_buffer: Arc> = Arc::new(Mutex::new(String::new())); + let result: Arc>>>> = Arc::new(Mutex::new(None)); + { - let mut lua = Lua::new(); - lua.openlibs(); - sandbox::set_sandbox_env(&mut lua, SANDBOX_ENV_NAME); - let printbuffer = - printbuffer::PrintBuffer::new_and_expose_to_lua(&mut lua, SANDBOX_ENV_NAME); - { - let mut sandbox_env = lua.get::, _>(SANDBOX_ENV_NAME).unwrap(); - sandbox_env.set( + let print_buffer = print_buffer.clone(); + let result = result.clone(); + + lua.try_enter(|ctx| { + setup_methods_metatable(ctx); + setup_print(ctx, print_buffer.clone()); + setup_factories(ctx, print_buffer); + + let result = result.clone(); + ctx.set_global( "build", - hlua::function1(|o: &LObject| result = o.as_object()), - ); - } - LObject::export_factories(&mut lua, SANDBOX_ENV_NAME, printbuffer.get_tx()); - LObjectVector::export_factories(&mut lua, SANDBOX_ENV_NAME); - - lua.checked_set(USER_FUNCTION_NAME, hlua::LuaCode(script))?; - lua.execute::<()>(&format!( - "debug.setupvalue({}, 1, {}); return {}();", - USER_FUNCTION_NAME, SANDBOX_ENV_NAME, USER_FUNCTION_NAME - ))?; - print_output = printbuffer.get_buffer(); + Callback::from_fn(&ctx, move |ctx, _, mut stack| { + let ud: UserData = stack.from_front(ctx)?; + let obj = ud.downcast_static::()?; + *result.lock().unwrap() = obj.as_object(); + stack.clear(); + Ok(CallbackReturn::Return) + }), + )?; + + Ok(()) + })?; } - Ok((print_output, result)) + + // Load Lua alias helpers + let aliases_exec = lua.try_enter(|ctx| { + let closure = Closure::load(ctx, None, LUA_ALIASES.as_bytes())?; + Ok(ctx.stash(Executor::start(ctx, closure.into(), ()))) + })?; + lua.execute::<()>(&aliases_exec)?; + + // Run the user script + let user_exec = lua.try_enter(|ctx| { + let closure = Closure::load(ctx, Some("script"), script.as_bytes())?; + Ok(ctx.stash(Executor::start(ctx, closure.into(), ()))) + })?; + lua.execute::<()>(&user_exec)?; + + let output = print_buffer.lock().unwrap().clone(); + let obj = result.lock().unwrap().take(); + Ok((output, obj)) } diff --git a/luascad/printbuffer.rs b/luascad/printbuffer.rs deleted file mode 100644 index 442948f..0000000 --- a/luascad/printbuffer.rs +++ /dev/null @@ -1,43 +0,0 @@ -use hlua; -use std::sync::mpsc; - -pub struct PrintBuffer { - rx: mpsc::Receiver, - tx: mpsc::Sender, -} - -impl PrintBuffer { - pub fn new_and_expose_to_lua(lua: &mut hlua::Lua, env_name: &str) -> PrintBuffer { - let (tx, rx): (mpsc::Sender, mpsc::Receiver) = mpsc::channel(); - let lua_tx = tx.clone(); - lua.set( - "__print", - hlua::function1(move |s: String| { - lua_tx.send(s).unwrap(); - }), - ); - lua.execute::<()>(&format!( - " - function print (...) - for i,v in ipairs{{...}} do - __print(tostring(v) .. \"\\t\") - end - __print(\"\\n\") - end - {env}.print = print;", - env = env_name - )) - .unwrap(); - PrintBuffer { rx, tx } - } - pub fn get_tx(&self) -> mpsc::Sender { - self.tx.clone() - } - pub fn get_buffer(&self) -> String { - let mut result = String::new(); - for s in self.rx.try_iter() { - result += &s; - } - result - } -} diff --git a/luascad/sandbox.rs b/luascad/sandbox.rs deleted file mode 100644 index d44bfb8..0000000 --- a/luascad/sandbox.rs +++ /dev/null @@ -1,41 +0,0 @@ -use hlua; - -pub fn set_sandbox_env(lua: &mut hlua::Lua, env_var_name: &str) { - lua.execute::<()>(&format!( - "{env} = {val};", - env = env_var_name, - val = SANDBOX_ENV - )) - .unwrap(); -} - -const SANDBOX_ENV: &str = "{ - ipairs = ipairs, - next = next, - pairs = pairs, - pcall = pcall, - print = print, - tonumber = tonumber, - tostring = tostring, - type = type, - unpack = unpack, - coroutine = { create = coroutine.create, resume = coroutine.resume, - running = coroutine.running, status = coroutine.status, - wrap = coroutine.wrap }, - string = { byte = string.byte, char = string.char, find = string.find, - format = string.format, gmatch = string.gmatch, gsub = string.gsub, - len = string.len, lower = string.lower, match = string.match, - rep = string.rep, reverse = string.reverse, sub = string.sub, - upper = string.upper }, - table = { insert = table.insert, maxn = table.maxn, remove = table.remove, - sort = table.sort }, - math = { abs = math.abs, acos = math.acos, asin = math.asin, - atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos, - cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor, - fmod = math.fmod, frexp = math.frexp, huge = math.huge, - ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max, - min = math.min, modf = math.modf, pi = math.pi, pow = math.pow, - rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh, - sqrt = math.sqrt, tan = math.tan, tanh = math.tanh }, - os = { clock = os.clock, difftime = os.difftime, time = os.time }, -}";