Skip to content

Repository files navigation

Simple 3D

Parametric 3D modelling with exact metric dimensions. Assemble models out of primitives (boxes rounded or chamfered, prisms, spheres, cylinders, cones, pyramids, tori, slots, regular polyhedra), combine them with booleans, and export to 3MF, STL, OBJ or PLY for a slicer.

Nothing has to be entered as a scale factor: a 40 mm box is 40 mm because its width parameter says 40, and dragging its right face rewrites that parameter rather than stretching anything. Every dimension typed in is reproduced exactly in the exported mesh. A scale tool exists alongside that, for the question dimensions cannot answer — making a whole group a proportion of what it was — and it is a factor the node carries, never something a resize writes.

The camera is orthographic, always. Parallel lines stay parallel, so the origin axes lie along the grid lines they belong to and two edges of the same length measure the same on screen wherever they sit in the frame — which is the point of a viewport whose job is to show dimensions rather than a photograph.

One self-contained binary. No runtime, no network, no accelerated graphics required, and nothing to install — the portable executable writes nothing outside its own directory. Packages that do install it, for people who would rather their system tracked the file, are below.

Download

Portable single-file executables for Linux (x86-64) and Windows (x64) are built from the tag by .github/workflows/release.yml and attached to each release:

Platform File
Linux x86-64 simple-3d-linux-x86_64 (chmod +x and run)
Linux x86-64, Debian/Ubuntu simple-3d_<version>_amd64.deb (sudo apt install ./simple-3d_<version>_amd64.deb)
Windows x64 simple-3d-windows-x86_64.exe
Windows x64, installer simple-3d-windows-x86_64.msi

The .deb is the same executable, packaged: it adds only the desktop entry, icon and MIME type that put Simple 3D in the applications menu and let a file manager open .simple3d files with it. It is built by packaging/deb/build.sh from the binary the same job produced.

The .msi is the Windows counterpart: the same executable, installed wherever the installer's directory page is pointed at (Program Files\Simple 3D by default), with three tick boxes for what to set up alongside it — a Start menu entry, a desktop shortcut, and the .simple3d file association. Each is on by default and each can be turned off, before the install or on the command line for a silent one. It is built from packaging/windows/simple-3d.wxs by the same job that uploads the .exe. Neither package replaces the portable executable, which still installs nothing.

Put a file named portable (or portable.txt) beside the executable and it keeps its settings there instead of in the user profile — otherwise they live in %APPDATA%\Simple3D or $XDG_CONFIG_HOME/simple3d.

Projects are .simple3d files (JSON, versioned, human-readable). Passing one as an argument opens it, so file associations work on both platforms. Both are the packages' job: the .deb registers the type on Linux and the .msi does on Windows, icon included, and each takes its registration back out when it is removed. The portable executables register nothing, which is what makes them portable.

What it does

  • Typing is the primary interface. Every numeric field takes an expression (40/3, (2+3)*4), a value in another unit (4cm in a millimetre document), or a delta (+2, - 5) that resolves against each selected shape separately. A field is also its own slider: drag it to scrub the value, click it to type into it. Shift is fine, Ctrl is coarse.
  • Direct manipulation writes parameters. Move, rotate and resize handles rewrite the shape's own dimensions and position — a completed drag is one undo step, and Escape during one puts everything back. The fourth tool, scale, writes a factor instead, and is the one that works on a group.
  • Paint what you are working on. An object or a whole group takes a colour — the full picker, eight presets, or one already used in this session — and it follows each surface through a boolean: after a painted cutter drills a painted plate, the wall of the hole is the cutter's colour. 3MF export carries the colours; the other formats have nowhere to put them.
  • Hidden, or a ghost, or neither. A node is visible, hidden (out of the model and not drawn), or a ghost: out of the model but drawn as a translucent shell, which is what a body about to be subtracted needs while it is being positioned. It is a property of the node, so one shape can be a ghost while another is simply gone.
  • A palette you can add to. Save a group, or a whole project, as a primitive and it is on the palette of every project afterwards. New shapes land where you choose — the origin, the 3D cursor, what the camera is looking at, or clear of the selection — set by Add at in the document options, with the palette saying which answer is in force.
  • Convert to a mesh, and break it apart. A shape, a boolean or a whole pattern can be baked into the triangles it evaluates to — the node keeps its name, its place, its transform and its colour, and loses only the parameters behind it. And whatever a node evaluates to can be broken into the separate pieces it is actually in, each a node of its own: cut a plate into four with a boolean, break it apart, and every piece is an object to move, paint and export. The pieces land in a split — a node kind of its own, made only by breaking something apart, standing where the shape stood and wearing its name and its transform — which keeps the shape itself, operands and parameters and all. So the break is reversible: join the pieces back together and the object returns as it was, where the pieces now stand, however long afterwards and across a save.
  • Booleans that hold up. Union, difference, intersection and hull, nested arbitrarily. Results are checked for manifoldness on every evaluation; a boolean that cannot be evaluated names its own node in the outliner while the rest of the scene still previews, and export refuses while the error stands.
  • An export that keeps its parts. A 3MF can be one solid, one component per top-level object, or bodies you group yourself: shapes given the same body are written as one solid, a group can be split open so its contents are considered one by one, and a difference stays whole because its operands are not shapes the result still holds. The grouping is saved with the project, so a re-export after a change only needs whatever is new to be placed. STL, OBJ and PLY hold one body, and say so.
  • Several models at once. A row of tabs holds every open document, each with its own model, selection, undo history and camera. New opens a tab rather than replacing what is open, a file already open is shown rather than opened twice, and Ctrl+Tab walks the row. Closing a document with unsaved changes asks first, and so does quitting while any tab has them.
  • A window you can rearrange. Panels move between the two docks by dragging their header and roll up by clicking it; Tab hides both docks and View ▸ Reset panel layout puts them back. The arrangement survives a restart. The orientation cube turns the camera to a face, and its centre dot returns to the isometric view.
  • Keys and mouse buttons are yours. Three presets (Simple 3D default, mesh editor, CAD) and per-command rebinding, with conflicts named rather than silently taken. A rebinding applies to the next gesture, without a restart.
  • It starts on anything. The viewport is a from-scratch software rasterizer, drawn across every core there is: no shader to fail to compile and no GPU to be missing. A GPU renderer is offered beside it, in the footer, for a large viewport -- it is a choice, never a requirement, and if a driver will not have it the viewport says so and goes on drawing in software.

