diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..788d761 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "vernier-app" +version = "0.0.1" + +[[package]] +name = "vernier-cli" +version = "0.0.1" + +[[package]] +name = "vernier-doc" +version = "0.0.1" + +[[package]] +name = "vernier-kernel" +version = "0.0.1" + +[[package]] +name = "vernier-occt-sys" +version = "0.0.1" + +[[package]] +name = "vernier-render" +version = "0.0.1" + +[[package]] +name = "vernier-solver" +version = "0.0.1" + +[[package]] +name = "vernier-tess" +version = "0.0.1" + +[[package]] +name = "vernier-ui" +version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a106535 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] +resolver = "3" +members = ["crates/*"] + +[workspace.package] +version = "0.0.1" +edition = "2024" +rust-version = "1.96" +license = "GPL-3.0-or-later" +repository = "https://git.briggen.dev/NilsBriggen/vernier" +publish = false + +[workspace.dependencies] +indexmap = "2" +thiserror = "2" +tracing = "0.1" + +# Deny-by-default discipline from CLAUDE.md. Test code may locally +# `#[allow(clippy::unwrap_used)]` — production code may not. +[workspace.lints.rust] +missing_docs = "deny" + +[workspace.lints.clippy] +unwrap_used = "deny" +expect_used = "deny" diff --git a/crates/vernier-app/Cargo.toml b/crates/vernier-app/Cargo.toml new file mode 100644 index 0000000..f3dc766 --- /dev/null +++ b/crates/vernier-app/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "vernier-app" +description = "VernierCAD desktop application" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[features] +# Forward to vernier-occt-sys/system-occt once the kernel dependency chain +# reaches this crate (Phase 1). Declared now so +# `cargo run -p vernier-app --features system-occt` works as documented. +system-occt = [] + +[lints] +workspace = true diff --git a/crates/vernier-app/src/main.rs b/crates/vernier-app/src/main.rs new file mode 100644 index 0000000..e6aae9c --- /dev/null +++ b/crates/vernier-app/src/main.rs @@ -0,0 +1,9 @@ +//! Thin `main()` for the VernierCAD desktop application. +//! +//! Everything of substance lives in the library crates; this binary only +//! wires them together. The egui/wgpu shell arrives with the Phase 0 render +//! milestone. + +#![forbid(unsafe_code)] + +fn main() {} diff --git a/crates/vernier-cli/Cargo.toml b/crates/vernier-cli/Cargo.toml new file mode 100644 index 0000000..8073850 --- /dev/null +++ b/crates/vernier-cli/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-cli" +description = "Headless driver for scripting and testing" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-cli/src/main.rs b/crates/vernier-cli/src/main.rs new file mode 100644 index 0000000..a6566a2 --- /dev/null +++ b/crates/vernier-cli/src/main.rs @@ -0,0 +1,32 @@ +//! Headless driver for VernierCAD — the primary verification path. +//! +//! Every modelling operation must be drivable through this binary with no +//! window, no GPU, and no display, producing a deterministic artifact that +//! can be asserted on. A feature that cannot be exercised here is not +//! finished. + +#![forbid(unsafe_code)] + +use std::process::ExitCode; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + match args.first().map(String::as_str) { + Some("--selftest") => selftest(), + _ => { + eprintln!("usage: vernier-cli --selftest"); + ExitCode::FAILURE + } + } +} + +/// Runs every registered self-check headlessly and prints a JSON report to +/// stdout. The report must be byte-identical across runs (invariant #1). +/// +/// Phase 0 gate: passes with no display attached. Subsystem checks register +/// here as they land; an empty run still proves the binary works headless. +fn selftest() -> ExitCode { + let report = r#"{"selftest":"pass","checks":[]}"#; + println!("{report}"); + ExitCode::SUCCESS +} diff --git a/crates/vernier-cli/tests/selftest.rs b/crates/vernier-cli/tests/selftest.rs new file mode 100644 index 0000000..cdd11f6 --- /dev/null +++ b/crates/vernier-cli/tests/selftest.rs @@ -0,0 +1,32 @@ +//! The Phase 0 gate: `--selftest` passes with no display, deterministically. + +#![allow(clippy::unwrap_used)] + +use std::process::{Command, Output}; + +fn run_selftest() -> Output { + Command::new(env!("CARGO_BIN_EXE_vernier-cli")) + .arg("--selftest") + .env_remove("DISPLAY") + .env_remove("WAYLAND_DISPLAY") + .output() + .unwrap() +} + +#[test] +fn selftest_passes_headless() { + let out = run_selftest(); + assert!(out.status.success(), "selftest exited non-zero"); + let report = String::from_utf8(out.stdout).unwrap(); + assert!(report.contains(r#""selftest":"pass""#), "report: {report}"); +} + +#[test] +fn selftest_output_is_deterministic() { + let first = run_selftest(); + let second = run_selftest(); + assert_eq!( + first.stdout, second.stdout, + "selftest output differs between runs — invariant #1 violated" + ); +} diff --git a/crates/vernier-doc/Cargo.toml b/crates/vernier-doc/Cargo.toml new file mode 100644 index 0000000..1b7a612 --- /dev/null +++ b/crates/vernier-doc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-doc" +description = "Document model: feature DAG, EntityId space, undo stack, serialization" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-doc/src/lib.rs b/crates/vernier-doc/src/lib.rs new file mode 100644 index 0000000..af4a295 --- /dev/null +++ b/crates/vernier-doc/src/lib.rs @@ -0,0 +1,7 @@ +//! Document model: feature DAG, `EntityId` allocation, undo stack, serialization. +//! +//! `EntityId`s are allocated here and mapped through the kernel façade — +//! entity identity never derives from an index or a pointer. Like +//! `vernier-solver`, this crate must never depend on `vernier-kernel`. + +#![forbid(unsafe_code)] diff --git a/crates/vernier-kernel/Cargo.toml b/crates/vernier-kernel/Cargo.toml new file mode 100644 index 0000000..dc3acd2 --- /dev/null +++ b/crates/vernier-kernel/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-kernel" +description = "Kernel abstraction: trait Kernel, OpResult, EntityId" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-kernel/src/lib.rs b/crates/vernier-kernel/src/lib.rs new file mode 100644 index 0000000..b5f2ba4 --- /dev/null +++ b/crates/vernier-kernel/src/lib.rs @@ -0,0 +1,8 @@ +//! Kernel abstraction: `trait Kernel`, `OpResult`, `EntityId`. +//! +//! Every kernel operation returns a shape *plus a history delta* describing +//! which entities were generated, modified, or deleted — an operation that +//! returns a bare shape is incomplete. The OCCT implementation lives behind +//! this boundary so hot paths can be replaced later without touching callers. + +#![forbid(unsafe_code)] diff --git a/crates/vernier-occt-sys/Cargo.toml b/crates/vernier-occt-sys/Cargo.toml new file mode 100644 index 0000000..6f80649 --- /dev/null +++ b/crates/vernier-occt-sys/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "vernier-occt-sys" +description = "cxx bridge and C++ facade to the OCCT geometry kernel" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[features] +# Build against the distro OCCT package instead of the vendored submodule. +# No-op until the Phase 1 bridge lands; declared now so the fast-iteration +# commands documented in CLAUDE.md work from day one. +system-occt = [] + +[lints] +workspace = true diff --git a/crates/vernier-occt-sys/src/lib.rs b/crates/vernier-occt-sys/src/lib.rs new file mode 100644 index 0000000..f57ef89 --- /dev/null +++ b/crates/vernier-occt-sys/src/lib.rs @@ -0,0 +1,9 @@ +//! FFI bridge to OCCT via a thin C++ façade with opaque `u64` shape handles. +//! +//! This is the **only** crate in the workspace permitted `unsafe`. The façade +//! owns a shape registry on the C++ side; OCCT's `Handle` never crosses the +//! FFI boundary, and every OCCT exception is caught in C++ and converted to a +//! result code before it can unwind into Rust. +//! +//! The bridge itself arrives in Phase 1. `facade.hpp` will be the complete API +//! surface. diff --git a/crates/vernier-render/Cargo.toml b/crates/vernier-render/Cargo.toml new file mode 100644 index 0000000..ad462f5 --- /dev/null +++ b/crates/vernier-render/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-render" +description = "wgpu renderer with ID-buffer picking" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-render/src/lib.rs b/crates/vernier-render/src/lib.rs new file mode 100644 index 0000000..cb35b7f --- /dev/null +++ b/crates/vernier-render/src/lib.rs @@ -0,0 +1,8 @@ +//! wgpu renderer: viewport, camera, ID-buffer picking. +//! +//! The render thread is sacred — it never blocks on the kernel. Kernel work +//! runs on a worker; the viewport orbits at refresh rate during an +//! eight-second recompute or the design is wrong. Target GPU is AMD RDNA3 via +//! Mesa/RADV (Vulkan); never assume NVIDIA. + +#![forbid(unsafe_code)] diff --git a/crates/vernier-solver/Cargo.toml b/crates/vernier-solver/Cargo.toml new file mode 100644 index 0000000..07252d3 --- /dev/null +++ b/crates/vernier-solver/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-solver" +description = "2D sketch constraint solver (sparse Jacobian, Levenberg-Marquardt)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-solver/src/lib.rs b/crates/vernier-solver/src/lib.rs new file mode 100644 index 0000000..5d6f75e --- /dev/null +++ b/crates/vernier-solver/src/lib.rs @@ -0,0 +1,8 @@ +//! 2D sketch constraint solver: sparse Jacobian + Levenberg–Marquardt. +//! +//! Depends on **nothing** from the kernel layer, by design — that is what +//! keeps it fast to test and small enough to reason about in one sitting. +//! If a task here seems to need `vernier-kernel`, the abstraction is wrong; +//! raise it rather than adding the dependency. + +#![forbid(unsafe_code)] diff --git a/crates/vernier-tess/Cargo.toml b/crates/vernier-tess/Cargo.toml new file mode 100644 index 0000000..02d9b99 --- /dev/null +++ b/crates/vernier-tess/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-tess" +description = "Tessellation and tessellation cache" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-tess/src/lib.rs b/crates/vernier-tess/src/lib.rs new file mode 100644 index 0000000..d7ee5f0 --- /dev/null +++ b/crates/vernier-tess/src/lib.rs @@ -0,0 +1,3 @@ +//! Tessellation of kernel shapes into render meshes, plus the tessellation cache. + +#![forbid(unsafe_code)] diff --git a/crates/vernier-ui/Cargo.toml b/crates/vernier-ui/Cargo.toml new file mode 100644 index 0000000..e4c1584 --- /dev/null +++ b/crates/vernier-ui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vernier-ui" +description = "egui shell: viewport widget, timeline" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/vernier-ui/src/lib.rs b/crates/vernier-ui/src/lib.rs new file mode 100644 index 0000000..86c55af --- /dev/null +++ b/crates/vernier-ui/src/lib.rs @@ -0,0 +1,7 @@ +//! egui application shell: viewport widget, timeline, panels. +//! +//! egui sits behind an abstraction so it can be swapped later without +//! touching the rest of the stack. Degrees exist only at this boundary — +//! everything below works in radians and millimetres. + +#![forbid(unsafe_code)] diff --git a/scripts/check.fish b/scripts/check.fish new file mode 100755 index 0000000..adae1f4 --- /dev/null +++ b/scripts/check.fish @@ -0,0 +1,7 @@ +#!/usr/bin/env fish +# Full check — must pass before any commit. + +cargo fmt --check +and cargo clippy --all-targets -- -D warnings +and cargo test --workspace +and cargo run -q -p vernier-cli -- --selftest