Files
Vernier/PROJECT_BRIEF.md

11 KiB
Raw Permalink Blame History

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 type verniercad-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.md holds the invariants. Per-crate CLAUDE.md holds local context.
  • MISTAKES.md at root. Every non-obvious bug that cost more than an hour gets an entry.
  • FUSION_LOG.md from L1 onward.
  • Conventional commits. One logical change per commit.
  • No feature merges without a headless test.
  • When an invariant in CLAUDE.md seems wrong, say so and stop. Do not route around it.