3draft 26.11.2005 → 26.11.4010

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/APP.md ADDED
@@ -0,0 +1,121 @@
1
+ # 3Draft viewer: how to use it
2
+
3
+ The viewer is the human side of 3Draft: a person and an AI look at, and edit, the same scene. The engine is described in the [README](README.md); this page is only about the app.
4
+
5
+ ```sh
6
+ npm run app # http://localhost:5173/app/ (repo only, not in the npm package)
7
+ ```
8
+
9
+ It is one file, `app/index.html`, plus a dependency-free static server (`app/serve.js`, localhost only). The same page is published as a Claude artifact, where the scene lives in a shared database. When the MCP server serves it, everything the AI builds appears live.
10
+
11
+ ## The view
12
+
13
+ - **Starting view: WebGPU.** If WebGPU is unavailable, or the GPU device is lost, the app falls back to SVG by itself and says so in the footer (`G` or the `WebGPU` button switches by hand). SVG shows colour and transparency only; WebGPU also shows metal, roughness and emission. The **filmic look** (`C`) adds linear light, Fresnel and tone mapping, so highlights roll off softly and `lamp`-like materials glow; switch it off for the plain flat look.
14
+ - **The drawing fills the stage.** On a desktop it covers the whole area; on a phone it is a little larger than the short side.
15
+ - **Drag** rotates, **double click** resets the view, the arrow keys rotate too.
16
+ - **The cross** follows the pointer and lies on the surface under it. The cursor is always a cross. On a phone, with no mouse, it aims at the middle.
17
+ - **Footer:** yaw, pitch, fly speed, which code answers picking (`pick hexaw` = the WebAssembly kernel), object count, live link, and the face under the cross.
18
+
19
+ ## Keys
20
+
21
+ | key | does |
22
+ | --- | --- |
23
+ | `w a s d r f` | fly: forward, left, back, right, up, down. Every press is a speed step; the opposite key slows down |
24
+ | `Space` | stop moving (all speeds to zero) |
25
+ | `T` | fly to the object under the cross. Over nothing, or when too close or inside a model: out to the nearest object |
26
+ | `/` | show only the object under the cross; again: the whole scene, and the camera flies back to where it was |
27
+ | `M` | place a prism on the surface under the cross, or shoot one into empty space. **In isolated mode (`/`) it fires a bullet** instead: a prism that starts at the camera point and flies along the ray through the cursor, so its long axis is always the view direction and you always see its back; if it hits the isolated object it sinks half its length and carves that part out (A − B, the cut is made of the selected material, `U` undoes it); if it misses it flies on to 4 times the camera-to-model distance and vanishes. Only the closed parts of the model are carved; open parts are ignored (see the health check below). A carve never leaves a crack: if the result would not be a closed solid it is retried with a hair's shift, and if that fails too the model stays as it was (the note "left as it was" shows). |
28
+ | `Tab` | edit mode on the object under the cross / off (see below) |
29
+ | `V` | Spider bar (in edit mode: draw a line): the first `V` remembers a surface point, the second joins the two with a thin prism (a quarter of the base of a placed one) made of the selected material. `V` over empty space forgets the first point |
30
+ | `X`, `Delete` | delete the object under the cross |
31
+ | `U`, `Backspace` | undo: placing, deleting, painting, booleans and moves, one step at a time |
32
+ | `P` | show / hide the material palette |
33
+ | `I` | pick material: the material of the face under the cross becomes the selected one |
34
+ | `Shift` + press | paint the face under the cross with the selected material (palette must be on) |
35
+ | `Shift` + double click | paint the whole object |
36
+ | `O` | boolean (see below) |
37
+ | `H` | move gizmo (see below) |
38
+ | `E` | edges and points on / off (also the `Edges` button) |
39
+ | `C` | filmic look on / off (also the `Filmic` button; on at the start, WebGPU view only) |
40
+ | `G`, `L` | SVG / WebGPU, turn the light |
41
+ | `?` | show / hide these shortcuts under the view (hidden at the start; also the `Keys` button) |
42
+
43
+ ## Clear and example models
44
+
45
+ - **`Clear`** (footer) removes every object in the scene. It is one undo step: `U` brings them all back.
46
+ - **`+ Example…`** (the drop-down in the footer) adds a ready-made model to the scene: the house (the Blender import, 45 objects), the Cessna, the stone bridge or the table. It is placed beside what is already there, on its +x side, and the view frames everything. One `U` removes the whole example again. Examples are normal objects after that: paint, move, boolean and delete them like any other.
47
+
48
+ ## Moving an object: the gizmo (`H`)
49
+
50
+ There is one gizmo at a time, and `H` cycles through three states:
51
+
52
+ 1. **First `H`**: the gizmo appears on the object under the cross, on the world axes (x red, y green, z blue).
53
+ 2. **Second `H`**: it turns to the object's own axes. Objects carry no transform, so these come from the shape: y stays up, x and z follow the turn around y that fits the footprint best (an object that already lines up with the world axes looks the same in both states).
54
+ 3. **Third `H`**: the gizmo goes away.
55
+
56
+ Drag one of the arrows to slide the object along that axis; the arrow thickens under the pointer. While dragging only the picture changes; one write goes to the database when you let go, and one `U` takes the whole drag back.
57
+
58
+ ## Edit mode: drawing on a model (`Tab`)
59
+
60
+ The idea: there is a base model, and you refine or extend it by drawing lines on it. Put the cross on an object and press `Tab`. A red bar at the top says which object is being edited. Its vertices show as small grey crosses.
61
+
62
+ - **`V` draws a line.** The first `V` takes a point, the next `V` joins it to the new one, and the chain goes on from there. A point is either an existing vertex (the pointer **snaps** to vertices; a white cross shows which) or a new vertex put on the surface under the cross. On a face it splits the face into triangles, on an edge it goes into both faces, so the surface never cracks.
63
+ - **Two points on one face cut it.** The line becomes a real edge of the mesh: the surface gets detail exactly where you drew.
64
+ - **Points in the air extend the model.** With a line in progress, `V` over empty space puts the next point on the plane that faces the camera through the last point. So you can draw a new part out from the model, and lines that end on existing vertices are attached to it.
65
+ - **`Enter` closes a loop into a face.** The shortest closed loop (up to 8 lines) through the last line becomes a face with the selected material, wound away from the middle of the object. A loop of 4 or more becomes triangles, so faces stay flat.
66
+ - **Nothing moves by default** (and never while you draw with `V`). Moving is Blender-style, with a key per mode, and every drag is one `U` step:
67
+ - `1` **move points:** hover a vertex (white cross), drag it.
68
+ - `2` **move lines:** hover an edge or a line (white), drag it; both ends move.
69
+ - `3` **move faces:** hover a face (white outline), drag it; all its vertices move together and the face stays flat.
70
+ - The drag slides in the plane that faces the camera. A selected point, line or face (orange) also gets a gizmo: drag an arrow to move along one axis. Everything that uses the moved vertices follows: lines, faces and the base surface, so this is how you reshape the original model. A polygon that gets only some of its vertices moved turns into triangles.
71
+ - `V` goes back to drawing (and drops the selection); `Esc` does the same.
72
+ - **`B` turns the spider bars into lines.** The prisms made with `V` in object mode (they are objects named `bar`) become lines on the edited object: each end snaps to a vertex that is close, or is put on the surface if it is near it, or stays a free point. The bars are removed (one `U` brings them back). So a plan sketched as bars can be worked on as a wire.
73
+ - **`X`** removes the lines at the selected point or line (or the last line); the points stay. `U` undoes every step, one key press at a time.
74
+ - **`Esc`** stops the line you are drawing; then it leaves a move mode back to drawing; then it leaves edit mode (so does `Tab`). The lines are only shown in edit mode.
75
+
76
+ The lines live next to the mesh as `wire: [[a, b], ...]` (vertex pairs) in the object's document, so the AI sees them too. The geometry is in the engine (README part 1): `insertVertex`, `cutFace`, `moveVertex`, `moveVertexSet`, `fillLoop`, `addLine`, `findLoop`.
77
+
78
+ Limits for now: a face made from a non-flat or non-convex loop is only as good as its triangle fan, there is no mirror yet, and the lines are not deformation weights (a surface does not bend around a moved vertex, it just follows the vertices that are in the mesh).
79
+
80
+ ## Boolean operations (`O`)
81
+
82
+ Press `O` over an object: it gets an orange box. Press `O` over another one (green box): a small question asks which operation to run.
83
+
84
+ | key | operation |
85
+ | --- | --- |
86
+ | `1` | union |
87
+ | `2` | A − B (A is the orange one) |
88
+ | `3` | B − A |
89
+ | `4` | intersect |
90
+ | `5`, `Esc` | cancel |
91
+
92
+ The result is a new object named like `A-B` and it replaces both operands; `U` brings the two originals back and removes the result. `O` over empty space forgets a marked object.
93
+
94
+ The math is the engine's `boolean(a, b, op)` (README, part 1). Only closed parts are calculated: a part that is not a closed solid (a surface, a box without a bottom) is left out, and a union keeps it as it is. The status line under the picture is the **health check** of the object under the cross: `solid`, `N of M parts closed · K faces open`, or `no closed part`. Each object needs at least one **closed solid with outward faces**; with none, the reason shows in the dialog. With messy data (coincident faces) the result can come out open; the engine's `validate` says so.
95
+
96
+ ## Materials (`P`)
97
+
98
+ Twelve starting materials: plaster, sand, wood, dark wood, concrete, slate, grass, brick, steel, gold, glass, lamp. Pick one, then `Shift`+press to paint a face or `Shift`+double click for the whole object.
99
+
100
+ **Pick material (`I`, or the `pick` button):** the eyedropper. `I` takes the material of the face under the cross. The `pick` button arms it, and the next click on a surface takes it (`Esc` cancels). The palette opens and that material is selected, so you can read its sliders or paint with it right away. A face that has only a colour selects the material with that colour, or starts a new material from it.
101
+
102
+ For the selected material there are four sliders (`metal`, `rough`, `alpha`, `emit`, 0 to 5) and a colour; `+ new` copies it as a new material. A painted face stores the material **name**, so editing a material changes every face made of it.
103
+
104
+ ## Isolating, travelling
105
+
106
+ `/` hides everything but the object under the cross and frames it. The camera pose from before is remembered and restored when you leave. `T` flies the camera to the object under the cross (nothing under it: out to the nearest object, which also gets you out of a model you are inside).
107
+
108
+ ## Phone
109
+
110
+ Four round buttons cover the main actions: travel, place, delete, and the view mode. Drag rotates; the cross aims at the middle of the screen.
111
+
112
+ ## One object model and the database
113
+
114
+ Objects that come from outside (the database, the MCP server) and objects made in the app are the same thing: `{ id, name, mesh }`. Painting, deleting, isolating, booleans, moving and undo work on all of them, whoever made them. With the artifact database every change is written back, so the AI sees what the person did.
115
+
116
+ Collections:
117
+
118
+ - `objects`: one document per object, `{ name, mesh }`, at most 256 KiB each.
119
+ - `palette`: one document per material, `{ color, metal, rough, alpha, emit, order }`.
120
+
121
+ Undo is a per-session stack in the page; it does not reach back past a reload.
package/README.md CHANGED
@@ -2,254 +2,123 @@
2
2
 
