11 KiB
VernierCAD — Project Brief
A vernier is the auxiliary scale on a caliper — the mechanism that gets you from "about right" to an actual measurement. Also a commune in Geneva, which is a coincidence that never needs explaining to anyone.
Display name: VernierCAD. The suffix disambiguates from Vernier Software & Technology, the US science-education company, and makes the project findable by people searching for open-source CAD. Crate prefix, binary, and repo: plain
vernier. Nobody wants to typeverniercad-occt-sys.
A parametric 3D CAD application. Native Linux, GPU-accelerated, history-based modelling with a B-rep kernel. Fusion 360's model, Shapr3D's restraint.
1. What this is
A single-user, solid-modelling CAD app:
- Sketch on a plane → constrain → extrude/revolve → modify with fillets, shells, patterns
- Full parametric history: edit any upstream feature, everything downstream rebuilds
- Exports STL and STEP; imports STEP
- Runs headless from a CLI for scripting and testing
Not in scope: assemblies, joints, 2D drawings, CAM, simulation, generative design, cloud sync, rendering/materials, sheet metal, and surfacing beyond loft/sweep. Fusion has those. This does not.
Multi-body is implemented. The old Phase 8+ exclusion no longer applies to multi-body;
the other exclusions above remain deliberate. Current sequencing lives in
docs/PLAN_2026-09-14_daily-reliability-spline-modeling.md.
2. Success metric
Parts I have actually made with it. Not features shipped, not lines written, not phases completed. The project is working if the count goes up.
This is the only metric. When a decision is ambiguous, ask which option gets a real part out of the machine sooner.
The ladder
| Rung | Part | Requires |
|---|---|---|
| L0 | Spacer / washer | sketch → extrude → cut → STL |
| L1 | Bike light mount | constraints, holes, fillets, edit-after-the-fact |
| L2 | Snapmaker jig / enclosure | shell, draft, patterns, naming under stress |
| L3 | Rod-holder bracket, organic transition | exact spline sweep, spline-section ruled loft, edge fillet, direct edit, upstream dimension change |
| L4 | Two-part snap-fit case | multi-body modeling and explicit body context |
L1 is the real milestone. Anything can do L0. L1 requires that a sketch edit propagates through a fillet without exploding — which is the entire problem this project exists to solve.
The dogfooding rule
Once L1 passes, Fusion is only opened for parts Vernier provably cannot express. Every time
it is opened, log the missing feature to FUSION_LOG.md. That log is the Phase 5–6 backlog,
ordered by real usage rather than by what is fun to build.
3. Locked decisions
These were argued out. Do not relitigate without new information.
| Decision | Choice | Rationale |
|---|---|---|
| Language | Rust | Compiler catches what an agent gets wrong. Cargo. wgpu. |
| Kernel | OCCT, own cxx bridge, behind trait Kernel |
Booleans, fillets, offsets, STEP on day one. Fornjot spent four years on pure-Rust b-rep and archived in June 2026 with goals unmet. That is the cost of the alternative. |
| Bridge style | Thin C++ façade + opaque integer handles | Binding OCCT's Handle<T> intrusive refcounting through cxx is a months-long fight with no payoff. |
| No OCAF | Own the document model | OCAF wants to own document, undo, and persistence. All three are ours. |
| Sketch solver | Own, sparse Jacobian + Levenberg–Marquardt | Tractable numerics. Avoids SolveSpace GPLv3. Keeps licence freedom. |
| Render | wgpu → Vulkan/RADV | Native. Target GPU is AMD RX 7800 XT, RDNA3, Mesa/RADV. Never assume NVIDIA. |
| UI | egui, behind an abstraction | Ships fast, wgpu-native. Swap for custom vello later if it reads too "egui". |
| Interaction model | Fusion: sketch → history tree | Direct push/pull arrives in Phase 5 as a timeline feature, not as a parallel modelling mode. |
| Units | Millimetres internally, f64 throughout | |
| Licence | GPL-3.0-or-later | Decided 2026-08-07. Strong copyleft: forks and derivatives stay open; proprietary plugins are knowingly off the table. OCCT is LGPL-2.1-with-exception — compatible; link dynamically in dev, statically for release. |
4. The three bets
Everything else is implementation detail. These three decide whether the project survives.
4.1 The kernel is not the bottleneck
Fusion feels slow because of recompute strategy, tessellation, and a blocking UI thread. Shapr3D feels fast because the viewport never drops a frame regardless of what the kernel is doing. All three of those are ours, not OCCT's.
The kernel runs on a worker thread. The render thread is sacred. A model that takes eight seconds to recompute must still orbit at monitor refresh while it does.
OCCT hot paths get replaced later — tessellation first (Phase 7), booleans in 2030, maybe
never. The trait Kernel boundary exists so that is possible, not because it is planned.
4.2 Topological naming
The problem that maimed FreeCAD. Downstream features reference faces and edges of upstream results. Identify them by ordinal index and a sketch edit silently reassigns a fillet to a different edge.
Identity derives from generative history, never from index, never from pointer.
Every kernel operation returns a shape plus a delta:
pub struct OpResult {
pub shape: ShapeId,
pub modified: HashMap<EntityId, SmallVec<[EntityId; 2]>>,
pub generated: HashMap<EntityId, SmallVec<[EntityId; 4]>>,
pub deleted: HashSet<EntityId>,
}
OCCT supplies the raw material: every BRepBuilderAPI_MakeShape subclass exposes
Generated(), Modified(), IsDeleted(); boolean operations additionally provide a
BRepTools_History. The C++ façade translates those into our stable EntityId space via
TopTools_IndexedMapOfShape. Rust never sees an OCCT index.
Fallback. When history is unavailable — a sketch edit changed the face count, the feature re-ran from scratch — match by geometric fingerprint:
surface type + area + centroid (feature-local coords) + adjacency signature
Best candidate above a confidence threshold wins. Below threshold, mark the reference broken and surface it in the timeline. Never silently pick the nearest face. Fail closed.
A wrong-but-plausible face assignment is worse than an error, because the user ships the part.
4.3 Headless-first
Every modelling operation must be drivable with no window, no GPU, no display, producing a deterministic artifact that can be asserted on.
cargo run -p vernier-cli -- run part.rhai --export out.step --json-report
This is what makes autonomous iteration possible. A feature that cannot be exercised headlessly is not finished.
5. Architecture
crates/
vernier-occt-sys/ cxx bridge + facade.cpp — the ONLY crate allowed `unsafe`
vernier-kernel/ trait Kernel, OpResult, EntityId; OCCT impl behind it
vernier-solver/ 2D constraint solver. ZERO dependency on kernel.
vernier-doc/ feature DAG, EntityId space, undo stack, serialization
vernier-tess/ tessellation + cache
vernier-render/ wgpu, ID-buffer picking
vernier-ui/ egui shell, viewport widget, timeline
vernier-cli/ headless driver — the agent's entry point
vernier-app/ thin main()
Dependency rule: arrows point downward only. vernier-solver and vernier-doc must
never depend on vernier-kernel or anything below it. That is what keeps them fast to test
and small enough to reason about in one context window.
Picking
Render entity IDs to an offscreen R32Uint target. Read back one pixel. Exact for faces, edges and vertices, constant time, no raycast geometry. Costs one extra pass; worth it.
Build
OCCT vendored as a git submodule, static link, for reproducibility. A system-occt cargo
feature points at the CachyOS package for fast iteration. Keep both — the vendored build
is a coffee break.
6. Phases
| # | Phase | Effort | Gate |
|---|---|---|---|
| 0 | Foundation: wgpu viewport, camera, command bus + undo, doc skeleton, headless CLI, CI | S | --selftest passes with no display |
| 1 | OCCT façade, primitives, booleans, tessellation, edge render, ID picking, STL export, naming spec written | M | Box − cylinder renders; click a face, it highlights |
| 2 | Sketcher + solver: 12 constraints, drag, DOF readout, auto-constraints | XL | Fully-constrained sketch does not move when dragged; solver <5 ms @ 200 constraints |
| 3 | Feature DAG, extrude/revolve, incremental recompute, timeline UI, naming implemented, save/load, crash recovery | L | L0 |
| 4 | Hole, fillet, chamfer, shell, draft, patterns, mirror | L | L1 → switch over |
| 5 | UX: gizmos, contextual toolbar, snapping, measure, sections, direct push/pull | M | L2 |
| 6 | Loft, sweep, splines, STEP I/O, Rhai scripting | L | L3 |
| 7 | Perf: parallel branch recompute, tess cache, LOD, kernel fully off render thread | M | 500-feature model recomputes with no frame drop |
| 8+ | Historical phase placeholder; see the current dated plan | XL | L4 |
Rough calibration at ~10 h/week with heavy agent use: L0 around month 4–5, L1 around month 7–9.
Phase 2 is where this dies if it dies. It is a numerical-methods project wearing a UI costume, and it produces nothing visible until Phase 3 wires it up. Budget for the morale dip. Consider building the solver's own debug visualiser early — seeing constraint residuals converge is the only dopamine available in that phase.
7. Risks, honestly
| Risk | Severity | Mitigation |
|---|---|---|
| Phase 2 morale collapse | High | Solver debug viewer. Timebox to 12 weeks. Ship a bad solver and iterate rather than perfecting it dry. |
| Naming design wrong, discovered Phase 4 | High | Spec written and reviewed in Phase 1, before extrude exists. Adversarial test fixtures from day one. |
| L1 plateau — works, but every real part hits a gap | High | FUSION_LOG.md discipline. Backlog ordered by real usage. |
| OCCT build friction eats weeks | Medium | system-occt feature. Do not fight the vendored build during development. |
cxx + OCCT refcounting rabbit hole |
Medium | Opaque handles + C++-side registry. Never expose Handle<T> to Rust. |
| Non-determinism makes testing impossible | Medium | Determinism is a hard invariant from Phase 0, not a later fix. |
| egui ceiling on Shapr3D-grade polish | Low | Real, but a Phase 5 problem. UI is behind an abstraction. |
| Scope creep into CAM/simulation | Low | Section 1. Re-read it. |
8. Working agreements
- Root
CLAUDE.mdholds the invariants. Per-crateCLAUDE.mdholds local context. MISTAKES.mdat root. Every non-obvious bug that cost more than an hour gets an entry.FUSION_LOG.mdfrom L1 onward.- Conventional commits. One logical change per commit.
- No feature merges without a headless test.
- When an invariant in
CLAUDE.mdseems wrong, say so and stop. Do not route around it.