Foxtrot is a fast viewer for STEP files, a standard interchange format for mechanical CAD. It is an experimental project built from the ground up, including new libraries for parsing and triangulation.
This repository includes a simple native GUI:
In addition, the same code can run in a browser (click to see the demo):
For more background on the project, check out this writeup by one of the main authors.
(Prerequisite: install Rust and Cargo, and clone this repository)
cargo run --release -- examples/cube_hole.step(Prerequisite: install wasm-pack and clone this repository)
cd wasm
wasm-pack build --target no-modules
python3 -m http.server --directory deploy # or the simple server of your choiceThen, open the local server's URL (typically 127.0.0.1:8000)
and select a sample file from the list.
The deploy directory links to the generated pkg/wasm.js and
pkg/wasm_bg.wasm; rebuild them after Rust changes. The worker transfers a
Float32Array plus completion and source-identified failure records, rather
than treating a partial mesh as a successful conversion. Partial-load warnings
remain visible when the camera moves.
cdt: Constrained Delaunay triangulation (standalone)express: Parser for EXPRESS schemas files and a matching code generation systemstep: Auto-generated STEP file parser. This take a very long time to compile, so it is isolated into this crate.triangulate: Converts a file loaded bystepinto a triangle mesh, usingcdtas its corenurbs: A handful of NURBS / B-spline algorithms used bytriangulategui: GUI for rendering STEP files, using WebGPUwasm: Scaffolding to run in the browser using WebAssembly
step/src/ap214.rs is automatically generated from
10303-214e3-aim-long.exp, which is available via CVS here
(check out the APs folder).
To regenerate, run
cargo run --release --example gen_exp -- path/to/APs/10303-214e3-aim-long.exp step/src/ap214.rs
STEP models are not vendored into this repository. Two public libraries provide a large, varied corpus of real-world component models for testing and benchmarking the parser and tessellator:
- Würth Elektronik KiCad Library
— several thousand vendor-authored models under
3dmodels/, exported from Solid Edge, FreeCAD, and other tools, spanning AP203, AP214, and AP242. - KiCad packages3D
— the official KiCad 3D model library, mostly FreeCAD/OCCT-generated
generic packages (
CC-BY-SA-4.0 WITH KiCad-libraries-exception).
Clone either library outside the repository (or under ignored local/).
Keep the library's license and revision with your corpus; the harness does not
download or redistribute models. Start with the checked-in examples:
cargo build --release -p triangulate --example corpus_worker
python3 scripts/corpus.py examples --output local/smoke --meshes allThe new harness requires Python 3.10+ and uses only the standard library unless
you enable OCCT. It recursively discovers .step/.stp files, regardless of
extension case. Each model runs in a separate process with a hard timeout;
on POSIX the harness kills the worker process group and reaps the worker.
Partial tessellation, panics, crashes, nonfinite browser attributes, empty or
entirely collapsed browser meshes, and oracle failures produce a nonzero exit.
The worker evaluates the same centered f32 position/normal/color buffer as
Three.js, produced from f64 geometry by Mesh::to_triangle_buffer.
Isolated collapsed triangles, zero normals, and f64 degenerates remain explicit
quality_diagnostics, not automatic whole-model failures. ok means processing
and browser-buffer checks passed, not that visual quality is established.
Review flagged models at useful viewing scales for holes, silhouette errors,
and shading defects. No triangles are deleted or vertices merged to pass checks.
STL remains a world-coordinate diagnostic for coarse measurements and optional
OCCT comparison; its f32 precision does not gate browser output. Reports identify
their classification basis. Worker protocol schema 2 is required; old aggregate
counter and STL-fallback workers are intentionally unsupported.
For fast browser-only iteration, use --meshes none: the worker computes the
same browser diagnostics but skips STL serialization, disk writes and Python
geometry validation. Use --meshes all on a focused selection when you need
world-coordinate geometry or --occt. Reports list failures and quality-review
cases before unflagged successes. review-manifest.json selects failures and
quality-flagged models with their source hashes; replay it with --manifest
to investigate only that set, without another full scan.
To capture the exact browser buffer for a Three.js reproduction:
target/release/examples/corpus_worker model.step metrics.json - browser.binbrowser.bin contains little-endian f32 values with stride nine: position,
normal, color, expanded three vertices per triangle. No STEP reparsing or
alternate mesh conversion is required on the JavaScript side.
Use the fixed seven-model stress/control sample rather than a full corpus
sweep. It covers winding surfaces, a capacitor, a connector, a planar control
and a degenerate-torus cap. Inputs are hash-pinned in
scripts/performance-sample.json, relative to local/ (the Wurth and KiCad
corpora must already be present).
Native phase instrumentation is opt-in through the profiling feature.
Repository examples enable it automatically; normal library consumers do not
pay for phase clocks or accumulation. Browser builds always omit it.
CARGO_BUILD_JOBS=1 CARGO_INCREMENTAL=0 cargo build --release -p triangulate --example profile
python3 scripts/performance.py local --output local/perf-before
# After changing and rebuilding the profile worker:
python3 scripts/performance.py local --output local/perf-after \
--compare local/perf-before/results.jsonThe capture runs two fresh processes per model, serially on one Rayon thread.
Its default global budget is 240 seconds; unfinished samples are explicit
failures, not omitted passes. Builds are a separate step. report.md and
results.json record elapsed/CPU stage timings, inclusive internal phases and
call counts, peak RSS, geometry/storage counts, raw samples, input/worker
hashes and before/after time/memory ratios. The initial complete capture took
136 seconds in the development orb.
This measures read → flatten → parse → tessellate → browser buffer, not STL, OCCT or color-grouped export. RSS is cumulative process high-water memory, not live allocation size; do not add the stage peaks or nested phase timings. No warmup or cache flush is performed. Use the same machine and sample for comparisons, and run the correctness harness separately before accepting a speedup. The profiler takes its thread-local timing snapshot inside the same single-worker pool that performs tessellation.
scripts/board-corpus.json lists 40 Diode board repositories. The acquisition
command shallow-clones them and inventories every .kicad_pcb, including
reference and vendored module layouts. Existing checkouts are not updated.
Unavailable repositories and LFS pointers remain explicit coverage gaps.
Configure Git credentials outside the transcript for private repositories.
python3 scripts/board_benchmark.py fetch
gh repo clone diodeinc/diode local/diode-benchmark -- --depth 1
python3 scripts/build_board_worker.py --diode local/diode-benchmark
python3 scripts/board_benchmark.py run --output local/board-bench/before
# After changing Foxtrot, rebuild the worker, then compare identical boards:
python3 scripts/build_board_worker.py --diode local/diode-benchmark
python3 scripts/board_benchmark.py run --output local/board-bench/after \
--compare local/board-bench/before/results.jsonUse a dedicated Diode checkout. The builder inserts asserted, recorded
diagnostic logging and a timer around its Foxtrot API call, because the
production adapter otherwise discards partial-tessellation diagnostics.
timings.foxtrot_call_secs excludes Diode model decoding, placement, board
geometry and scene serialization. This does not change scene
generation or error handling. Unknown source drift is rejected. build.json
records revisions, diff/source/worker hashes, compiler and resolved dependency
identities; the generated Cargo lockfile is retained in the build directory.
Cargo metadata verifies Diode uses this checkout's Foxtrot, not its pinned
upstream dependency. Initial integration uses Diode revision
0bc053a26e30019465eccb14c9210dd06eb2c87f.
To measure another Foxtrot revision, pass --foxtrot /path/to/worktree to the
builder. Older revisions returning diagnostic fields rather than methods
also need --diagnostics fields. Keep separate copies of each executable,
build metadata and lockfile before rebuilding. Cross-version comparisons must
check the recorded Diode revision and uninstrumented source hash; the logging
adapter differs only to read the two diagnostic APIs. Report changed face
coverage and component triangle counts alongside API time, rather than calling
different meshes equivalent work. Whole-scene RSS is not Foxtrot-only memory.
The worker calls the actual web pipeline: parse_kicad_pcb,
placements_with_dnp(..., true), full prepare_scene, then serialize_scene.
It includes board geometry, color-grouped world-space component meshes,
instancing, viewer metadata and serialization. It does not use the normalized
Foxtrot demo buffer or the metadata-omitting prepare_scene_headless shortcut.
Only embedded STEP models are resolved, matching Diode; external library paths
are not silently replaced or fetched. Each unique embedded name is tessellated
once per board, with no cross-board mesh cache.
The suite runs fresh processes serially, twice per board, with a 240-second
capture budget and 120-second per-board timeout. Increase --budget for a
larger full sweep; exhausted samples are listed, never omitted. Native wall,
CPU and peak RSS measurements exclude builds, network access, GPU rendering
and browser/WASM startup. Validation runs after the timed scene serialization.
The first accessible subset (32 boards in seven repositories) takes about
14 seconds for 64 samples. This is not a browser latency measurement.
JSON retains stage times, model/placement/mesh counts, output sizes, diagnostics
and per-sample logs. missing_models includes footprints without models or
with unresolved/unsupported sources; it is distinct from partial Foxtrot
processing. Exit 1 indicates incomplete corpus/model coverage or a processing
failure, even when all accessible boards generate scenes. Comparisons require
matching inputs and instrumentation, and omit timing ratios when processing
coverage or mesh counts change. No mesh blobs are retained by default.
python3 -m unittest discover -s scripts -p 'test_board_benchmark.py'# Stable hash-ranked sampling; globs match relative paths and may be repeated.
python3 scripts/corpus.py path/to/KiCad-Library/3dmodels \
--sample 100 --seed 42 --exclude '*Connector*' \
--repeat 5 --warmup 1 --output local/before
# Rebuild after your change, then use precisely the same inputs.
cargo build --release -p triangulate --example corpus_worker
python3 scripts/corpus.py path/to/KiCad-Library/3dmodels \
--manifest local/before/manifest.json --compare local/before/results.json \
--repeat 5 --warmup 1 --timing-threshold 0.15 --output local/afterTEST_ASSETS_DIR supplies the root if the positional root is omitted.
--jobs controls model concurrency and --threads controls Rayon threads per
worker (both default to 1). Use concurrency for functional sweeps; benchmark
serially on the same idle machine with identical settings. Warmups are discarded
fresh-process runs, warming OS caches, not an in-process allocator or thread pool.
Reports retain raw samples, median/min/max phase timings, worker SHA-256, checkout
revision/status, and configuration. The timing gate uses median parse +
triangulation time, excluding reads, STL export, validation, OCCT, and startup;
those worker I/O phases and total worker wall time are recorded separately.
Timing gates are opt-in because short runs and shared machines are noisy.
The manifest records relative paths, sizes, and SHA-256 hashes, so duplicate
basenames cannot collide and replay rejects changed inputs. A fixed seed repeats
selection for an unchanged corpus; use the manifest to freeze it as the library
grows. Comparisons require the same selected paths and contents, report changed
face/triangle counts and increased tessellation failures, and reject incompatible
benchmark settings when timing is gated. Counts changing is a review signal,
not necessarily a bug. Any current functional failure still fails the run even
if it existed in the baseline. The old thread-based regression_test executable
is removed; use this process-isolated harness and record a fresh baseline instead
of importing its basename-keyed JSON.
Each new output directory contains:
report.md: failures and quality-review cases first, with links to diagnostics.results.json: machine-readable report and baseline, including geometry metrics.manifest.json: exact replayable selection, with input hashes.review-manifest.json: replayable failures and quality-review cases.progress.jsonl: flushed completed results; per-case results also survive interruption.cases/<path-hash>/: per-invocation logs/backtraces, worker metrics, a reproduction command inresult.json, and any requested binary STL meshes. Failed meshes are retained by default;--meshes nonewrites no STL, while--meshes allor--compareretains successful meshes too. Inputs are not copied.
# Recheck just failures, with verbose Rust diagnostics and saved meshes.
RUST_LOG=debug python3 scripts/corpus.py path/to/KiCad-Library/3dmodels \
--rerun local/after/results.json --timeout 120 --meshes all --output local/recheck
# Or isolate one relative path, then open its mesh in an STL viewer.
python3 scripts/corpus.py examples --include 'cube_hole.step' \
--meshes all --output local/one-modelThe output directory must not already exist. Exit codes are 0 for success, 1 for model failures or baseline changes, and 2 for setup/selection/report errors. Ctrl-C cancels queued work, terminates active workers, and exits with code 130. An interrupted run retains individual case results but is not a complete baseline; replay its manifest into a new directory. Timeouts default to 60 seconds per invocation, independently for Foxtrot and OCCT. There is no memory limit: reduce concurrency or use OS/container limits for untrusted or very large inputs.
python3 -m venv local/occt-env
local/occt-env/bin/pip install cadquery-ocp libigl
local/occt-env/bin/python scripts/corpus.py examples --occt \
--meshes all --output local/oracleOCCT independently reads STEP and emits a millimeter-scale STL with 0.01 mm
linear and 0.1 rad angular deflection. The transferred shape must pass OCCT's
BRepCheck_Analyzer before meshing. Every transferred face must have triangles;
a partial reference is retained for diagnosis but rejected for comparison.
Models OCCT rejects or cannot completely tessellate are outside the repair
corpus: retain their diagnostics and omit them from the active replay manifest,
rather than counting them as Foxtrot passes. A timeout or tool-installation error
alone is not an established model rejection. OCCT validity describes its
transferred shape, not a proof of original STEP conformance.
In addition to bounds and area, the harness compares bidirectional sampled
point-to-triangle distances using libigl's native AABB hierarchy. Mesh transport
is memory-mapped and only one target hierarchy exists at a time. libigl is an
optional offline-tool dependency, not linked into the Rust or browser code.
Use --jobs 1 for large models: OCCT reference generation can itself be expensive.
OCCT meshing runs serially within each job; it does not create another parallel
meshing pool. Reuse retained reference meshes with compare_meshes when iterating
on native geometry, rather than regenerating unchanged references for each trial.
Foxtrot → OCCT detects extra/displaced surfaces; OCCT → Foxtrot
detects missing surfaces. There is no alignment, rescaling, mesh repair or vertex
correspondence: different triangulations can represent the same surface.
Each direction uses 10,000 seeded, area-weighted samples plus up to 10,000 face
centroids to probe smaller faces. --surface-samples controls that budget.
--surface-tolerance defaults to 0.1 mm; the worst sampled distance in both
directions must be within it. Choose tolerances appropriate to the part's smallest
important features; this default is not an accuracy guarantee. oracle.json
records p50/p95/p99, RMS, the worst sampled distance and its coordinates, and the
area-sampled fraction outside tolerance. Percentiles/fractions use only the
area-weighted samples; the maximum also includes the face probes.
Defaults are 5% relative tolerance and 0.01 mm absolute bounds tolerance;
adjust --relative-tolerance and --absolute-tolerance for your models.
Bounds allow absolute tolerance + relative tolerance × reference diagonal;
area allows absolute tolerance squared + relative tolerance × reference area.
Signed volume is diagnostic only because STEP can contain open shells or
inconsistent winding. Zero-area STL facets are counted but excluded from the
distance query; they do not block comparison of the remaining surface. Neither
exported mesh is modified. Empty/nonfinite/zero-area meshes are not comparable.
Agreement is evidence, not proof: sampling can miss tiny holes; it does not certify Hausdorff distance, topology, watertightness, winding or shading. OCCT can also be wrong. Both STL transports have world-coordinate f32 rounding, so extreme coordinate offsets or very tight tolerances need a higher-precision follow-up. Both meshes and detailed deltas are kept on a mismatch. Conversion and distance queries run together in the timeout-isolated oracle subprocess. Missing optional dependencies or failed conversion is an explicit failure, never a skipped check. Reports record comparison settings and dependency versions. Re-record old OCCT baselines: aggregate-only acceptance is not equivalent to surface agreement.
Generate a three-pane browser report for every oracle_mismatch in a retained
corpus run. The panes share camera, scale and lighting; rotation, zoom and
wireframe controls support inspection beyond the initial view.
python3 scripts/oracle_report.py local/before --after local/after \
--output local/visual-report
python3 -m http.server 8090 --directory local/visual-reportBoth corpus runs must retain meshes and oracle measurements for the same input
hashes. --after is required: every comparison shows genuine baseline, current
Foxtrot, and OCCT meshes rather than substituting the baseline for a missing
output. The generator downloads pinned Three.js 0.180.0 modules and its license once into the output;
viewing needs no external assets. Geometry uses neutral flat shading, not STEP
colors or the application's vertex-normal pipeline. Serve only the generated
directory, not the repository or corpus roots. In an orb, use a supervised
service with a portal to share the report.
Harness tests (no Rust build or corpus download required):
python3 -m unittest discover -s scripts -p 'test_corpus*.py'Run the same command with local/occt-env/bin/python to include the optional
surface-distance tests (different triangulations, displaced/missing faces and
zero-area facets).
To extract STEP models embedded in a KiCad board or footprint for local
testing, use scripts/extract_steps.py (requires pip install zstandard).
© 2021 Formlabs
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Foxtrot is a proof-of-concept demo, not an industrial-strength CAD kernel. It may not work for your models! Even in the screenshots above, there are a handful of surfaces that it fails to triangulate; look in the console for details.
This isn't an official Formlabs project (experimental or otherwise), it is just code that happens to be owned by Formlabs.
No one at Formlabs is paid to maintain this, so set your expectations for support accordingly.

