Modelcore
← Back to Blog
4 min readShane Scranton

A Correctness Contract for Browser CAD

Why 'watertight' geometry is necessary but not sufficient.

engineering

Browser-first CAD can feel fast and accessible, but it often earns less trust than desktop tools. Not because the browser can't draw triangles quickly, but because modeling systems fail in ways users can't tolerate: booleans that "mostly work," selections that aren't repeatable, and operations that leave the scene subtly invalid.

A closed shell ("watertight") is necessary. It's not sufficient.

In Modelcore, we formalize a Correctness Contract: a small set of guarantees that define "safe, predictable modeling" in a browser-first direct modeler. This post lays out the contract, why it matters, and how we enforce it.

Why watertight doesn't solve the real problem

A model can be watertight and still be unusable in practice.

An operation might partially apply before failing, leaving the user with a model that's changed in ways they didn't intend. A selection that used to resolve to a face might now resolve to a different face, or nothing. IDs can churn, breaking undo/redo assumptions, tools, and automation. Exports might succeed once, but later edits introduce degeneracies you only discover downstream.

Watertight is the baseline. The contract is about behavioral correctness: how the system behaves under success, failure, and repeated edits.

The Correctness Contract

Atomic failures: success or no change. If an operation claims to produce a solid, it must either succeed and produce a valid result, or fail with a specific reason and leave the authoritative model unchanged. No partial topology updates. No stale caches. No half-mutated selection state. We enforce this at command boundaries with validation and rollback, and failures return actionable reasons (not generic "operation failed").

Selection determinism: the same click yields the same target. Selection is the foundation of editing. Given the same camera, scene state, and cursor input, selection must be deterministic. If multiple targets are plausible, we either choose the same one every time via an explicit tie-breaker, or return a structured ambiguity that tools can resolve intentionally. This matters even more in a browser environment with high-DPI input, dense geometry, and multiple competing snap systems.

ID guarantees: define what stays stable and what doesn't. A lot of CAD pain comes from pretending all identities can remain stable across topology edits. They can't. Stable face/edge identity through booleans and splits is one of the hardest problems in geometric modeling (the topological naming problem). So we do the honest thing: we explicitly define what is stable, and we write tests to enforce it.

At v1, our contract is:

  • Stable: nodeId (scene graph identity), solidId (solid identity across repeated edits), and component/group instance IDs
  • Not guaranteed across topology-changing operations: face IDs and edge IDs across booleans and splits; per-face material assignments across operations that rebuild topology

This isn't a limitation to hide. It's a constraint to design around. When identities aren't stable, tools and UX are designed to re-target intentionally rather than silently drifting. This is our current v1 contract; as the engine matures we'll expand what we can guarantee.

The Stability Matrix: what survives which operations

A correctness contract is only as good as your ability to verify it. We maintain a stability matrix that specifies, for each operation class (move node, edit group, boolean union/subtract, split face, etc.), whether these properties remain stable: nodeId, solidId, face/edge IDs, selection, and materials.

Each cell is one of three states: stable by contract, best-effort, or not guaranteed. This prevents two common failures: tools assuming stability that doesn't exist, and refactors quietly changing stability and breaking downstream behavior.

Enforcing it with authoritative command results

Correctness and determinism are difficult to enforce if the model is an opaque blob and change is inferred by diffing snapshots.

Instead, every command returns an authoritative result: entities created, modified, or deleted; selection changes and reasons; and structured failure errors. This makes behavior enforceable across undo/redo, automation, collaboration, and incremental render caches. Most importantly, it gives us a test artifact: the engine itself states what changed.

We enforce this with invariant checks at command boundaries and regression tests that lock the stability matrix in place.

What this enables

This contract isn't "BIM in the browser." It's more foundational: a direct modeler you can trust day-to-day, a geometry core that fails safely, and a platform surface that can grow without becoming brittle.

Once you have atomic operations, deterministic targeting, and explicit stability rules, features like automation, plugins, and parametric components become feasible without turning into a tarpit.

The goal is trust, not perfection

We want Modelcore to feel as fast as SketchUp while producing models that behave reliably downstream. That requires being explicit about what "correct" means and enforcing it.

Watertight is the starting line. A correctness contract is what earns trust.