BenchUp

Download for desktop
A free, open-source 3D modeling app for woodworking and furniture design, written in Rust. Plan individual boards, wood joints, and complete assemblies with exact measurements before heading to the workshop. The same editor runs as a native Windows, macOS, or Linux application and in a desktop browser through WebAssembly.
The workspace includes Select, Line, Rectangle, Circle, Polygon, Freehand, Push/Pull, Scale, Offset, Move, Rotate, Paint, Tape Measure, Protractor, Dimensions, a Union flyout, Erase, and Orbit, automatic geometry snapping, filled planar faces, measured lines, full-enclosure box selection, groups and components, a right-click object menu, geometry clipboard, undo/redo, local autosave, and portable .bup project files, with support for legacy .k3d and .ketchup projects. It starts with an empty model. File → Import/Export adds SketchUp .skp import and Wavefront .obj export. Mobile and cloud storage are outside this version.
Woodworking features
Model shelves, tables, cabinets, workbenches, and other furniture with precisely sized parts. Use groups, reusable components, and tags to keep the assembly organized, then inspect dimensions and fit from any angle.
Wood joinery
Create mortise-and-tenon, half-lap, tongue-and-groove, rabbet, dovetail, and finger joints between boards, including angled connections. Adjust joint dimensions and clearance with live previews, inspect machining diagnostics and cut guides, and trim excess material past a joint while keeping the original boards editable.