Status

v0.0.29. All four crates are implemented and all 29 of the spec's acceptance criteria are behaviourally met. What each release changed is on its release page; the commit log is the record between them.

cargo test --workspace runs 726 tests, none failing, and every one of the 29 criteria is asserted by a test that cites it by name. Check that last claim rather than trusting it:

python3 tools/criteria_audit.py

It prints the test covering each criterion and exits non-zero if any is uncovered. Do not use grep -rn "criterion" crates/ for this: it counts a doc comment as coverage, and four criteria were once passing that check without a test.

A passing suite is not the same as a working application. Several of the bugs fixed so far — a drag that finished wherever the mouse button came up, an orientation cube a quarter turn out of step with the viewport, a side wall the orthographic camera culled although it faced the viewer, a perspective projection that fanned the grid apart from the axes lying along it — were all found by running the application with the full suite passing over it. Each of them is now covered by a test that fails without its fix.

Why Rust, and why no external CSG/geometry crate

  • Hard constraint: single self-contained native binary, no runtime/interpreter on the user's machine, <2 s cold start, works fully offline. A compiled Rust binary satisfies this directly.
  • The obvious existing CSG crate (csgrs) currently fails to build from crates.io at any published version (0.18–0.20.1) because it has a mandatory, non-optional dependency on core2, and every version of core2 has been yanked from crates.io. Rather than depend on a git fork (which would break reproducible builds), crates/simple3d-geom has its own small, from-scratch BSP boolean CSG kernel and a QuickHull-style convex hull implementation.
  • The viewport is a from-scratch software rasterizer, so the app starts and stays usable on a machine with no accelerated graphics (acceptance criterion 19). eframe only has to provide a window and 2D drawing. The GPU renderer the footer offers draws through the OpenGL context that window already has, so it adds no dependency and no requirement: it is picked, it can fail, and the software renderer is what it falls back to.

Workspace layout

crates/
  simple3d-geom/     Vec3, Mesh, primitive generators, BSP CSG boolean
                       kernel, convex hull, post-boolean mesh repair,
                       flat-region retriangulation and connected-piece
                       splitting. Pure math; depends on none of the others.
  simple3d-core/     Domain model: Node/Scene tree, the declarative
                       primitive parameter registry, the stored-mesh and
                       broken-apart bodies,
                       scene evaluation with per-subtree caching and
                       cancellation, undo, clipboard, units, project files,
                       settings, keymaps.
  simple3d-export/   STL (binary and ASCII), OBJ, PLY and 3MF writers, with
                       pre-write watertightness verification, progress
                       reporting and cancellation. Includes a minimal zip
                       writer for the 3MF container.
  simple3d-app/      eframe/egui desktop UI: outliner, property editor,
                       software-rasterized viewport, direct-manipulation
                       handles, docks, menus and dialogs, evaluation worker
                       thread.
packaging/
  deb/               Debian package: desktop entry, icon, MIME type and the
                       script that assembles them around the built binary.
  windows/           The application icon compiled into the .exe, and the WiX
                       authoring for the .msi installer.

