AI-powered 3D modeling for 3D printing, built with signed distance fields.
Sinter is a web-based parametric 3D modeler that uses signed distance fields (SDF) instead of traditional BREP geometry. This means:
- Smooth booleans — union, subtract, intersect with adjustable fillet radius. No topology failures.
- Shell/hollow — one click to make any solid hollow with uniform wall thickness.
- AI-powered — describe what you want in natural language, the AI builds the model. Viewport renders are sent for visual context.
- Real-time preview — GPU ray marching renders every parameter change instantly.
- 3D print ready — export watertight STL/3MF at your choice of resolution, dimension overlays for verification.
git clone https://github.com/kmatzen/sinter.git
cd sinter
npm install
npm run devOpen http://localhost:5173 and click "Start Modeling" to launch the app.
To use AI chat, open Settings (the gear in the toolbar, top right), pick a provider, then either Connect OpenRouter — a one-click sign-in, no API key to create — or paste an Anthropic or OpenAI key directly. Credentials are stored only in your browser.
No Sinter account is needed for any of that: OpenRouter bills your own OpenRouter credits, and Sinter never proxies your requests.
- Primitives: Box, Sphere, Cylinder, Torus, Cone, Capsule, Ellipsoid, visually editable line-and-circular-arc profile extrusion (XY/XZ/YZ planes; symmetric/one-sided/two-sided; taper; thin wall) and partial/full revolve around a named axis or straight profile edge, with closed SVG and ASCII DXF profile import that preserves circular arcs
- Booleans: Union, Subtract, Intersect (with smooth/fillet parameter), and a bounded faceted Hull whose support-plane detail is adjustable
- Modifiers: Shell, Offset, Round, Chamfer, Draft, Twist, Bend, Mirror, Half-Space Cut (with flip). Chamfer is a generic SDF approximation, so its visual quality follows the child field. Draft, Twist, and Bend provide bounded mechanical deformations with fixed reference/origin planes.
- Patterns: Linear Pattern, Circular Pattern
- Transforms: Translate, Rotate, Scale
- Presets: Pre-built parts (enclosures, standoffs, brackets, vents, clips). Every preset states a guaranteed outer envelope that a test checks against the geometry itself, so the size on the card is the size you get.
- STL import: Bring outside geometry in as an editable node — subtract it, intersect it, pattern it. Stored as a signed-distance field, so detail finer than the field's grid is rounded off; the resolution is adjustable per node.
- Drag & drop: Click or drag parts from the palette into the node tree
- GPU ray marching with screen-space outline post-process
- Tap-to-select — click/tap on a surface to select the contributing node
- Hover preview — the node under the pointer is outlined and named before you click, so selection is not a guess
- Selection breadcrumb — shows where the selected node sits in the tree (
Subtract › Move › Cylinder); click any crumb to select that ancestor - Alt+Click — select the operation above the shape you clicked, since picking always lands on a leaf
- Hovering a row in the node tree highlights its geometry, and vice versa
- Clipping plane (+X/-X/+Y/-Y/+Z/-Z) with cross-section fill
- Per-node dimension labels with wireframe bounding box
- Transform gizmo with snap-to-grid (1/5/10mm)
- Screenshot export (gizmo auto-hidden)
- Describe models in natural language
- Streaming responses — text appears token-by-token as the model generates
- Iterative refinement ("make it bigger", "add ventilation holes")
- Multi-view renders sent automatically (current view + front/right/top with rulers)
- OpenRouter sign-on — connect once and pick from hundreds of models across providers. OpenRouter bills your own account; Sinter never proxies your requests and never holds your payment details.
- Model picker — browse the provider's live catalog with pricing and context length. Defaults to vision-capable models only, since Sinter sends viewport renders with every message.
- Also supports Anthropic Claude and OpenAI GPT directly (bring your own API key)
- Google Drive — sign in with Google to save/load projects
- GitHub Gists — sign in with GitHub to save/load projects
- Read-only share links for published projects
| Key | Action |
|---|---|
| Click | Select the shape under the pointer |
| Alt+Click | Select the operation above that shape |
| W | Move tool |
| E | Rotate tool |
| R | Scale tool |
| Escape | Deselect gizmo |
| Ctrl+C | Copy node |
| Ctrl+V | Paste node |
| Ctrl+D | Duplicate node |
| Ctrl+Z | Undo |
| Ctrl+Shift+Z | Redo |
| Delete | Remove selected |
| Shift (hold) | Disable snap |
| ? | Show all shortcuts |
Sinter is a static site that deploys to Cloudflare Pages. The only server-side code is two small Pages Functions that hold the OAuth client secrets and exchange code for tokens. Everything else (project storage, sharing) is browser → provider direct.
See DEPLOY.md for the current architecture, deployment, and rollback runbook.
Quick version:
- Create OAuth apps (callback URL:
https://YOUR_DOMAIN/auth/callback):- Google: console.cloud.google.com — scope
drive.file - GitHub: github.com/settings/applications/new — scope
gist
- Google: console.cloud.google.com — scope
- Set GitHub Actions secrets:
CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID,VITE_GOOGLE_CLIENT_ID,VITE_GITHUB_CLIENT_ID. - Set Cloudflare Pages environment variables:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET. - Push to
main.
Without OAuth configured, the app still works fully for local modeling and AI chat — cloud save/load just won't be available.
- Frontend: React, TypeScript, Vite, Tailwind CSS
- 3D Viewport: Three.js (pure, no R3F)
- Geometry: Custom SDF engine with GPU ray marching and marching cubes export
- Hosting: Cloudflare Pages (static) + Pages Functions (OAuth token exchange)
- Storage: User's own Google Drive or GitHub Gists, accessed directly from the browser
- State: Zustand
- Font: Outfit + JetBrains Mono
- Tests: Vitest (unit) + Playwright (E2E)
npm run typecheck # tsc --noEmit
npm test # unit tests
npm run test:e2e # Playwright, including CPU/GPU parity and golden images
npm run bench # export pipeline timings, per stage
npm run check:clarity # measured legibility/a11y regression gate (requires ../clarity)
npm run test:live # asks a real model for real geometry (needs a credential)CI keeps one Playwright worker per runner because WebGL uses software rendering,
then distributes Chromium tests across four isolated runners. Reproduce one
shard with CI=1 npx playwright test --project=chromium --shard=1/4; change the
numerator to select another shard. Run the complete browser gate with
npm run test:e2e. Mobile and golden projects run independently with
CI=1 npx playwright test --project=mobile and --project=golden.
The Clarity check builds Sinter, audits landing, login, and deterministic editor
states at desktop and mobile widths, writes clarity-report.json, and fails if
any gating finding increases. For local use, clone
kmatzen/clarity next to this repository,
run npm install && npm run build there once, then run
npm run check:clarity here. CI checks out the reviewed Clarity revision
recorded in ci.yml, uploads the full JSON report, and is part of the CI result
required by the release guard. Existing contrast findings are explicit ratchet
baselines; reduce those numbers whenever a UI change fixes them.
test:live is the only test that spends money, so it is skipped unless you opt
in with a credential:
SINTER_LIVE_API_KEY=sk-... npm run test:live
# SINTER_LIVE_PROVIDER anthropic | openai | openrouter (default: openrouter)
# SINTER_LIVE_MODEL overrides the provider's default modelIt also runs weekly in CI (live-llm.yml), because what breaks this path is a
provider changing its wire format or a model drifting out of the response format
the system prompt asks for — neither of which any commit of ours triggers.
A few of these are load-bearing rather than routine, and are worth knowing about before changing the geometry code:
e2e/sdf-parity.spec.tsrenders the emitted GLSL to a float texture and diffs it against the TypeScript evaluator. The viewport and the exporter are two implementations of one field; drift between them is invisible until a print comes out wrong.e2e/viewport-golden.spec.tscompares the viewport against committed reference images, rendered through SwiftShader on every machine so they are not hostage to the local GPU. Shader changes are expected to alter these — regenerate with--update-snapshots, and look at the diff.npm run benchreports the minimum of--repeat=Nruns rather than the mean, because noise only ever adds time. It also runs in CI, non-gating, with the numbers in the job summary.specs/holds TLA+ models of the worker bridge, token refresh and undo history.specs/check.shruns them.
Non-commercial license. See LICENSE for details.
Copyright (c) 2026 Kevin Blackburn-Matzen.