Wood grain orientation
Set the grain direction on groups and components and show grain-direction guides directly on the model. Review how the grain runs across each board and through the full assembly before building. Grain orientation is saved with the project and follows the parts when they rotate.
Plan materials and workshop work
Assign stock materials and wood types to parts, configure sanding and finishing products, and prepare cut lists, cutting plans, and workshop labels. Export cutting plans and time and cost estimates to PDF to take the design into the workshop.
Documentation
This README covers developer installation, building, verification, and deployment. For instructions on using BenchUp, see the user documentation.
Prerequisites
- Rust 1.95 or newer, installed with rustup; the repository was checked with Rust 1.97.1.
- A C/C++17 compiler for the native SketchUp reader (GCC/Clang on Linux, Xcode Clang on macOS, or MSVC on Windows). Browser builds do not require it.
- A GPU/driver supported by wgpu. The browser prefers WebGPU and falls back to WebGL2.
- On Linux, development libraries for X11/Wayland and xkbcommon. For Debian/Ubuntu:
sudo apt install build-essential pkg-config libx11-dev libxcb1-dev libxkbcommon-dev libwayland-dev libegl1-mesa-dev. - On Windows, the MSVC Rust toolchain and Visual Studio C++ build tools.
- On macOS, Xcode Command Line Tools.
Run on desktop
cargo run -p benchup-app
On Linux, register the application launcher and icons once after compiling:
python3 scripts/install_linux_desktop.py
This installs into your user data directory ($XDG_DATA_HOME, or ~/.local/share) without root access. GNOME on Wayland uses the application's ID and matching desktop entry to find its icon. Restart BenchUp after installing. The launcher points to target/debug/benchup-app, so later development compilations update the same executable. For a release installation, pass its path: python3 scripts/install_linux_desktop.py target/release/benchup-app. Keep that executable in place, or rerun the installer with its new path if you move it.
Run in a browser
Install the browser target and Trunk once:
rustup target add wasm32-unknown-unknown
cargo install --locked trunk
From the repository root:
trunk serve
Open . Keep the app on the same origin/port to restore the same saved model. No Node.js, JavaScript application framework, or backend is required. Trunk generates the JavaScript needed to load the Rust WebAssembly program.
Development builds use light optimization for application code and stronger optimization for dependencies and the geometry core, on both desktop and WebAssembly. Debug symbols, debug assertions, and overflow checks remain enabled. These optimizations target the cost of menu hovering, scrolling, and rendering in debug mode. The profile takes effect on the next development compilation; changing it recompiles affected dependencies once.
Application messages
The English catalog is assets/i18n/en.json. Use
tr!("area.message") for a label and tr!("area.message", name = name) for a
template with a named variable. Keep the key stable when editing the text.
Templates use Rust format syntax, including precision such as {value:.2}.
For a quantity, put both complete messages under one and other and pass
count = quantity before the other arguments. English uses one for exactly
1 and other for zero and all other quantities.
Rust embeds English at build time and the browser startup screen reads the same catalog. Keep storage keys, shortcuts, file-format tokens and wire codes separate from display messages. Release-note content lives in its own feed.
Compile for distribution
Desktop, on the OS you want to target:
cargo build -p benchup-app --release --locked
The executable is target/release/benchup-app (benchup-app.exe on Windows). These commands produce executables, not installers or signed application bundles. Native builds should run on the corresponding OS; this project does not configure cross-linkers or signing identities.
Automated desktop builds
Build desktop runs when a v* tag is pushed
(for example, git push origin v0.1.0). The tag must be a stable vX.Y.Z
version matching [workspace.package].version in Cargo.toml. It builds with
stable Rust and the committed Cargo.lock, runs update regression tests, then
packages the app with Velopack:
| Artifact | Platform | Install / run |
|---|---|---|
benchup-windows-x64 |
Windows x64 (MSVC) | *-win-x64-Setup.exe (per-user installer) |
benchup-macos-arm64 |
macOS Apple Silicon | *-osx-arm64-Setup.pkg or the portable .app zip |
benchup-linux-x64 |
Linux x64 (built on Ubuntu 22.04) | *-linux-x64.AppImage |
After all three builds succeed, the workflow creates a GitHub Release for the
pushed tag with the prepared release notes, installers, portable packages, full
.nupkg updates, one releases..json feed per OS/architecture, and
SHA256SUMS. It validates the packages against their feed hashes and publishes
only after every platform succeeds and all assets have uploaded. Reruns can
replace assets on an unfinished draft; a published version cannot be overwritten.
To ship another update, first add its user-facing notes to
assets/release-notes.json (newest version first, keeping previous releases),
then bump the workspace version and lockfile. Validate and preview the notes:
python3 scripts/desktop_release.py version --tag vX.Y.Z
python3 scripts/desktop_release.py notes --tag vX.Y.Z
Refresh the README screenshot by launching an existing desktop executable as described below. Deploy requests do not run local builds or tests; GitHub Actions performs the desktop and browser builds, tests, and packaging. Commit and push the release changes and notes before creating and pushing the matching tag. Release validation rejects missing or mismatched notes. The same catalog supplies the GitHub Release description and Help → Release notes in the app. Publishing uses GitHub's automatic token. Signing needs the credentials below.
Release signing
Without signing secrets, CI produces unsigned packages for development.
Before public distribution, configure signing and set the repository Actions
variable BENCHUP_REQUIRE_SIGNING=true. This makes packaging fail if Windows or
macOS signing is missing. Checksums detect damaged downloads; they do not
replace publisher signatures. Release access and signing credentials should be
restricted to maintainers. Velopack's signing guide
describes certificate/account setup.
For Windows, configure these repository Actions secrets:
| Secret | Value |
|---|---|
AZURE_CREDENTIALS |
Azure service-principal credentials JSON for azure/login, with permission to sign using the chosen profile |
WINDOWS_AZURE_SIGNING |
JSON with Endpoint, CodeSigningAccountName, and CertificateProfileName for Azure Artifact Signing |
The workflow logs into Azure and passes a temporary configuration file to Velopack's Windows signing integration. The account needs the Artifact Signing Certificate Profile Signer role. Windows SDK signing tools are supplied by the Windows build runner.
For macOS, export the Developer ID Application and Developer ID Installer
certificates with their private keys together into a password-protected
.p12, then configure:
| Secret | Value |
|---|---|
MACOS_CERTIFICATES_P12 |
Base64 of the .p12, on one line |
MACOS_CERTIFICATE_PASSWORD |
Password protecting the .p12 |
MACOS_SIGN_APP_IDENTITY |
Exact Developer ID Application signing identity |
MACOS_SIGN_INSTALL_IDENTITY |
Exact Developer ID Installer signing identity |
APPLE_ID |
Apple developer account email |
APPLE_TEAM_ID |
Apple developer team ID |
APPLE_APP_PASSWORD |
App-specific password for notarization |
CI imports the certificates into a temporary keychain, stores a notarytool
profile, and asks Velopack to sign and notarize the app and installer. It removes
the .p12 after importing and deletes the keychain even if packaging fails.
Partially configured signing fails instead of silently producing unsigned files.
The Rust dependency, VPK_VERSION in the workflow, and the version in
scripts/desktop_release.py are pinned together at 1.2.161. Update them
together. Only the packaging machine needs .NET 8 for the vpk CLI; the Rust
app does not require users to install .NET.
Before shipping publicly, install one packaged version on each supported OS, make a test project, publish a newer version, and verify automatic download, restart, restored work/file association, the opt-out preference, and failure recovery. Native update tests run in tag CI; package installation and OS trust dialogs still need this platform smoke test.
Browser distribution
trunk build --release --locked
Serve the resulting dist/ directory with a static web server. Use HTTPS for WebGPU (localhost is also supported). public_url = "./" allows serving from a subdirectory. There is no service worker or external asset dependency.
GitHub Pages website
The presentation page lives in site/. The
Deploy GitHub Pages workflow builds the browser
editor into dist/site/play/, adds the landing page and existing branding and
screenshot assets to dist/site/, and deploys them together. The main browser
entry point (index.html) and trunk serve continue to open the editor directly.
To enable publishing for BenchUp3D/editor:
- In the repository, open Settings → Pages → Build and deployment and set Source to GitHub Actions. See the GitHub Pages setup guide.
- Push a
v*tag (for example,git push origin v0.1.0), or run Deploy GitHub Pages from the Actions tab withmainselected. Version tag pushes start the desktop release workflow. Pages builds and deploys the same commit only after that workflow succeeds, including publishing the desktop release. The updated Pages workflow must be on the default branch for this automatic trigger to work. Ordinary branch pushes do not trigger either workflow. - After the deployment succeeds, the landing page is at and its Try in browser links open . The workflow's environment link shows the actual URL if a custom domain is configured.
All local page and editor asset links are relative, so the same package supports
the repository path or a custom domain. GitHub Pages supplies HTTPS. The editor
uses browser-local autosave; visitors should download .bup projects they
want to keep. GitHub Pages is not cloud model storage.
The landing page includes a canonical URL, Open Graph image metadata, and a large-image Twitter/X card using the editor screenshot. During deployment, the workflow sets these absolute URLs from the GitHub Pages configuration, including a configured custom domain. Social previews require the deployed public HTTPS page and image; the local preview URL cannot be fetched by social networks.
Development and verification
Refresh the README screenshot from an existing desktop executable:
python3 -B scripts/capture_readme_desktop.py
On Linux, this requires X11 or XWayland, ImageMagick's import command, and
Python Pillow. The script selects the newest existing executable from the native
release and debug output directories; --binary /path/to/benchup-app selects one
explicitly. It launches the desktop app in an isolated temporary profile, loads
assets/examples/Example.bup with its saved
camera and display settings, dismisses open dialogs and popups, captures the
settled window at 1920 × 1080, and updates assets/screenshots/benchup.jpg and its
README link. Visually verify that no modal remains open before using the image.
It never compiles the app, runs tests, or starts benchmarks. The screenshot
reflects the existing executable; a missing executable is reported without
building one.
cargo check --workspace --locked
cargo check --workspace --target wasm32-unknown-unknown --locked
cargo test --workspace --locked
The vendored SketchUp reader, adapter, and browser shortcut handling can also be tested without compiling the application:
node --test scripts/test_skp_import.cjs
node --test scripts/test_web_shortcuts.cjs
An optional offscreen GPU test renders the actual UI and custom viewport to PNGs without opening a window:
cargo test -p benchup-app gpu_renders_empty_and_modeled_workspace --locked -- --ignored --nocapture
Snapshots appear in target/qa/. This test requires a working native GPU adapter. Normal tests cover document validation, geometry, inference, camera math, selection, undo/redo, and synthetic keyboard/pointer interactions through egui.
Measure actual viewport frame intervals in the running Web app without starting a build:
node scripts/benchmark_viewport_web.mjs --headed --project /path/to/Project.bup --seconds 25 --min-fps 45 --output /tmp/benchup-viewport.json
The benchmark uses an isolated browser profile, preserves the complete project,
warms up for five seconds, and records FPS, frame timings, the hardware adapter,
geometry buffer rebuilds, screenshots, and the project hash. It rejects software
adapters, blank screenshots, GPU validation errors, and missing measurements.
Use --mode select to alternate selection while picking across the scene, or
--mode groups to repeatedly enter/leave nested groups. --mode draw-line
and --mode draw-rectangle drive real pointer/keyboard events with snapping,
preview, geometry creation and autosave enabled; their FPS includes the commit
stall and counts actual GPU frame submissions. Reports include commit latency
and the number of created edges. --mode measure-tape measures live tape
snapping with a saved guide present and checks that another guide is saved.
--mode walk places a person on a real floor, walks with WASD and turns the
camera. --after-walk checks an editor tool after exiting Walk and restoring its
camera. The reports count Walk bridge mutations so repeated browser capture
updates cannot silently return. --require-pointer-lock additionally requires
browser mouse capture; without it, a reported browser refusal uses ordinary
mouse look. Each run pins its loaded artifact against development-server reloads.
--validate-snapping compares indexed and exhaustive inference on the actual
project, including axis/plane locks, editing scopes and both projections.
--validate-state
checks immediate selection/context restoration after reload and isolation from
another project; --validate-shader checks edge occlusion on the GPU.
Pass --legacy to compare
the CPU projection path on the same scene, or --profile for a separate CPU
profile (profiling adds overhead). The default server is http://127.0.0.1:8080/;
use --url or --browser to override the server or Chromium executable.
Large opaque perspective scenes retain their face and edge geometry on the GPU;
other display modes continue to use the precision renderer. Selection, hover,
and editing scope update a small GPU state texture without rebuilding geometry.
Exact inspector measurements run in a background worker. Selection and editing
scope persist immediately in a small record tied to the current autosave,
avoiding a complete document rewrite for each click.
Run the repeatable CPU interaction benchmarks explicitly (10/100-face drawing, 10/100-object Move, a 4,096-face imported model, axes on/off with 512 groups, and 256/1,024 shadow casters over a large receiving face):
cargo test -p benchup-app --locked --config 'profile.test.package.benchup-app.opt-level=2' --config 'profile.test.package.benchup-core.opt-level=2' workload_benchmark -- --ignored --nocapture --test-threads=1
These settings optimize application geometry and rendering code while retaining the existing test dependencies. Timings exclude model construction and GPU presentation; compare runs on the same machine without other CPU-heavy work. The shadow workload reports regeneration after sun changes and CPU mesh/edge rendering separately. The normal suite also exercises all 22 tools through interrupted gestures, focus loss, resizing, and document reload, plus display styles and effects.
For a sequential feature audit, including the normally ignored GPU comparisons and per-tool CPU workloads, run:
python3 scripts/run_feature_audit.py
This uses an isolated temporary autosave directory and writes the feature
checklist, individual test results, GPU comparisons, timings, command logs, and
remaining platform checks to target/qa/feature-audit/report.txt and
report.json. Every display field has an independent before/after GPU case;
normal interaction tests click the individual toggles, face-style choices,
color pickers, paint swatches, and project confirmation buttons.
Verified during implementation: native Linux and browser WASM Cargo checks; core and UI interaction tests; offscreen Vulkan rendering of empty, modeled, and compact workspaces. Windows/macOS execution, browser startup, and runtime WebGPU/WebGL2 fallback still require platform smoke testing. Production builds and hosted deployment were not run.
Layout
crates/benchup-core: double-precision document geometry, planar face discovery/triangulation, inference, picking, camera, tool definitions, and bounded history. It has no GUI or GPU dependencies.crates/benchup-app: egui workspace, gesture routing, local persistence, native/browser launchers, and the wgpu/WGSL edge renderer. Model coordinates remain independent of GPU resources.index.htmlandTrunk.toml: static browser host and WASM development configuration.
The viewport uses depth-tested filled triangles and antialiased triangle strips for consistent edge widths, CPU projection/clipping, and the same shader and editor behavior on every target. Rendering is event-driven rather than a continuous idle loop.
Account server
Google login and account sessions are provided by the separate Ketchup Server
repository at /home/dcoulombe/dev/ketchup-server. See its README.md for OAuth
configuration and startup instructions.
The account server is deployment configuration, with no address field in the app.
Launch the desktop app with benchup-app --server-url https://accounts.example.com
or cargo run -p benchup-app -- --server-url http://localhost:8081.
Both --server-url URL and --server-url=URL are supported.
Alternatively, copy .env.example to .env in the workspace root and set
BENCHUP_SERVER_URL. The example uses port 8080 for the local Web client through
Trunk; for native-only development, use port 8081. Builds embed this value for desktop and Web; changing .env
causes Cargo to refresh it on the next build. You can also supply it directly:
BENCHUP_SERVER_URL=https://accounts.example.com cargo build -p benchup-app
BENCHUP_SERVER_URL=https://accounts.example.com trunk build --release
Desktop precedence is launch argument, runtime BENCHUP_SERVER_URL, .env in the
current working directory, then the compiled value. Web uses the compiled value.
There is no implicit server address; without configuration the modeling app runs
with the account avatar, its separator, and the login dialog hidden. Saved user preferences
cannot override the server address.
For local desktop development, use http://localhost:8081. For the local Web
client, compile with BENCHUP_SERVER_URL=http://localhost:8080 to use the
Trunk.toml account proxies to the server on port 8081. These proxies use
no_redirect = true so Google login redirects reach the browser. Restart
trunk serve after changing proxy configuration. Other browser deployments
need a same-origin reverse proxy or the server's explicitly configured CORS and
compatible cookie policies. Use HTTPS outside localhost.
.env supports BENCHUP_SERVER_URL=value, single/double quoted values, optional
export, and trailing comments. It is ignored by Git; do not put secrets in the
URL because Web builds expose it to the browser.
Desktop update feeds follow the repository that built the application. GitHub
Actions embeds github.repository, so fork releases update from that fork's
GitHub Releases. Local builds use the GitHub origin remote; source archives
without Git metadata default to BenchUp3D/editor. Set BENCHUP_UPDATE_REPOSITORY
to owner/repository at build time to select an explicit feed. The fork must
publish its own compatible installer and update assets through the release
workflow. Already installed upstream binaries retain their original update feed.
Forks use their own GitHub repository for website source, documentation, license,
and release links. Pages resolves these links from GITHUB_REPOSITORY during
staging. Enable Actions in a fork and run Synchronize repository links once
(or push a README change) to update the README links; later README changes on
the default branch synchronize automatically. Set the repository variable
PAGES_BASE_URL when using a custom Pages domain. The fork must enable Pages
and publish its own releases before its browser and download links are available.
BenchUp identity migration
The application, desktop package, and bundle ID are now BenchUp /
io.github.BenchUp3D.BenchUp; the executable is benchup-app and new
projects use .bup. Existing .k3d and .ketchup projects remain readable.
Desktop workspaces, update preferences, restart metadata, and inspector settings
are copied from the old KetchUp locations on first launch without deleting the
original data. Browser storage migrates on the same origin; a change of hostname
requires exporting projects before moving because browsers isolate local storage.
The old KETCHUP_DATA_DIR, KETCHUP_SERVER_URL, and
KETCHUP_UPDATE_REPOSITORY variables remain accepted as compatibility aliases;
the BENCHUP_* equivalents take precedence.
The official migration is a transfer/rename of the original repository from
Ketchup3D/app to BenchUp3D/editor. Transfer and rename may happen in either
order. Existing 0.6.5 applications retain the original GitHub URL in their binary;
GitHub's repository redirects let those applications reach the new releases and
download their assets. Do not create a new repository or fork at Ketchup3D/app:
GitHub removes the transfer redirects when the old location is reused.
The organization/repository transfer itself remains a GitHub administration step.
Before an official release builds, CI runs:
python3 -B scripts/verify_update_transfer.py --repository BenchUp3D/editor
This uses public, unauthenticated GitHub API requests, like the installed updater. It requires the old and new repository URLs to return the same numeric repository ID and the same releases. A missing redirect, an incorrect transfer, or a recreated old repository blocks the release before publication. Forks skip this official transfer requirement and continue using their own release source.
The first BenchUp release must be newer than 0.6.5 (for example 0.6.6). Packaging
explicitly uses --delta None; publication validation rejects delta feeds so old
clients receive a complete replacement with the new app ID and executable. Keep
the existing win-x64, osx-arm64, and linux-x64 channels. New official builds
normalize both intermediate repository names to BenchUp3D/editor.
The pinned Velopack 1.2.161 client is exercised in CI with an old 0.6.5 manifest, HTTP transfer/rename redirects, synthetic full packages and real checksums on all three channels. It checks discovery, download validation, pending-update discovery, and the new executable metadata. The test intentionally does not use BenchUp's new update-preparation code, which is absent from the already-installed clients. An actual installed-app replacement/restart with signed release packages remains a separate OS-level verification; the fixture test does not execute an installer.