3
3
  Minimalist AI first 3D engine.
4
4
 
5
- > Status: **pre-alpha**. The `0.0.x` releases reserve the package name and
6
- > pin down the direction; the engine itself is built step by step in this repo.
5
+ > Status: **pre-alpha**. The engine is built step by step in this repo.
7
6
 
8
7
  ```sh
9
8
  npm install 3draft
10
9
  ```
11
10
 
12
- ## Usage (v0.1: points, edges, flat faces, SVG)
11
+ The README has three parts: [1. the engine](#1-the-engine), [2. the viewer app](#2-the-viewer-app) and [3. how the engine and an LLM work together](#3-engine-and-llm).
12
+
13
+ ---
14
+
15
+ ## 1. The engine
16
+
17
+ A model is plain data, so a human, a program and an LLM can all read and write it:
13
18
 
14
19
  ```js
15
- import { writeFileSync } from 'node:fs';
16
- import { box, merge, renderSheet, renderViews } from '3draft';
20
+ const mesh = {
21
+ positions: [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], // Vec3[], y is up
22
+ faces: [{ v: [0, 1, 2, 3], color: '#c8a27a', mat: 'wood' }], // CCW seen from outside, planar, convex
23
+ };
24
+ ```
25
+
26
+ A scene is a list of objects `{ id, name, mesh }`. One mesh can hold many differently coloured faces (`examples/cessna.js` is one mesh with 9 colours).
27
+
28
+ ### Rendering
29
+
30
+ ```js
31
+ import { box, merge, renderSheet, renderView, orbit, createGpu } from '3draft';
17
32
 
18
33
  const table = merge(
19
34
  box({ size: [2, 0.15, 1.2], at: [0, 0.925, 0], color: '#c8a27a' }),
20
35
  box({ size: [0.15, 0.85, 0.15], at: [-0.85, 0.425, -0.45], color: '#8a6a4a' }),
21
- // ...more legs
22
36
  );
23
-
24
- writeFileSync('table.svg', renderSheet(table)); // 2x2 sheet
25
-
26
- for (const { name, svg } of renderViews(table)) {
27
- // one after another: 'front', 'side', 'top', 'persp'
28
- writeFileSync(`table.${name}.svg`, svg);
29
- }
37
+ renderSheet(table); // SVG: front, side, top, persp on one sheet
38
+ renderView(table, 'persp', { occlude: true }); // one view, only what is seen
39
+ const gpu = await createGpu(canvas); // WebGPU, same camera, same pixels
40
+ gpu.render(table, orbit({ yaw: -0.6, pitch: 0.45 }), { size: 600 });
30
41
  ```
31
42
 
32
- `npm run example` writes the table above to `examples/out/`:
33
-
34
43
  ![table sheet](examples/out/table.sheet.svg)
35
44
 
36
- Views: `front` (eye on +z), `side` (eye on +x), `top` (eye on +y), all
37
- orthographic with the same scale, and a perspective `persp` view. Every
38
- `<polygon>` carries `data-face` and `data-depth`, every `<circle>` carries
39
- `data-v`, so the output can be inspected as plain text.
45
+ - **SVG** is the deterministic preview (the same calls give the same text). Every `<polygon>` has `data-face` and `data-depth`, every `<circle>` has `data-v`. `occlude: true` paints into a small depth buffer first and sends only the faces and points that are seen (on the 5000-face house: 2793 -> about 680 polygons, 802 KB -> 175 KB, same picture).
46
+ - **WebGPU** draws the same scene through the same camera (`viewProjection` is tested to hit the same pixels as `project`), with one shader, one light, real alpha blending (see-through faces in a second pass, sorted far to near) and instancing.
47
+ - Cameras: `front`, `side`, `top` (orthographic) and `persp`; `orbit({ yaw, pitch })` is a turntable; any `{ dir, up, ortho }` works.
48
+ - Faces are painter-sorted with plane tests; interpenetrating solids can show artifacts, hidden edges are not drawn, back-face culling assumes closed CCW meshes.
40
49
 
41
- ### Surfaces: what is under the cursor
50
+ ### Materials
42
51
 
43
- The engine senses surfaces, not just pixels. `pick` casts a real ray from a
44
- screen pixel into the geometry and returns the face, the hit point, the
45
- surface normal and the distance. `crosshair` builds a target cross that lies
46
- flat on that surface (perpendicular to its normal), aligned to the face, so it
47
- turns with the object. Marks are drawn on top by `renderView`.
52
+ The palette is data: `{ name: { color, metal, rough, alpha, emit } }`. A face points at an entry with `face.mat`; its `color` stays as the fallback. Editing an entry changes every face made of it.
48
53
 
49
- ```js
50
- import { makeCamera, pick, crosshair, renderView } from '3draft';
51
-
52
- const cam = makeCamera('persp', mesh, 400);
53
- const hit = pick(mesh, cam, x, y); // { face, point, normal, distance, color } or null
54
-
55
- const marks = hit ? [{ name: 'cursor', lines: crosshair(mesh, hit, 0.08) }] : [];
56
- const svg = renderView(mesh, 'persp', { marks });
57
- ```
54
+ | field | meaning | drawn as |
55
+ |---|---|---|
56
+ | `metal` 0..1 | paint ... metal | reflects sky and ground, tinted; strong highlight |
57
+ | `rough` 0..1 | mirror ... matte | widens the highlight, blurs the reflection |
58
+ | `alpha` 1..0 | solid ... invisible | blended in a second pass, far to near (SVG: `fill-opacity`, and a see-through face hides nothing) |
59
+ | `emit` 0..5 | lit ... glowing | up to 1 the colour fades to unshaded; above 1 (with `tonemap`) it is a light source, that many times brighter |
58
60
 
59
- Picking uses the geometry, not the painter's drawing order, so it stays exact
60
- where the drawing has artifacts. `ray(cam, x, y)` is the inverse of
61
- `project`. The viewer shows the cross under the pointer and reads out the face
62
- and its object-space normal.
61
+ Pass the palette to the renderers: `renderView(mesh, view, { palette })`, `gpu.render(mesh, view, { palette })` (a new object when an entry changes). SVG shows colour and alpha only.
63
62
 
64
- Any camera works: `renderView(mesh, { dir, up, ortho, name })` takes a custom
65
- view, `rotate(mesh, { yaw, pitch })` turns the mesh, and `{ fit: bounds(mesh) }`
66
- keeps the framing fixed while it moves.
63
+ `gpu.render(mesh, view, { tonemap: true })` switches on a filmic look: lighting in linear colour, HDR light, Fresnel on the edges, ACES tone mapping, and `emit` that really glows above white (`tonemap: { exposure, emit }` to tune; off by default, so the plain look stays the same).
67
64
 
68
- ### Placing things
65
+ ### Surfaces, picking, editing
69
66
 
70
- `orbit({ yaw, pitch })` is a turntable as a camera, so the scene stays in object
71
- space and picks stay valid. `standOn(mesh, hit, { base, height, color })`
72
- returns a prism standing on the picked surface along its normal (height
73
- defaults to 3 x base). In the viewer, Space puts one down under the cross and
74
- Backspace takes the last one back. Over empty space Space shoots the prism out under the cursor to a random distance, animated, flying exactly along the camera axis (it lies along that axis like a bullet). The viewer starts empty (no table) and lays out only what you place. X deletes the prism under the cross. V is the Spider bar: the first V remembers the surface point under the cross, the second joins the two points with a thin prism (half the base of a shot prism, a darker brown); V over empty space forgets a pending first point. T flies the camera straight to the front of the object under the cross without turning it; T over nothing sets the speed to zero. W A S D R F fly: every press is a speed step along that axis (the speed grows faster than the level: 1, 2.7, 5.6, 9.9 ...) (w w w = speed 3 forward, s s s brings it back to a stop), R and F go up and down. While Shift is held the view turns towards the pointer, faster the farther the pointer is from the middle. H toggles edges and points. T flies the camera, animated, to the object under the cross and in front of the surface you point at (the whole scene is framed again when T is pressed over nothing, or with Reset view). Prisms get a random wood brown. On a phone or tablet the stage fills the screen with no text: drag orbits, and four round translucent buttons do T, Space, D and the SVG/WebGPU switch (the cross starts in the middle and stays where you last touched).
67
+ - `pick(mesh, camera, x, y)` casts a real ray: face, point, normal, distance. `ray` is the inverse of `project`; `crosshair` builds a cross lying on the surface; `standOn` returns a prism standing on a picked surface.
68
+ - `validate(mesh)` returns text notes: closed shell, winding, planar and convex faces. `extrudeFace`, `moveVertices`, `extrude`, `rotate`, `merge`, `bounds` shape meshes.
69
+ - `boolean(a, b, op)` (also `union`, `subtract`, `intersect`) is solid modelling on BSP trees: faces are split by planes, so the result stays convex and planar. Both operands must be closed with outward faces; an open shell (a wall without thickness) is refused with the reason. With `{ strict: false }` the parts that are not closed solids are left out of the calculation instead (a union keeps them as they are, a subtract keeps those of `a`); `health(mesh)` is the check behind it: the connected parts, which are closed, and how many faces a boolean can work with. Face colours and materials are kept; cut faces take the colour of the cutting solid. The result is welded and T-junctions are repaired. A solid stays a solid: a result that is not a closed, consistently wound solid (numerical trouble with coincident faces) is thrown away and the operation is repeated with the second operand nudged by a tiny amount; if no try is sound it throws and the caller keeps the original.
70
+ - **Drawing on a mesh** (`wire.ts`): `insertVertex(mesh, face, point)` puts a vertex on a surface (inside a face it makes a triangle fan, on an edge it joins both faces), `cutFace(mesh, a, b)` cuts a face along a line between two of its vertices, `moveVertex(mesh, i, to)` moves one (polygons that use it become triangles, so faces stay planar), `fillLoop(mesh, loop)` closes a loop of vertices into a face, `addLine` and `findLoop` keep the wire (a list of vertex pairs). These are what the viewer's edit mode (`Tab`, see [APP.md](APP.md)) uses.
71
+ - **HEXAW kernel.** The picking hot loop is also written in [HEXAW](https://github.com/Pengeszikra/hexaw) (`kernel/pick.hexaw`, 569 bytes of WebAssembly). `initKernel()` starts it; `kernel.pick` returns exactly what `pick` returns, 0.14 ms instead of 1.7 ms on 7200 faces. The module is embedded as base64 (`src/kernel-bytes.ts`), so the engine needs neither HEXAW nor a fetch at runtime. `npm run kernel` re-assembles it.
75
72
 
76
- ### WebGPU view
73
+ ### TypeScript and `|>`
77
74
 
78
- `createGpu(canvas)` returns a WebGPU renderer that draws the same scene through
79
- the same camera as the SVG renderer, so picking, framing and the cross marks stay
80
- valid. Faces are flat colored (the face color is the albedo) and lit by one
81
- material (`ambient`, `diffuse`, `specular`, `shininess`) and one global light
82
- (hemisphere ambient plus one directional light):
75
+ The source is TypeScript (`src/*.ts`) with the F# style pipeline operator, `value |> fn` ([why](https://dev.to/pengeszikra/pipeline-operator-great-again-1cbn)). `npm run build` rewrites every `|>` into plain calls (`scripts/pipe.mjs`: `a |> f |> g(1)` becomes `g(1)(f(a))`) and lets `tsc` check and emit `dist/`, which is committed, so a clone runs without a build.
83
76
 
84
- ```js
85
- import { createGpu, orbit } from '3draft';
86
- const gpu = await createGpu(canvas); // rejects when WebGPU is missing
87
- gpu.render(mesh, orbit({ yaw, pitch }), {
88
- size: 600, // CSS px; the canvas is made size x size x dpr
89
- fit, // keep the framing fixed, as with SVG
90
- light: { direction: [0.5, 0.8, 0.6] },
91
- material: { diffuse: 0.8 },
92
- });
77
+ ```ts
78
+ const dir = spec.dir |> norm |> times(dist) |> plus(center); // the right side is a function
93
79
  ```
94
80
 
95
- `viewProjection(camera)` is the matrix behind it; it is unit tested to land every
96
- point on the same pixel as `project()`. `gpu.pixels(...)` draws the same frame
97
- offscreen and reads it back as RGBA.
81
+ Put arrow functions in parentheses (`x |> ((v) => v + 1)`) and wrap a pipeline in parentheses next to `+` and `-`.
98
82
 
99
- ### MCP server: let an AI build the models
83
+ ---
100
84
 
101
- `mcp/server.js` is an MCP server (stdio, no dependencies) that holds a scene of
102
- named objects and edits it through tool calls. The AI looks at the SVG preview,
103
- fixes what is wrong and renders again, so it never needs a screenshot.
85
+ ## 2. The viewer app
104
86
 
105
87
  ```sh
106
- # Claude Code
107
- claude mcp add 3draft -- node /absolute/path/to/3Draft/mcp/server.js
88
+ npm run app # http://localhost:5173/app/ (repo only, not in the npm package)
108
89
  ```
109
90
 
110
- ```json
111
- { "mcpServers": { "3draft": { "command": "node", "args": ["/absolute/path/to/3Draft/mcp/server.js"] } } }
112
- ```
91
+ A single `app/index.html` plus a dependency-free static server. Person and AI work on the same scene: objects loaded from outside and objects made in the viewer are one model, `{ id, name, mesh }`. WebGPU view with an SVG fallback, a cross cursor on the surface, flying, materials, booleans, a move gizmo, isolate, travel, undo. It is also published as a Claude artifact whose shared database is the scene.
113
92
 
114
- (that JSON goes into `claude_desktop_config.json` for Claude Desktop.)
115
-
116
- **Live viewer.** The same process serves the viewer and pushes every change to it
117
- (Server-Sent Events). Ask the AI for the `viewer` tool's URL (default
118
- `http://localhost:5173/app/`, next free port if that is taken, `3DRAFT_PORT` to
119
- change it, `3DRAFT_LIVE=0` to turn it off), open it in a browser and watch the
120
- model grow while the AI builds. The view re-frames to the scene; the objects are
121
- read-only in the viewer, but you can still rotate, switch to WebGPU, and place or
122
- delete your own prisms on top. Loopback only, other `Host` headers are refused.
123
-
124
- | tool | what it does |
125
- | --- | --- |
126
- | `add_box`, `add_extrusion`, `add_mesh` | boxes, prisms from a convex footprint, raw faces |
127
- | `transform`, `recolor`, `remove`, `clear` | scale / rotate Y / move, colors, deleting |
128
- | `get_mesh`, `extrude_face`, `move_vertices` | shape an object: push, taper, inset or dent a face, move vertices |
129
- | `list`, `inspect` | objects and bounds; closed shell, winding, planar and convex faces, as text notes |
130
- | `render` | SVG: a 2x2 sheet (front, side, top, persp) or one view, also `orbit` with yaw/pitch |
131
- | `pick` | pixel of a render -> object, face, 3D point, normal |
132
- | `export`, `import` | scene as JSON, optionally a `.json` file under the working directory |
133
- | `viewer` | URL of the live viewer |
93
+ **How to use it, all keys and the database layout: [APP.md](APP.md).**
134
94
 
135
- Y is up, the ground is y = 0, faces are counter-clockwise seen from outside.
136
- Everything is deterministic, so the same calls give the same SVG.
95
+ ---
137
96
 
138
- ### Known limitations
97
+ ## 3. Engine and LLM
139
98
 
140
- - Faces are painter-sorted with plane tests (Newell-Sancha style), which is
141
- exact for solids that touch or stand on each other. Interpenetrating solids
142
- and cyclic overlaps fall back to center depth and can show artifacts; a
143
- face-splitting pass is planned.
144
- - Edges are the polygon outlines, so hidden edges are not drawn.
145
- - Back-face culling assumes closed meshes with CCW faces seen from outside.
99
+ The engine is small enough to read whole (about 2000 lines) and everything it works with is text: meshes, palettes and scenes are JSON, previews are SVG, reports are sentences. That is the interface for an LLM:
146
100
 
147
- ## TypeScript and the `|>` pipeline operator
101
+ - **Look.** The LLM renders a model to SVG and reads it (or the numbers in it), without a screenshot. `occlude` keeps the text small.
102
+ - **Check.** `validate` / `inspect` say in words what is wrong (open shell, inside-out faces, a non-planar face, which one), so the next edit is targeted.
103
+ - **Edit.** The same data goes back in: write documents, call tools, render again. Output is deterministic, so a change in the picture is a change in the model.
104
+ - **MCP server.** `mcp/server.js` (stdio, no dependencies) holds a scene of named objects and exposes it as tools: `add_box`, `add_extrusion`, `add_mesh`, `transform`, `boolean`, `recolor`, `remove`, `clear`, `get_mesh`, `extrude_face`, `move_vertices`, `list`, `inspect`, `render`, `pick`, `export`, `import`, `viewer`. It also serves the viewer and pushes every change to it, so a person watches the model grow. `claude mcp add 3draft -- node /absolute/path/to/3Draft/mcp/server.js`
105
+ - **Shared database.** In the artifact, the LLM and the person edit the same `objects` and `palette` collections, so either side can start from what the other made.
148
106
 
149
- The engine is written in TypeScript (`src/*.ts`) with the F# style pipeline operator, `value |> fn`
150
- (see [Pipeline operator great again](https://dev.to/pengeszikra/pipeline-operator-great-again-1cbn)).
151
- Neither `tsc` nor Node understands `|>` yet, so `npm run build` (`scripts/build.mjs`) first rewrites
152
- every `|>` into plain calls (`scripts/pipe.mjs`, `a |> f |> g(1)` becomes `g(1)(f(a))`) and then lets the
153
- TypeScript compiler type check and emit `dist/*.js` and `dist/*.d.ts`.
107
+ Y is up, the ground is y = 0, faces are counter-clockwise seen from outside.
154
108
 
155
- ```ts
156
- const t1 = mesh.positions[b] |> minus(mesh.positions[a]) |> norm; // the right side is a function
157
- const dir = spec.dir |> norm |> times(dist) |> plus(center); // curried helpers: plus, minus, times
158
- ```
109
+ Examples: `examples/table.js`, `examples/bridge.js` (50 named objects), `examples/cessna.js` (one mesh, 9 colours), and `examples/house.js` (a house converted from Blender with `tools/blend/`, 45 objects, approximate colours).
159
110
 
160
- - The right side must evaluate to a function; put an arrow function in parentheses: `x |> ((v) => v + 1)`.
161
- - `|>` binds like `>>>` while it is parsed, so wrap a pipeline in parentheses next to `+`, `-` and friends.
162
- - `dist/` is committed: the viewer, the examples, the MCP server and the npm package use the built JavaScript, so a
163
- clone runs without a build. `npm run build:check` fails when `dist/` is out of date, `npm test` builds first.
164
- - The compiler is a dev dependency (`typescript`); `TYPESCRIPT=/path/to/typescript.js` points the build at another copy.
111
+ ---
165
112
 
166
- ## Develop
113
+ ## Develop, version, license
167
114
 
168
115
  ```bash
169
116
  npm run build # src/*.ts -> dist/ (|> desugared, type checked)
170
- npm test # builds, then runs every test (dist, examples, mcp, scripts)
117
+ npm test # builds, then runs every test
171
118
  npm run build:check # fails when dist/ is out of date
172
- npm run kernel # re-assemble kernel/pick.hexaw (needs the hexaw repo next to this one)
173
- npm run app # the viewer, http://localhost:5173/app/
119
+ npm run app # the viewer
174
120
  ```
175
121
 
176
- Edit `src/*.ts`, never `dist/`: it is generated and committed so that a clone runs without a build.
177
-
178
- ## Viewer app (repo only, not part of the npm package)
179
-
180
- ```sh
181
- npm run app # http://localhost:5173/app/
182
- ```
183
-
184
- Drag to rotate the view, double click to reset. A target cross follows the pointer and lies on the surface under it.
185
-
186
- | key | does |
187
- | --- | --- |
188
- | `w a s d r f` | fly: forward, left, back, right, up, down; every press is a speed step (an accelerating curve), the opposite key slows down |
189
- | `Shift` | steer towards the pointer while flying |
190
- | `Space` | place a prism on the surface (or shoot one into empty space) |
191
- | `V` | Spider bar: two surface points joined by a thin brown prism; `V` over empty space forgets the first point |
192
- | `T` | travel to the target (a straight flight); `T` over nothing stops and zeroes the speed |
193
- | `X`, `Backspace` | delete the prism under the cross, undo |
194
- | `H`, `G`, `L` | edges, SVG / WebGPU view, light |
195
-
196
- On a phone the four round buttons do the same. The viewer picks with the HEXAW kernel (the footer shows `pick hexaw`). It is a
197
- single `app/index.html` plus a dependency-free static server
198
- (`app/serve.js`, localhost only, serves `app/`, `dist/` and `examples/`).
199
-
200
- ## Versioning
201
-
202
- Date based, written `YY.MM.DD.NNN`. npm needs valid semver (three numbers, no
203
- leading zeros), so the published form is `YY.M.DNNN` where the last number is
204
- day * 1000 + build:
205
-
206
- | readable | package version |
207
- | -------------- | --------------- |
208
- | `26.11.01.001` | `26.11.1001` |
209
- | `26.11.01.002` | `26.11.1002` |
210
- | `26.11.15.012` | `26.11.15012` |
211
-
212
- `npm run bump` sets the next version for today's date in `package.json` and
213
- `src/index.ts` (`node scripts/bump.js --dry` only prints it).
214
-
215
- ## Idea
216
-
217
- - **One material, one palette texture.** Every face's UV points at a single
218
- texel of a small palette texture, so a whole scene is one pipeline, one bind
219
- group and instanced draw calls.
220
- - **Thin WASM core.** Transforms, culling and animation live in a WASM module
221
- (HEXAW and/or Rust) behind a small, documented ABI. JS only uploads buffers.
222
- - **WebGPU first, WebGL2 fallback** behind a minimal `Renderer` interface.
223
- - **AI first.** A deterministic SVG preview and structured `inspect` reports
224
- (plus an MCP server) let an AI build and verify models from text.
225
-
226
- ## Roadmap
227
-
228
- 1. ABI description + reference JS kernel + spinning instanced cube
229
- 2. SVG/PNG preview renderer and MCP `render` / `inspect`
230
- 3. Text modelling language + palette/UV validator
231
- 4. HEXAW kernel next to the JS reference, with differential tests
232
- 5. Rust core only if the logic outgrows HEXAW
233
-
234
- ## License
122
+ Edit `src/*.ts`, never `dist/`. Versions are date based, `YY.MM.DD.NNN`, published as semver `YY.M.DNNN` (`26.11.01.002` is `26.11.1002`); `npm run bump` sets the next one.
235
123
 
236
124
  MIT
237
-
238
- ## Examples
239
-
240
- - `examples/table.js`: the table demo (`npm run example`).
241
- - `examples/bridge.js`: a stone arch bridge of 50 named objects (`npm run bridge`). It writes `examples/bridge.scene.json`, a scene file you can load with the MCP `import` tool (`path: "examples/bridge.scene.json"`).
242
- - `examples/cessna.js`: a small Cessna as ONE mesh with 9 face colours (single flat material, colour per face): lofted closed parts (fuselage, high wing, tail, struts, gear, spinner, propeller) merged into one mesh. `node examples/cessna.js` checks it and writes `examples/cessna.scene.json` (one object, loadable with the MCP `import` tool) and `examples/out/cessna.svg`.
243
-
244
- ## HEXAW kernel
245
-
246
- The picking hot loop (ray against every triangle, Moller-Trumbore, the same arithmetic as `src/pick.ts`) is also written in
247
- [HEXAW](https://github.com/Pengeszikra/hexaw), the byte-visible WebAssembly assembly dialect:
248
-
249
- - `kernel/pick.hexaw` is the source (569 bytes of WebAssembly once assembled).
250
- - `npm run kernel` assembles it with `hexaw-core.js` (found through `HEXAW_CORE`, a checkout next to this repo, or the `hexaw` package) and writes `src/kernel-bytes.ts`, the module as base64 (then `npm run build` carries it into `dist/`). The engine needs neither HEXAW nor a file fetch at runtime. `node scripts/build-kernel.mjs --check` fails when the generated file is out of date.
251
- - `initKernel()` starts it; `kernel.pick(mesh, camera, x, y)` returns exactly what `pick()` returns. The mesh is copied into the kernel's linear memory once for as long as it is the same object.
252
- - The viewer uses the kernel as soon as it has started (the footer shows `pick hexaw`), and plain `pick()` before that.
253
- - Measured on 1200 prisms (7200 faces): 1.7 ms per pick in JavaScript, 0.14 ms with the kernel.
254
-
255
- The kernel exports its linear memory (`EXPM "memory"`, needs hexaw with JS-compatible memory), so the mesh is written straight into it with `Float64Array` / `Int32Array` views and the distance is read back from address 0; the former `setf`/`seti`/`getf`/`grow`/`size` helper functions are gone.
package/dist/csg.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Mesh } from './types.js';
2
+ export type BooleanOp = 'union' | 'subtract' | 'intersect';
3
+ /** One connected piece of a mesh (faces joined by shared edges). */
4
+ export interface Part {
5
+ /** Indices into mesh.faces. */
6
+ faces: number[];
7
+ closed: boolean;
8
+ insideOut: boolean;
9
+ openEdges: number;
10
+ }
11
+ export interface Health {
12
+ parts: Part[];
13
+ /** Faces in closed, outward parts: the ones a boolean can work with. */
14
+ usableFaces: number;
15
+ /** Faces in open (or inside-out) parts: a boolean ignores them. */
16
+ skippedFaces: number;
17
+ /** true when at least one part is a closed solid. */
18
+ hasSolid: boolean;
19
+ /** true when every part is a closed solid. */
20
+ allSolid: boolean;
21
+ }
22
+ /** Splits a mesh into connected parts and says which of them are closed solids (the health check). */
23
+ export declare const health: (mesh: Mesh) => Health;
24
+ /**
25
+ * Boolean of two closed solids (outward CCW faces): 'union', 'subtract' (a minus b) or 'intersect'.
26
+ * Throws with a reason when an input is open or inside out. With `strict: false` the parts that are not closed solids are
27
+ * left out of the calculation instead (see `health`): a union keeps them as they are, a subtract keeps those of `a`,
28
+ * an intersect drops them; it throws only when `a` has no closed part at all.
29
+ */
30
+ export declare const boolean: (a: Mesh, b: Mesh, op: BooleanOp, { strict }?: {
31
+ strict?: boolean;
32
+ }) => Mesh;
33
+ export declare const union: (a: Mesh, b: Mesh) => Mesh;
34
+ export declare const subtract: (a: Mesh, b: Mesh) => Mesh;
35
+ export declare const intersect: (a: Mesh, b: Mesh) => Mesh;