Draft Studio — command-driven CAD-light
Policies that keep the org coherent
name: Draft Studio CAD
overview: Ship Draft Studio (LibreCAD/AutoCAD-light) with a shared command pipeline, interchange-ready .cadjson (incl. NURBS splines) and .cadphys.json mesh/collider extension. Concept Studio unchanged; no CadKit.
todos:
- id: cadjson-schemas
content: Author novolis.cad + novolis.cad.phys schemas (incl. NURBS spline, interchange fields) + examples + cadjson.md status: completed
- id: cmd-expressions
content: Add Novolis.Commands.Expressions (function-call parser + tests) in novolis-commands; map regen; publish GPR status: completed
- id: studio-command-bar
content: Add StudioCommandBar to Novolis.Avalonia.Studio; publish GPR status: completed
- id: draft-scaffold
content: Scaffold DraftStudio WinExe (csproj, DI, StudioChrome, settings/workspace paths, slnx) status: completed
- id: draft-doc-commands
content: DraftDocument load/save .cadjson + IDraftCommand bus (Line/Circle/Rect/Spline/Box/Move/Delete/Undo/Redo) from DSL + tools status: completed
- id: draft-viewport
content: 2D DraftViewport (pan/zoom/grid/snap/select) + ToolController (incl. spline fit points) emitting same commands status: completed
- id: draft-3d
content: "Model view via Avalonia.Raylib: solids + sketch on ground plane; Fit/orbit" status: completed
- id: cadphys-export
content: Optional v1 export of .cadphys.json (tessellate primitives → meshes + matching colliders) status: completed
- id: draft-release
content: Installer/CI catalog + docs; nuget-only / project-ref verification status: completed isProject: false
Decisions (locked)
| Choice | Value |
|---|---|
| Product | **New sibling app** — Concept Studio stays as 3D blockout |
| Name / path | **Draft Studio** → [`novolis-apps/src/DraftStudio/`](novolis-apps/src/DraftStudio/) |
| Commands | **Both**: typed function-call DSL + mouse tools that emit the **same** command objects |
| CadKit | **None** (governance: apps compose Math + Avalonia + Commands) |
| Document formats | **`.cadjson`** (authoring, interchange-ready) + **`.cadphys.json`** (meshes + colliders) |
| Splines | **NURBS curves** on disk (`degree`/`knots`/`controlPoints`/`weights`); fit points optional authoring hint |
Product shape (v1)
In scope
- 2D drafting as primary mode (plan/XZ plane,
Vector3withY = 0per library-boundaries) - Sketch entities including spline (NURBS curve on disk; tool may collect fit points and bake control/knot data on commit)
- Light 3D model mode: solids (
Box,Cylinder,Sphere) + sketch entities drawn on the ground plane - Command bar: type
Line(0,0,1,0),Circle(0,0,5),Spline(...),Box(1,1,1),Undo,Delete, … - Interactive tools: Line / Circle / Rect / Spline (or type verb with no args) → prompt for points; finished gesture builds the same command as the typed form
- Grid + snap, pan/zoom, entity selection, undo/redo stack
- Persist as `.cadjson` under
%LocalAppData%\Novolis\Draft Studio\— format kept interchange-ready (not a minimal toy dump) - Export (or sidecar write) `.cadphys.json` for mesh + collider consumers
- StudioChrome shell, Inno/CI catalog entry like other apps
Out of scope (v1)
- Constraint solver, parametric history, CSG, NURBS surfaces / solids (curves yes; BREP/STEP writer no)
- Shipping DXF/DWG/glTF/STEP exporters in v1 (format must still carry enough data that a later converter is not guesswork)
- Serializing
CompiledScene/ BVH / GPU buffers - Sharing code with Concept Studio via ProjectReference
- Replacing Concept Studio
- Migrating Concept Studio off
concept.json(optional later; formats designed so it can)
flowchart LR
subgraph ui [DraftStudio Avalonia]
Bar[StudioCommandBar]
Tools[Mouse tools]
View2D[Draft viewport 2D]
View3D[Raylib model view]
end
subgraph pipe [Command pipeline]
Dsl[FunctionCallParser]
Cmd[DraftCommand]
Doc[CadDocument]
end
subgraph formats [On disk]
Cad[".cadjson novolis.cad"]
Phys[".cadphys.json novolis.cad.phys"]
end
Bar --> Dsl
Tools --> Cmd
Dsl --> Cmd
Cmd --> Doc
Doc --> View2D
Doc --> View3D
Doc --> Cad
Doc --> PhysDocument standards
Authoritative schemas live in `novolis-governance/schemas/cad/` (cross-repo contract; same role as registry schemas). Draft Studio implements load/save against them. No CadKit NuGet — optional thin C# DTOs stay app-local until a second consumer needs a shared package.
Shared conventions (both formats)
| Rule | Value | ||
|---|---|---|---|
| Encoding | UTF-8 JSON, camelCase, indented (match Concept Studio / workspace manifests) | ||
| Versioning | `format` string + `schemaVersion` int (start at `1`); bump version on breaking changes | ||
| Coordinates | Right-handed, **+Y up**, planar sketch on **XZ** (`y = 0`); document also records `handedness` + `forwardAxis` for exporters | ||
| Units | SI meters via `unitScaleMeters`; also explicit `linearUnit: "meter"` (DXF `$INSUNITS` mapping) | ||
| Angles | Radians in JSON; `angleUnit: "radian"` stated so converters do not guess degrees | ||
| Vectors | `number[3]` = `[x, y, z]` — never `{x,y,z}` objects, never `(x\ | y\ | z)` text |
| Quaternions | `number[4]` = `[x, y, z, w]` when used | ||
| Colors | `number[3]` RGB 0–1 or `number[4]` RGBA; optional `colorIndex` (ACI 1–255) for DXF round-trip | ||
| Ids | UUID strings (stable handles for xref / converter identity) | ||
| Transforms | Prefer TRS (`center`, `rotation`/`rotationY`, `scale`) over full matrices in authoring JSON | ||
| Extensibility | Every document and entity may carry `properties: { [key: string]: string \ | number \ | boolean }` for round-trip / app data |
Do not put in either file: CompiledScene, BVH, material runtime graphs, workspace manifests (workspace.json stays separate).
Interchange readiness (anti–data-poor)
v1 does not ship DXF/glTF/STEP writers, but the on-disk model must not force converters to invent geometry or frame conventions. Design bar:
| Target (later) | Data we keep now |
|---|---|
| DXF / LibreCAD | Layers with name + visibility/lock; entity `style` (linetype, lineWeightMm, color/colorIndex); arcs with normal + CCW angles; **spline as NURBS** (degree, knots, controlPoints, weights, closed); stable ids |
| glTF / mesh tools | `.cadphys` meshes with optional `normals` / `uvs`; winding; `space`; materials as named presets + RGB |
| Physics / engines | Explicit collider kinds + optional body mass/inertia; mesh refs separate from visuals |
| STEP / BREP (far) | Parametric solids keep analytic fields (`center`/`halfExtents`/`radius`/`height`) — never replace authoring with tessellation-only in `.cadjson` |
| Generic converters | Document `generator` (`name`, `version`); `createdAt` / `modifiedAt` (ISO-8601); `coordinateSystem` block; open `properties` bags |
Rule: Sketch/solids stay analytic in .cadjson. Tessellation is derived into .cadphys.json (or at draw time). Fit-point editing UI may exist, but commit to disk must store evaluable NURBS (or both fitPoints + derived NURBS), not only opaque polylines.
1. `novolis.cad` — file extension `.cadjson`
Purpose: Authoring description of 2D sketch + 3D solid objects (parametric / constructive), layers, styles, optional view state — rich enough for later interchange.
Top-level
{
"format": "novolis.cad",
"schemaVersion": 1,
"name": "Untitled",
"generator": { "name": "DraftStudio", "version": "2026.1.0" },
"createdAt": "2026-07-28T20:00:00Z",
"modifiedAt": "2026-07-28T20:00:00Z",
"unitScaleMeters": 1,
"linearUnit": "meter",
"angleUnit": "radian",
"coordinateSystem": {
"handedness": "right",
"upAxis": "y",
"forwardAxis": "z"
},
"layers": [
{ "id": "…", "name": "0", "visible": true, "locked": false, "color": [0.8, 0.8, 0.8] }
],
"linetypes": [
{ "name": "Continuous" },
{ "name": "Dashed", "pattern": [6, -3] }
],
"entities": [],
"camera": {
"yaw": 0.9,
"pitch": 0.35,
"distance": 40,
"target": [0, 1, 0]
},
"properties": {}
}Entity common fields: id, name?, layerId?, parentId?, kind, optional style, optional properties.
`style` (CAD stroke metadata):
| Field | Type | Notes |
|---|---|---|
| `linetype` | string | Name from document `linetypes` (default `Continuous`) |
| `lineWeightMm` | number | Plot weight; `0` = default |
| `color` | `number[3]` | Entity override |
| `colorIndex` | int? | Optional ACI for DXF |
Entity kinds
| `kind` | Required fields | Notes |
|---|---|---|
| `group` | — | Hierarchy only |
| `line` | `a`, `b` | Sketch; typically `y=0` |
| `polyline` | `points: number[3][]`, `closed: bool` | Optional `bulges: number[]` (DXF LWPOLYLINE bulge per segment) |
| `circle` | `center`, `radius`, `normal?` (default `[0,1,0]`) | |
| `arc` | `center`, `radius`, `startAngle`, `endAngle`, `normal?` | Radians, CCW about `normal` |
| `rect` | `center` or `min`/`max`, `halfExtents` or corners | Axis-aligned in plane |
| `spline` | see below | **NURBS curve** (interchange-grade) |
| `box` | `center`, `halfExtents`, `rotationY?` or `rotation?` | Solid |
| `sphere` | `center`, `radius` | |
| `cylinder` | `center`, `radius`, `height`, `rotationY?` | Axis +Y local |
| `cone` | `center`, `radius`, `height`, `rotationY?` | |
| `wedge` | `center`, `halfExtents`, `rotationY?` | Concept-compatible |
Optional visual on solids: material (preset string), color: number[3].
`spline` entity (NURBS curve)
Aligns with DXF SPLINE / STEP curve data so exporters do not approximate from polylines.
| Field | Required | Notes |
|---|---|---|
| `degree` | yes | Integer ≥ 1 (cubic = 3 typical) |
| `controlPoints` | yes | `number[3][]` |
| `knots` | yes | Nondecreasing; length = `controlPoints.length + degree + 1` (clamped open) |
| `weights` | no | Same length as control points; default all `1` (B-spline). Non-1 → rational NURBS |
| `closed` | no | Default `false` |
| `periodic` | no | Default `false` |
| `fitPoints` | no | Authoring/edit hints; **not** a substitute for control/knot data on disk |
| `normal` | no | Plane hint for planar splines; default `[0,1,0]` |
UI may collect fit points (Spline tool), then on commit compute degree/controlPoints/knots/weights (or store both). Display may tessellate; file of record remains NURBS.
Example sketch + spline + solid
{
"format": "novolis.cad",
"schemaVersion": 1,
"name": "Hull blockout",
"generator": { "name": "DraftStudio", "version": "2026.1.0" },
"linearUnit": "meter",
"angleUnit": "radian",
"unitScaleMeters": 1,
"coordinateSystem": { "handedness": "right", "upAxis": "y", "forwardAxis": "z" },
"layers": [{ "id": "a0000000-0000-4000-8000-000000000001", "name": "0", "visible": true, "locked": false }],
"linetypes": [{ "name": "Continuous" }],
"entities": [
{
"id": "a0000000-0000-4000-8000-000000000010",
"kind": "line",
"layerId": "a0000000-0000-4000-8000-000000000001",
"name": "keel",
"style": { "linetype": "Continuous", "lineWeightMm": 0.25 },
"a": [0, 0, 0],
"b": [0, 0, 12]
},
{
"id": "a0000000-0000-4000-8000-000000000012",
"kind": "spline",
"name": "sheer",
"degree": 3,
"closed": false,
"controlPoints": [[0, 0, 0], [2, 0, 3], [2, 0, 9], [0, 0, 12]],
"knots": [0, 0, 0, 0, 1, 1, 1, 1],
"weights": [1, 1, 1, 1],
"fitPoints": [[0, 0, 0], [2, 0, 6], [0, 0, 12]]
},
{
"id": "a0000000-0000-4000-8000-000000000011",
"kind": "box",
"name": "hull",
"center": [0, 1, 6],
"halfExtents": [2, 1, 6],
"material": "hull",
"color": [0.72, 0.35, 0.28]
}
]
}Maps to: Draft Studio document; Concept Studio ConceptDocument is a subset of solids/groups (migration path later). Tessellation stays at compile time for rendering / phys export.
Schema file: `novolis-governance/schemas/cad/novolis.cad.schema.json`
2. `novolis.cad.phys` — file extension `.cadphys.json`
Purpose: Extension of novolis.cad that adds explicit triangle meshes and colliders for rendering/physics consumers. Same entity tree may be inlined (superset document) or referenced.
Extension rules
- Every
.cadphys.jsonis a validnovolis.caddocument plus required extension fields below (sameentities/layers/ header conventions). formatmust be"novolis.cad.phys"(not"novolis.cad").schemaVersionis independent per format (both start at1).- Optional
baseDocument: relative path to a.cadjsonwhen the phys file is a sidecar that only carries meshes/colliders + entity id bindings (entities may be omitted ifbaseDocumentis set; loaders merge).
Additional top-level
{
"format": "novolis.cad.phys",
"schemaVersion": 1,
"name": "Hull blockout",
"unitScaleMeters": 1,
"upAxis": "y",
"baseDocument": "hull.cadjson",
"meshes": [],
"colliders": [],
"entities": []
}`meshes[]` — maps 1:1 toward `TriangleMesh` / StaticTriangleMesh (and later glTF primitives)
| Field | Type | Notes | |
|---|---|---|---|
| `id` | string UUID | ||
| `name?` | string | ||
| `entityId?` | string | Visual/authoring entity this mesh realizes | |
| `vertices` | `number[3][]` | World or local; see `space` | |
| `indices` | `number[]` | Length % 3 == 0; triangle list | |
| `normals?` | `number[3][]` | Per-vertex; length = vertices (glTF-ready) | |
| `uvs?` | `number[2][]` | Per-vertex; **exception**: UV is 2D param space, not Math stack — allowed only in phys mesh payload | |
| `winding` | `"ccw"` \ | `"cw"` | Default `"ccw"` |
| `space` | `"local"` \ | `"world"` | Default `"local"`; if local, apply entity TRS |
| `material?` | string | Preset name for glTF/material mapping |
`colliders[]` — shapes separate from visual mesh (may share mesh id)
| Field | Type | Notes | |||
|---|---|---|---|---|---|
| `id` | string UUID | ||||
| `entityId?` | string | Bind to authoring entity | |||
| `kind` | string | `box` \ | `sphere` \ | `capsule` \ | `triangleMesh` |
| `center?` / `halfExtents?` | for `box` | Match Math AABB semantics | |||
| `radius?` | for `sphere` / `capsule` | ||||
| `a?` / `b?` | for `capsule` | Endpoints `number[3]` | |||
| `meshId?` | for `triangleMesh` | Ref into `meshes[]` | |||
| `isTrigger?` | bool | Default false | |||
| `body?` | object | Optional rigid-body seed: `mass`, `inertiaDiagonal: number[3]`, `kinematic: bool` — aligns with physics `RigidBodyState` fields without inventing numerics |
Example (sidecar style)
{
"format": "novolis.cad.phys",
"schemaVersion": 1,
"name": "Hull blockout",
"unitScaleMeters": 1,
"upAxis": "y",
"baseDocument": "hull.cadjson",
"meshes": [
{
"id": "b0000000-0000-4000-8000-000000000001",
"entityId": "a0000000-0000-4000-8000-000000000011",
"name": "hull-mesh",
"space": "local",
"winding": "ccw",
"vertices": [[-2, -1, -6], [2, -1, -6], [2, 1, -6], [-2, 1, -6]],
"indices": [0, 1, 2, 0, 2, 3]
}
],
"colliders": [
{
"id": "c0000000-0000-4000-8000-000000000001",
"entityId": "a0000000-0000-4000-8000-000000000011",
"kind": "box",
"center": [0, 1, 6],
"halfExtents": [2, 1, 6],
"body": { "mass": 1, "inertiaDiagonal": [1, 1, 1], "kinematic": false }
}
]
}Pipeline: .cadjson → (tessellate / export) → .cadphys.json → Math TriangleMesh + Physics colliders. Never write CompiledScene.
Schema file: `novolis-governance/schemas/cad/novolis.cad.phys.schema.json`
Also add short doc: `novolis-governance/docs/cadjson.md` linking both schemas + examples under schemas/cad/examples/.
flowchart TB
Cad[".cadjson novolis.cad"]
Phys[".cadphys.json novolis.cad.phys"]
TM[TriangleMesh]
Col[Colliders]
Scene[SceneBuilder then CompiledScene]
Cad -->|"extends / tessellate"| Phys
Phys --> TM
Phys --> Col
Cad -->|"parametric tessellate in app"| Scene
TM -->|"optional MeshInstance"| SceneWhere fundamentals go
| Concern | Home | What to add |
|---|---|---|
| Format contracts | **`novolis-governance/schemas/cad/`** + `docs/cadjson.md` | JSON Schema + examples; no CadKit package |
| `Name(arg,…)` parsing | **`novolis-commands`** → `Novolis.Commands.Expressions` | Pure function-call parser → bind to app commands |
| Command bar UI | **`novolis-avalonia`** → `Novolis.Avalonia.Studio` | `StudioCommandBar` (events only) |
| Geometry primitives | **Reuse** Math Topology/Geometry; **add** thin NURBS curve evaluate/tessellate helper in `Novolis.Math.Geometry` if missing (degree/knots/controls → polyline samples) — no CadKit | |
| 2D CAD canvas | **App-local first** | Avalonia `DrawingContext` viewport |
| 3D preview | Existing Avalonia.Raylib packages | Thin app renderer |
| Document / tools / undo / export | **`DraftStudio`** | Load/save `.cadjson`; export `.cadphys.json` |
App architecture
Scaffold mirrors Concept Studio bootstrap under src/DraftStudio/:
Models/— DTOs matchingnovolis.cad/ optional phys export DTOsCommands/—IDraftCommand+ bus; factories from DSL and toolsServices/DraftSession.cs— load/save.cadjsonServices/CadPhysExporter.cs— tessellate solids → meshes + default matching collidersServices/DraftSceneBridge.cs— document → Raylib / 2D paintUi/DraftViewport.cs,Ui/ToolController.cs,MainWindow.cs
Command surface (v1)
| Typed | Interactive | Effect |
|---|---|---|
| `Line(x1,z1,x2,z2)` | Line tool: 2 clicks | Add line entity |
| `Circle(cx,cz,r)` | Circle: center + radius | Add circle |
| `Rect(x1,z1,x2,z2)` | Rect: 2 corners | Add rect |
| `Spline(...)` / fit-point form | Spline tool: N clicks + Enter | Add NURBS spline (bake knots/controls on commit) |
| `Box(w,h,d)` / `Box(x,y,z,w,h,d)` | optional | Add box |
| `Move(dx,dy,dz)` | drag selected | Translate |
| `Delete` / `Undo` / `Redo` / `Fit` | hotkeys | Stack / view |
Ambiguous Line / Spline with no args → enter tool. Parser errors → StudioFeedback.
Packaging and publish order
- Land schemas +
cadjson.mdin novolis-governance (no NuGet required for contracts) Novolis.Commands.Expressions→ map regen → GPRStudioCommandBar→ GPR- DraftStudio app + installer/CI/docs (PackageReference only)
- Policy checks:
verify-nuget-only.ps1+verify-project-ref-mode.ps1
Explicit non-goals for this plan
- Editing Concept Studio’s REPL or forcing migration off
concept.jsonin v1 - Shipping DXF/DWG/glTF/STEP exporters in v1 (schemas stay converter-ready)
- NURBS surfaces / BREP / boolean solids
- In-repo shared library between DraftStudio and ConceptStudio
- Formal C# shared DTO package until a second product needs it
- Storing sketch geometry as tessellated polylines only (forbidden — use analytic + NURBS)