Engineering highlights worth knowing about:

  • Every primitive from the spec's table: box, rounded box, wedge, regular prism, sphere/ellipsoid, spherical cap, cylinder, tube, capsule, torus (full and arc), cone, pyramid, regular pyramid, and all four regular polyhedra (tetrahedron/octahedron/icosahedron by hand, dodecahedron built as the icosahedron's dual), plus a chamfered box and a slot. Plate/disc/ring are thin aliases onto box/cylinder/tube per the spec's "must produce identical geometry" requirement. The round solids take a sweep angle, so a quarter cylinder or a pipe elbow is one shape rather than a boolean. Measured against the circumscribed convention and exact on flat axes.
  • Booleans are watertight and manifold for the degenerate cases that are the normal case in practice — coplanar faces, coincident surfaces, operands touching at a single edge, fully contained and fully disjoint operands.
  • Evaluation is deterministic, cached per subtree and cancellable. The spec's 200-primitive scene (fifty assemblies, 100 nested boolean groups) evaluates cold in ~0.12 s and updates after a one-dimension edit in ~6 ms. See crates/simple3d-core/tests/performance.rs.
  • Boolean output is retriangulated per flat region, so a plate with a hole, a slot and a boss comes out at ~230 triangles rather than the ~1500 a plane-clipping BSP leaves behind. See crates/simple3d-geom/src/planar/.
  • Pointer gestures are executed by tests, not only reasoned about: crates/simple3d-app/src/gestures/ replays real pointer events over a real frame with egui_kittest.

Building

cargo build --workspace --release
cargo test --workspace
cargo run --release -p simple3d-app          # or: target/release/simple-3d
cargo run --release -p simple3d-app -- my-project.simple3d

# What one boolean chain costs, step by step -- the first thing to run when a
# scene starts feeling slow or an export comes out unexpectedly large.
cargo run --release -p simple3d-geom --example boolean_cost

The toolchain is pinned by rust-toolchain.toml. On Linux the usual X11/Wayland development packages are needed (libx11-dev libxcursor-dev libxi-dev libxrandr-dev libxkbcommon-dev libwayland-dev on Debian and Ubuntu); the release workflow lists the same set.

Releases are cut by pushing a v* tag: .github/workflows/release.yml takes the version from the tag, tests and builds both targets from that one commit, and attaches the two portable executables, the Debian package and the Windows installer to the GitHub release. Each package is built from the very binary the same job uploads, so a release cannot carry two different builds. The Debian package can be built by hand from any release binary:

packaging/deb/build.sh target/release/simple-3d 0.1.0 dist

Where things live

If you are looking for Start at
A new primitive type simple3d-core/src/primitive/registry/ — one declaration drives the Add menu, the property editor and the project file
Boolean semantics and operand ordering simple3d-geom/src/lib.rs (evaluate_boolean)
Why a boolean result is the shape it is simple3d-geom/src/csg_bsp/, then repair/ and planar/
Caching and invalidation simple3d-core/src/eval/hash.rs (subtree_key)
Manipulator handle behaviour and modifiers simple3d-app/src/gizmo/ (mods.rs, drag_update.rs)
What a numeric field accepts simple3d-core/src/unit/entry.rs, then unit/expr.rs
The docks, and what moves between them simple3d-app/src/dock/
Several documents open at once, and what switching a tab swaps simple3d-app/src/tabs/swap.rs
The menu bar, and the dialogs that are windows of their own simple3d-app/src/app_chrome/menu.rs and app_chrome/dialog.rs
A colour, a row height or a type size simple3d-app/src/theme/palette.rs — nothing else names one
An icon, a primitive's silhouette, or the window's own icon simple3d-app/src/icon/paint.rs, and icon/app_icon.rs for the window's
The ground grid, the origin axes and how they fade simple3d-app/src/render/grid_extent.rs and render/axis_draw.rs
Whether a value field is a slider or a text field this frame simple3d-app/src/ui/buffers.rs (FieldBuffers::scrub_field)
Keymap presets, rebinding and conflicts simple3d-core/src/keymap/preset.rs and keymap/lookup.rs
Navigation bindings taking effect without a restart simple3d-app/src/panel_viewport/gesture.rs (nav_gesture)
Driving a pointer gesture in a test simple3d-app/src/gestures/
Whether a criterion is really covered tools/criteria_audit.py
What the Debian package installs, and what it depends on packaging/deb/build.sh
What the Windows installer puts where packaging/windows/simple-3d.wxs
File format and migration simple3d-core/src/project/io.rs, then project/unknown.rs

Licence

PolyForm Noncommercial License 1.0.0. Free for any noncommercial purpose — personal projects, hobby work, study, research, and use by schools, charities and public bodies. Commercial use needs a separate licence. See LICENSE.md for the full terms.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages