Add OpenSCAD file support (.scad files) - #20
Merged
Conversation
Adds a lexer, parser, and evaluator for the OpenSCAD language, taken from OpenRSCAD (https://github.com/matthova/openrscad) at a084615. Upstream is a clean-room reimplementation: its grammar comes from public documentation and black-box observation of the OpenSCAD CLI, with no OpenSCAD source consulted. It is Apache-2.0 OR MIT, so it composes with LuaCAD's AGPL. Only the front half is taken. The evaluator lowers a .scad program to a CSG tree that is nearly variant-for-variant LuaCAD's own ScadNode, so upstream's geometry crate is dropped and Manifold keeps doing the meshing. Vendored rather than depended on: the upstream crates are not published to crates.io, so a git dependency would make luacad itself unpublishable and break `cargo install luacad`. This follows what luacad-manifold-sys and luacad-prime-core already do, including renaming the packages back to their upstream names at the dependency site so the sources stay diffable. Deviations from upstream, each marked in the source and in the crate READMEs: - No bundled fonts. Upstream ships the twelve Liberation faces OpenSCAD uses so glyphs match byte-for-byte; that is 4 MB of TTFs, and LuaCAD's own text() is system-font-only anyway. Both paths now resolve a family the same way. - Font resolution is fallible. Without a bundle to fall back on, a machine with no fonts is an ordinary outcome, so with_face() returns None and text() warns and emits no geometry instead of panicking the process. - png 0.18 instead of 0.17, to match the rest of the workspace. - The font-dependent tests asserted on Liberation's exact glyph metrics. They now register system fonts, assert on resolution behavior and glyph shape rather than metrics, and skip when nothing is installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
scad_import turns openrscad_ir::Node into ScadNode, so a .scad file joins the pipeline where a Lua script does and everything downstream works unchanged: Manifold meshing, all six mesh formats, .scad round-trip, the PNG renderer, the path tracer, and Studio's preview. Nearly every IR variant has a ScadNode counterpart. The cases that do not: - $fn/$fa/$fs. The IR keeps all three per curved primitive; ScadNode stores a resolved count, so the OpenSCAD fragment formula is applied here. For rotate_extrude that needs the profile's reach from the axis, which is measured by materializing the profile. - polygon(paths). ScadNode::Polygon gains an optional `paths` field, resolved even-odd. Without it every hole would fill in — including the counters in text(), which the evaluator lowers to contours rather than a text node. - Multi-child hull() becomes a hull of the union, which is the same set. - import() arrives as bytes, so it is read with mesh_import and emitted as a polyhedron. That also carries the mesh into formats with no way to reference an external file. - Provenance (editor-to-preview source spans) is dropped. Three constructs cannot be carried exactly and warn instead of silently differing: linear_extrude with a non-uniform scale (LuaCAD scales both axes together), resize(auto=), and an import in a format mesh_import cannot read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
run, info, convert, watch and render each read the input file themselves and hand it to the Lua engine. They now go through one load_model(), which picks the front end from the extension, so an OpenSCAD file works everywhere a LuaCAD script does — including `convert model.scad out.scad`, which round-trips through the ScadNode tree with modules inlined. echo() output goes to stdout and anything the adapter could not carry across exactly goes to stderr as a warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
The open dialog and drag-and-drop take .scad alongside .lua, and the build picks its front end from the open file's extension. Everything past that point is unchanged, because both languages produce ScadNode trees: the viewport, the CSG tree, and every export already read them. execute_lua_code is now execute_source, since it no longer only runs Lua. Two things stay language-specific. selene only knows Lua, so an OpenSCAD buffer is left unlinted instead of being reported as one long syntax error. And syntect ships no OpenSCAD definition, so the editor highlights .scad with the C rules — the closest fit for its comments, braces, numbers and strings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
Integration tests for what only shows up once real files and the export stack are involved: multi-file include/use resolution, import(), meshing, exporting to all six formats, and the .scad round-trip. One of them caught a real bug. mesh_import yields Manifold's winding, but ScadNode::Polyhedron faces wind the way OpenSCAD writes them and the materializer reverses them — so an imported mesh came in inside-out, with a negative volume. The adapter now reverses to cancel that out. Each vendored crate gets its own empty rustfmt.toml. rustfmt resolves config from the file upwards, so this shadows the workspace's 2-space/80-column style and keeps those sources formatted like upstream, and diffable against it. render_text() gains a type alias for its return, which clippy's type_complexity wanted once it became an Option. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
Publishing luacad now needs luacad-scad-syntax, -ir and -eval on crates.io first. luacad-prime-core was already missing from the list even though luacad depends on it through the default raytrace feature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
…ort-i5nv6p # Conflicts: # changelog.md
The lint job runs clippy with --deny warnings, and 1.98 added two lints that Studio trips. Both are behavior-preserving: - FrameInputGenerator::generate drained its event buffer into a fresh Vec. mem::take hands the buffer over and leaves an empty one behind, which is what draining did, minus the reallocation. - compute_face_normals walked chunks_exact(3). as_chunks::<3>() types each group as a fixed [_; 3], so its three indexes are checked at compile time rather than at runtime; a trailing partial triangle is still dropped. Neither file belongs to the OpenSCAD work in this branch — these fail the same way on main, and are fixed here so the PR's lint job can go green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LuaCAD can now open and evaluate OpenSCAD
.scadfiles directly, alongside its existing Lua support. This is implemented by vendoring the OpenRSCAD language front-end (lexer, parser, and evaluator) and adapting it to LuaCAD's geometry pipeline.Key Changes
Three new crates (
luacad-scad-syntax,luacad-scad-ir,luacad-scad-eval): Vendored from OpenRSCAD to provide complete OpenSCAD language supportluacad-scad-syntax: Lexer (logos-based) and recursive-descent parser producing a typed ASTluacad-scad-ir: CSG intermediate representation (tree of geometry operations)luacad-scad-eval: Tree-walk interpreter with optional bytecode VM fast-path, plus support fortext(),color(), and customizer schema extractionNew
scad_importmodule (crates/luacad/src/scad_import.rs): Adapter between OpenRSCAD's CSG tree and LuaCAD's geometry pipeline.scadfiles from disk withinclude/useresolution againstOPENSCADPATHCsgGeometryso downstream (meshing, export, rendering) works unchangedCLI and Studio integration:
load_scad_file()andload_scad()public APIs for programmatic useis_scad_file()helper to detect.scadextension.luaand.scadGeometry export updates:
ScadNode::Polygonnow carries optionalpathsfield for contour index lists (OpenSCAD'spolygon(points, paths))Implementation Details
fontdbfortext()rendering; system fonts are registered on demandrustfmt.tomlfiles in each crate preserve upstream formatting conventionshttps://claude.ai/code/session_01D3KhNt8JmPPjSZgFNdTW5T