lineadraw 0.8.0 → 0.10.0

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/README.md CHANGED
@@ -1,178 +1,29 @@
1
1
  # lineadraw
2
2
 
3
- [Linea](https://lineadraw.com) for Node: the headless drawing SDK, the
4
- typings for blocks and commands, and the marketplace CLI — the one
5
- dependency a script, an agent, or a block/command repository needs.
3
+ [Linea](https://lineadraw.com) for Node — one package, four jobs:
6
4
 
7
- - **Typings** — `lineadraw.d.ts` makes `import { defineBlock } from
8
- "lineadraw"` and `import { … } from "lineadraw/helpers"` type-check in
9
- any editor/CI, with parameter types inferred into your `draw` function.
10
- - **SDK** — `import { openDrawing } from "lineadraw/sdk"`: open, edit,
11
- render and export `.linea` documents headlessly
12
- ([below](#headless-sdk-lineadrawsdk)).
13
- - **CLI** — `lineadraw manifest | check | init` turns any repository into
14
- a Linea **marketplace**: a git repo the editor can browse and install
15
- blocks/commands from.
16
-
17
- ## Quick start
18
-
19
- Scaffold a fresh marketplace repository:
20
-
21
- ```bash
22
- npm create lineadraw my-blocks
23
- ```
24
-
25
- Or add the structure to an existing project:
5
+ | Job | What you get | Entry point |
6
+ | --- | --- | --- |
7
+ | [**Drawing SDK**](#1-drawing-sdk) | Open, create, edit, render and export `.linea` / DXF / DWG / PDF / SVG / PNG headlessly — for scripts, agents and batch tools. Also drives drawings open in the desktop app. | `import { openDrawing } from "lineadraw/sdk"` |
8
+ | [**Extension development**](#2-extension-development) | Typings for parametric **blocks** and macro **commands**: `defineBlock` / `defineCommand` type-check in any editor, with parameter types inferred. | `import { defineBlock } from "lineadraw"` |
9
+ | [**Marketplace management**](#3-marketplace-management) | Turn a git repo into a Linea **marketplace** the editor can browse and install from: scaffold it, validate every module, generate the catalog, check it in CI. | `lineadraw init` · `manifest` · `check` |
10
+ | [**CLI for the desktop app**](#4-cli-for-the-running-desktop-app) | Talk to the running Linea desktop app from a terminal: list open drawings, open and close files, run any `linea_*` operation. | `lineadraw status` · `open` · `close` · `call` · `tools` |
26
11
 
27
12
  ```bash
28
- npm i -D lineadraw
29
- npx lineadraw init
30
- ```
31
-
32
- Either way you get `blocks/` and `commands/` folders, a `tsconfig.json`
33
- wired to the typings, a sample block, a CI workflow, and a generated
34
- catalog (`.lineadraw/marketplace.json`) — the repo is addable in the
35
- editor from the first push.
36
-
37
- ## Writing a block
38
-
39
- A block is one module with a single `defineBlock` default export:
40
- typed parameters in, geometry out.
41
-
42
- ```ts
43
- // blocks/surface.block.ts
44
- import { defineBlock } from "lineadraw";
45
-
46
- export default defineBlock({
47
- // The id is the block's identity everywhere: @<collection>/<slug>.
48
- // Re-publishing the same id updates installed copies in place.
49
- id: "@my-blocks/surface",
50
- name: "Surface",
51
- version: "1.0.0",
52
- params: [
53
- { name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
54
- ],
55
- place: ["Start point", "End point"], // one point pick per label, in order
56
- draw: ({ params, inputs: [start, end] }) => {
57
- params.kind; // "rock" | "ground" — inferred from the params table
58
- return [{ type: "line", a: start, b: end }];
59
- },
60
- });
61
- ```
62
-
63
- Notable optional members: `place` may instead be an interactive
64
- `({ params, pickPoint, pickObject })` function run once at insertion;
65
- `dependencies` lists ids of blocks your `draw` emits instances of (installs
66
- pull them along); `paramVisibility({ params })` hides property rows;
67
- `previewInputs` curates the inputs used for thumbnails. Units are
68
- millimeters, Y-up, angles in radians CCW.
69
-
70
- Parameter types (the same rows serve a command's `params`):
71
-
72
- - `number` — `min`/`max`, `integer`, `step`, `prefix`/`suffix` ("1:",
73
- "mm"), `presets` (standard values in the field's dropdown).
74
- - `string` — `placeholder`, `multiline`, `presets`.
75
- - `boolean` (`display: "toggle"` — a toggle button showing the label,
76
- compact in a row), `enum` (`options`, each optionally with a `swatch:
77
- "#rrggbb"` color square; `display: "buttons"` — a segmented toggle group
78
- for short choices, falling back to a dropdown when it doesn't fit),
79
- `color` (`"#rrggbb"`, `presets` = a palette).
80
- - `numberList` (`number[]`, typed Tekla-style: `"0 2*7200"` →
81
- `[0, 7200, 7200]`) and `stringList` (`string[]`: `'A "B 2"'` →
82
- `["A", "B 2"]`), both with `presets` and `placeholder` (shown while
83
- empty).
84
- - Pickers over the document's resources, valued by id / name:
85
- `textStyle`, `dimStyle`, `font`, `lineType`, `hatchPattern`.
86
- - `label` — a section heading (no value); `row` — 2–3 compact params on one
87
- panel row (`items`, each label a short prefix like "W"); its items are
88
- ordinary flat params (`params.w`).
89
-
90
- Every param may carry a `hint` (the label's tooltip). An instance that
91
- overrides a param shows "Reset" beside it in Properties, back to your
92
- default. `draw` also receives
93
- `styles`: `styles.text(id)` / `styles.dim(id)` give a document style (an
94
- empty or unknown id → the first), for blocks that lay out with real text
95
- sizes.
96
-
97
- ## Writing a command
98
-
99
- A command is a macro with a single `defineCommand` default export. Its
100
- whole run is one undo step; Esc during a pick reverts cleanly. A
101
- `params` table (the same rows a block's `params` uses) is asked for in a
102
- dialog before the run and arrives typed as `ctx.params`; headless callers
103
- pass it by name.
104
-
105
- ```ts
106
- // commands/circle-here.cmd.ts
107
- import { defineCommand } from "lineadraw";
108
-
109
- export default defineCommand({
110
- id: "@my-blocks/circle-here",
111
- name: "Circle here",
112
- run: async ({ document, pickPoint, prompt, showToast }) => {
113
- const center = await pickPoint("Center point");
114
- const r = await prompt("Radius");
115
- document.add([{ type: "circle", center, radius: Number.parseFloat(r) }]);
116
- showToast("Done", "success");
117
- },
118
- });
119
- ```
120
-
121
- Imports are limited to `"lineadraw"` and `"lineadraw/helpers"` (geometry
122
- helpers) — the editor resolves both when executing your module.
123
-
124
- ## The CLI
125
-
126
- ```
127
- lineadraw manifest Validate all modules and write .lineadraw/marketplace.json
128
- lineadraw check Validate without writing; fail if the committed manifest is stale
129
- lineadraw init Add the marketplace structure to the current npm project
130
-
131
- lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
132
- lineadraw open <file.linea> Open a file in a new window
133
- lineadraw close [--editor <x>] [--discard]
134
- lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* tool
135
- lineadraw tools The operations the bridge offers
136
- ```
137
-
138
- The bridge commands talk to the running Linea desktop app on
139
- `127.0.0.1:7431` (the app's `bridge.json` supplies the port and the agent
140
- token; `--port`, `--token` and `LINEA_BRIDGE_TOKEN` override) — see
141
- docs/desktop-bridge.md in the linea repo.
142
-
143
- The manifest is deterministic (repo-relative paths, no git metadata), so
144
- `check` compares the parsed JSON (formatting, a trailing newline or CRLF
145
- line endings never make it stale) — the included CI workflow runs it on every
146
- push and PR. `manifest`/`check` also **dry-run every block** through
147
- `lineadraw/sdk` and render thumbnails.
148
-
149
- Collection name and owner come from your package.json:
150
-
151
- ```json
152
- { "lineadraw": { "marketplaceName": "My blocks", "marketplaceOwner": "Me" } }
13
+ npm i lineadraw # the SDK in a script or tool
14
+ npm i -D lineadraw # typings + CLI in a block/command repository
15
+ npm create lineadraw my-blocks # a fresh marketplace repository
153
16
  ```
154
17
 
155
- ## Using a marketplace in the editor
156
-
157
- In [the editor](https://lineadraw.com): **Library → Marketplace tab → the
158
- gear button (Manage marketplaces…) → Add a marketplace** and enter
159
- `owner/repo` (or `owner/repo@branch`). For a private repository,
160
- create a fine-grained personal access token with read-only Contents access
161
- to just that repository and paste it in the add dialog — it is stored only
162
- on that device. Installing embeds the module (and its declared dependency
163
- closure) in the document, so drawings keep working offline and travel with
164
- the file.
18
+ Requires Node.js >= 20. Units everywhere are millimeters, Y-up, angles in
19
+ radians CCW.
165
20
 
166
- ## Headless SDK (`lineadraw/sdk`)
21
+ ## 1. Drawing SDK
167
22
 
168
23
  A script-friendly API over the Linea CAD kernel: open, create, query, edit,
169
24
  render, and import/export `.linea` documents headlessly in Node — no browser,
170
25
  no server. Built for agents, CLIs, and batch tooling.
171
26
 
172
- ```bash
173
- npm i lineadraw
174
- ```
175
-
176
27
  ```js
177
28
  import { newDrawing, openDrawing } from "lineadraw/sdk";
178
29
 
@@ -243,8 +94,7 @@ appended when missing):
243
94
  a throw rolls back), `undo()`, `redo()`, `applyOps(ops)`, `ready()`
244
95
  - **Styles** (by name or id): `listTextStyles()`, `addTextStyle({name, …})`,
245
96
  `updateTextStyle(nameOrId, patch)`, `removeTextStyle(nameOrId)` (refused
246
- while objects use it) — the same four for `DimStyle`; `setTextStyle` /
247
- `setDimStyle` remain as deprecated aliases of update
97
+ while objects use it) — the same four for `DimStyle`
248
98
  - **Line types / hatch patterns** (the document's own definitions, by
249
99
  name, resolved ahead of the built-in catalogue): `listLineTypes()`,
250
100
  `addLineTypes([{name, dashesMm}])`, `updateLineType(name, patch)`,
@@ -263,7 +113,9 @@ appended when missing):
263
113
  configuration, never written to the file.
264
114
  - **Scripts**: `await runScript(source, inputs?, params?)` runs a
265
115
  `defineCommand` module (the same one the editor palette runs) headlessly
266
- as ONE undo step — its declared `inputs` (points) are supplied up front,
116
+ as ONE undo step — its declared `inputs` are supplied up front (by name
117
+ for an inputs table: `{ origin: [0, 0], doors: [id] }`; points in order
118
+ for labels),
267
119
  its declared `params` by name (`{ count: 3 }`; omitted ones take their
268
120
  defaults, unknown names and wrong types throw); `runUserCommand(id, inputs?,
269
121
  params?)` runs one of the document's own commands. Returns the script's
@@ -271,8 +123,10 @@ appended when missing):
271
123
  - **Layers**: `addLayer`, `updateLayer(nameOrId, patch)`,
272
124
  `removeLayer(nameOrId)` (must be empty and not current),
273
125
  `reorderLayers(order)`, `setCurrentLayer(nameOrId)`
274
- - **Assets**: `addAsset(dataUrl) → assetId` (for `image` objects;
275
- content-keyed), `removeAsset(id)` (refused while referenced)
126
+ - **Assets**: `addAsset(dataUrl) → assetId` (content-keyed; an embedded
127
+ reference names it), `listAssets()`, `removeAsset(id)` (refused while
128
+ referenced)
129
+ - **Settings**: `updateSettings(patch)` (document settings, e.g. units)
276
130
  - **Transform**: `move`, `rotate` (radians), `scale`, `mirror`, `copy`, `array`
277
131
  - **Layouts**: `addLayout(name, {sheet, scale, viewport, index})`,
278
132
  `updateLayout(nameOrId, {name, sheet, printPalette})`, `removeLayout`,
@@ -286,13 +140,13 @@ appended when missing):
286
140
  `unitOverride`), `importDwg(path)`/`importDwgData(bytes)` (DWG R13–R2018),
287
141
  `importPdfData(arrayBuffer)`
288
142
 
289
- Full parameter docs are in the bundled `dist/index.d.ts` JSDoc.
143
+ Full parameter docs are in the bundled `dist/sdk.d.ts` JSDoc.
290
144
 
291
145
  ### Object types
292
146
 
293
147
  `line` (a, b) · `polyline` (points with optional bulge, closed) · `circle` ·
294
148
  `arc` (CCW start→end) · `ellipse` (center, majorAxis vector, ratio) ·
295
- `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
149
+ `point` (p) · `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
296
150
  `text` (position, content; **default alignment center/center**; optional
297
151
  leaderLines with the arrowhead at index 0) · `dimension` (linear/angular/
298
152
  radial/diameter; linear: `offset` is the signed distance of the dimension
@@ -301,46 +155,70 @@ line from points[0] (left of the measuring direction positive) and
301
155
  signed arc radius; `labels[i]` place and word each segment's text —
302
156
  `along`/`across` in mm, unset = automatic, `content` with `{value}` for the
303
157
  measurement; linear with >2 points = chain) · `block` (instances of script definitions) ·
304
- `viewport` (layouts only) · `image` (placement of an asset).
158
+ `table` (columns, rows, cells) · `comment` (a review bubble, never
159
+ printed) · `reference` (a linked or embedded file: `.linea`, DXF, DWG,
160
+ PDF, PNG/JPEG — a picture is a reference whose `source` is
161
+ `{ kind: "asset", assetId }`) · `viewport` (layouts only).
162
+
163
+ ### Block definitions in a document
164
+
165
+ `document.blockDefinitions` is an array of ES-module **source strings** —
166
+ the same `defineBlock` modules described under
167
+ [Extension development](#2-extension-development). Load them with
168
+ `await doc.setBlockDefinitions([...sources])` or `addBlockDefinitions`
169
+ (async, so the script transpiler is loaded and block geometry is
170
+ definitive when it resolves), then place instances with
171
+ `add([{ type: "block", definitionId }])`. An instance's first point input
172
+ is its pivot: `draw` sees inputs localized to it, and the instance's
173
+ `rotation`/`scale` apply about it.
305
174
 
306
- ### Block scripts
175
+ ### Running editors (the desktop bridge)
307
176
 
308
- `document.blockDefinitions` is an array of ES-module **source strings**
309
- (`await doc.setBlockDefinitions([...sources])` — async so the script
310
- transpiler is loaded and block geometry is definitive when it resolves).
311
- A block is one `defineBlock` call:
177
+ `Drawing` works on a document in your own process. To work on a drawing
178
+ that is OPEN in the Linea desktop app (or a web tab paired with it), connect
179
+ to the app's bridge on `127.0.0.1:7431`:
312
180
 
313
181
  ```ts
314
- import { defineBlock } from "lineadraw";
315
- import { polar, add } from "lineadraw/helpers"; // vector-math helpers
182
+ import { connectEditors } from "lineadraw/sdk";
316
183
 
317
- export default defineBlock({
318
- id: "chair", // instances reference this
319
- name: "Chair",
320
- params: [{ name: "width", type: "number", default: 450 }],
321
- draw: ({ params }) => [
322
- // Plain object DTOs in block-local coordinates; params.width: number.
323
- { type: "line", a: { x: 0, y: 0 }, b: { x: params.width, y: 0 } },
324
- ],
184
+ const linea = await connectEditors(); // port + agent token from the app's bridge.json
185
+ const [first] = await linea.editors(); // { id, name, path, kind, readOnly, dirty }
186
+ const plan = linea.editor(first.id); // by document id, file path or unique name
187
+
188
+ // One linea_* operation = one undo step in that editor.
189
+ await plan.call("linea_add_objects", {
190
+ objects: [{ type: "circle", center: [0, 0], radius: 500 }],
325
191
  });
192
+
193
+ // A whole command module, run INSIDE the editor against the full Drawing
194
+ // API: one round trip, one undo step. `inputs` / `params` answer the
195
+ // module's declared ones.
196
+ const result = await plan.runScript(
197
+ `import { defineCommand } from "lineadraw";
198
+ export default defineCommand({
199
+ id: "count-lines",
200
+ name: "Count lines",
201
+ run: ({ document }) => document.listObjects({ type: "line" }).length,
202
+ });`,
203
+ );
204
+
205
+ const other = await linea.open("details.linea"); // a window of its own
206
+ await linea.editor(other.id).close({ discard: true });
326
207
  ```
327
208
 
328
- Optional members: `place` — an array of point labels (one pick per label)
329
- or an interactive `({ params, pickPoint, pickObject })` function run once
330
- at insertion (default is one insertion point) — and
331
- `paramVisibility({ params })` (hides property rows). `params` may be the
332
- table literal or a zero-arg function returning it. Imports are limited to
333
- `"lineadraw"` and `"lineadraw/helpers"`. The instance's first point input
334
- is its pivot: `draw` sees inputs localized to it, and instance
335
- `rotation`/`scale` apply about it. (Plain named exports — incl. the
336
- pre-rename `main`/`defineInput` names — remain evaluatable as the frozen
337
- wire form.)
209
+ The operations are the same `linea_*` catalog the MCP server and the CLI
210
+ offer (`await linea.tools()` lists them). Failures throw a `BridgeError`
211
+ whose `code` says why: `unreachable` (the app isn't running),
212
+ `ambiguous` (several editors are open and none was named — `details`
213
+ lists them), `noSuchEditor`, `readOnly`, `validation`… The connection
214
+ options (`port`, `token`, `bridgeFile`) override what bridge.json says;
215
+ `LINEA_BRIDGE_TOKEN` supplies a token too.
338
216
 
339
217
  ### Interchange notes
340
218
 
341
219
  - DXF/DWG export flattens semantics: block instances are exported as their
342
- evaluated geometry (no `INSERT` reuse) and `image` objects are skipped
343
- (DXF images reference external files). The export result reports both in
220
+ evaluated geometry (no `INSERT` reuse), and embedded references are
221
+ skipped (DXF can't embed a file). The export result reports both in
344
222
  `skipped`.
345
223
  - PDF import extracts vector strokes and text runs (dimensions arrive as
346
224
  exploded geometry). pdf.js loads lazily on first use, as do the PNG
@@ -352,10 +230,306 @@ wire form.)
352
230
  - Dimensions validate their point counts per kind (linear ≥ 2, angular ≥ 3,
353
231
  radial/diameter ≥ 2).
354
232
 
233
+ ## 2. Extension development
234
+
235
+ Blocks and commands are plain TypeScript (or JavaScript) modules. The
236
+ package's `lineadraw.d.ts` types the two module names a script may import —
237
+ `"lineadraw"` (`defineBlock`, `defineCommand`) and `"lineadraw/helpers"`
238
+ (geometry helpers) — so authoring type-checks in VS Code, any other
239
+ TypeScript-aware editor, and CI. The editor resolves both names when it
240
+ runs your module; nothing else can be imported. `lineadraw init` writes a
241
+ `tsconfig.json` wired to the typings.
242
+
243
+ A module works on its own too: drop a `.block.ts` / `.cmd.ts` file onto the
244
+ editor, or exercise it headlessly with the SDK
245
+ (`await doc.addBlockDefinitions([source])`, `await doc.runScript(source)`).
246
+
247
+ ### Writing a block
248
+
249
+ A block is one module with a single `defineBlock` default export:
250
+ typed parameters in, geometry out.
251
+
252
+ ```ts
253
+ // blocks/surface.block.ts
254
+ import { defineBlock } from "lineadraw";
255
+
256
+ export default defineBlock({
257
+ // The id is the block's identity everywhere: @<collection>/<slug>.
258
+ // Re-publishing the same id updates installed copies in place.
259
+ id: "@my-blocks/surface",
260
+ name: "Surface",
261
+ version: "1.0.0",
262
+ params: [
263
+ { name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
264
+ ],
265
+ // What the user picks at insertion, read by name in `draw`.
266
+ inputs: [
267
+ { name: "start", type: "point", label: "Start point" },
268
+ { name: "end", type: "point", label: "End point" },
269
+ ],
270
+ draw: ({ params, inputs: { start, end } }) => {
271
+ params.kind; // "rock" | "ground" — inferred from the params table
272
+ return [{ type: "line", a: start, b: end }];
273
+ },
274
+ });
275
+ ```
276
+
277
+ Input kinds: `point`; `points` — a chain picked until Enter (`min`,
278
+ `max`, `closed` for a polygon) arriving as `Vec2[]`; `object` / `objects`
279
+ — picked objects filtered by `types` (`["line", "polyline"]`), arriving in
280
+ `draw` as the objects themselves (re-evaluated when they change; a
281
+ deleted one is `null` / dropped). `point` and `object` may be `optional`
282
+ (Enter skips). The first point is the pivot. Instances store their inputs
283
+ by name, so reordering or adding inputs never breaks placed instances —
284
+ removing or renaming one does. (The older `place: ["Start", "End"]` labels
285
+ form, with `draw` reading `inputs` as an array, still works.)
286
+
287
+ Notable optional members: `place` — an interactive
288
+ `({ params, pickPoint, pickPoints, pickObject, pickObjects })` function run
289
+ once at insertion, for conditional or chained picks; with an `inputs`
290
+ table it returns the inputs by name;
291
+ `dependencies` lists ids of blocks your `draw` emits instances of (installs
292
+ pull them along); `paramVisibility({ params })` hides property rows;
293
+ `previewInputs` curates the inputs used for thumbnails. Units are
294
+ millimeters, Y-up, angles in radians CCW.
295
+
296
+ Parameter types (the same rows serve a command's `params`):
297
+
298
+ - `number` — `min`/`max`, `integer`, `step`, `prefix`/`suffix` ("1:",
299
+ "mm"), `presets` (standard values in the field's dropdown).
300
+ - `string` — `placeholder`, `multiline`, `presets`.
301
+ - `boolean` (`display: "toggle"` — a toggle button showing the label,
302
+ compact in a row), `enum` (`options`, each optionally with a `swatch:
303
+ "#rrggbb"` color square; `display: "buttons"` — a segmented toggle group
304
+ for short choices, falling back to a dropdown when it doesn't fit),
305
+ `color` (`"#rrggbb"`, `presets` = a palette).
306
+ - `numberList` (`number[]`, typed Tekla-style: `"0 2*7200"` →
307
+ `[0, 7200, 7200]`) and `stringList` (`string[]`: `'A "B 2"'` →
308
+ `["A", "B 2"]`), both with `presets` and `placeholder` (shown while
309
+ empty).
310
+ - Pickers over the document's resources, valued by id / name:
311
+ `textStyle`, `dimStyle`, `font`, `lineType`, `hatchPattern`.
312
+ - `label` — a section heading (no value); `row` — 2–3 compact params on one
313
+ panel row (`items`, each label a short prefix like "W"); its items are
314
+ ordinary flat params (`params.w`).
315
+ - `data` — structured data the block owns: any JSON value, typed by its
316
+ default (`default: {} as Record<string, Wall>`). It has no Properties row;
317
+ handles, handle menus and actions edit it (agents set it like any param).
318
+
319
+ Geometry helpers come from `"lineadraw/helpers"` — vector math
320
+ (`add`, `sub`, `scale`, `rotate`, `polar`, …) and polygon booleans:
321
+ `union(...polygons)`, `difference(subject, ...clips)`,
322
+ `intersection(...polygons)`. A polygon is a ring of points, or rings with
323
+ holes (first the outer boundary). The result is a flat list of rings,
324
+ ready for closed polylines or ONE hatch, which fills even-odd so holes
325
+ stay holes:
326
+
327
+ ```ts
328
+ const solid = union(...walls.map(wallRect)); // overlapping walls → clean outlines
329
+ return [
330
+ { type: "hatch", loops: solid.map((points) => ({ points })),
331
+ fill: { kind: "pattern", name: "ANSI31", angle: 0, scale: 20 } },
332
+ ...solid.map((points) => ({ type: "polyline", points, closed: true })),
333
+ ];
334
+ ```
335
+
336
+ Every param may carry a `hint` (the label's tooltip). An instance that
337
+ overrides a param shows "Reset" beside it in Properties, back to your
338
+ default. `draw` also receives
339
+ `styles`: `styles.text(id)` / `styles.dim(id)` give a document style (an
340
+ empty or unknown id → the first), for blocks that lay out with real text
341
+ sizes.
342
+
343
+ #### Hosted blocks
344
+
345
+ A block can LIVE ON another instance: a door on a floor plan. Give it a
346
+ `host` input; it then draws in the host's frame (moving, rotating and
347
+ copying with it, and deleted with it), and the two talk through
348
+ `expose`, JSON each publishes from its OWN params:
349
+
350
+ ```ts
351
+ // The plan publishes its walls and cuts them where its openings say.
352
+ expose: ({ params }) => ({ walls: wallTable(params) }),
353
+ draw: ({ params, hosted }) => {
354
+ const cuts = hosted.map((h) => h.exposed?.cut).filter(Boolean);
355
+ return wallSolids(params, cuts);
356
+ },
357
+
358
+ // A door: picked by clicking a plan's wall; `attach` turns the click
359
+ // (plan-local) into its params.
360
+ inputs: [{ name: "plan", type: "host", types: ["@my-blocks/floor-plan"] }],
361
+ expose: ({ params }) => ({ cut: { wall: params.wall, at: params.at, width: params.width } }),
362
+ attach: ({ host, point }) => nearestWall(host.walls, point), // → { wall, at }
363
+ draw: ({ params, host }) => doorLeaf(host.walls[params.wall], params),
364
+ ```
365
+
366
+ `expose` never sees the host or what's on it, so the two can't loop
367
+ (ADR 0008). What a host publishes and what its openings read is a
368
+ contract between the modules: document it.
369
+
370
+ A hosted block has no host in Library / marketplace previews. Give it a
371
+ stand-in with `previewHost`: the JSON a host's `expose` would return.
372
+ The preview runs `attach` as if the host were clicked at its origin, and
373
+ `draw` gets the stand-in as `host` (the save-time dry run uses it too):
374
+
375
+ ```ts
376
+ previewHost: { preview: true, walls: { A: { a: [-1500, 0], b: [1500, 0], t: 200 } } },
377
+ // …and in draw, `host.preview` can add context: a stretch of wall.
378
+ ```
379
+
380
+ #### Handles and actions
381
+
382
+ `handles({ params, inputs })` returns the handles a selected instance
383
+ shows, in block-local coordinates. It re-runs as values change, so a
384
+ handle can sit at the current width:
385
+
386
+ ```ts
387
+ handles: ({ params }) => [
388
+ // Drag along local X: the distance becomes `width` (clamped to its min/max).
389
+ { id: "width", at: [params.width, 0], shape: "triangle",
390
+ along: { origin: [0, 0], direction: [1, 0] }, param: "width" },
391
+ // A button: one click, one undo step.
392
+ { id: "flip", at: [params.width / 2, 150], fixed: true, icon: "flip-horizontal",
393
+ click: ({ params }) => ({ params: { swing: params.swing === "left" ? "right" : "left" } }) },
394
+ // A line handle (shown while hovered) with a hover menu.
395
+ { id: "wall", kind: "line", from: [0, 0], to: [params.width, 0], fixed: true,
396
+ menu: ["thickness",
397
+ { field: { type: "boolean", label: "Enabled" }, value: !params.off,
398
+ set: (on) => ({ params: { off: !on } }) }],
399
+ actions: [{ label: "Reset", icon: "x", run: () => ({ params: { off: false } }) }] },
400
+ ],
401
+ // The instance's context menu (right-click): pick a point, then run.
402
+ actions: [
403
+ { label: "Add mark", pick: "point",
404
+ run: ({ params, point }) => ({ params: { marks: [...params.marks, [point.x, point.y]] } }) },
405
+ ],
406
+ ```
407
+
408
+ A handle has ONE drag behaviour: `param` (the constrained value → a number
409
+ param), `input` (moves a point input, replacing its default handle), or
410
+ `drag` (a function returning a patch). Otherwise it is `fixed`. The
411
+ constraint is free, `along` an axis (value = distance) or `around` a
412
+ centre (value = angle in radians). A line handle moves along its normal by
413
+ default. `click` turns it into a button. Shapes are `square`, `circle`,
414
+ `diamond`, `triangle`, `arrow`, `double-arrow`, `plus` and `minus`, and `icon` names a
415
+ lucide glyph from a small set. Handle functions and actions return a PATCH
416
+ — `{ params?, inputs? }`, with inputs by name and block-local — and never
417
+ mutate anything. Each applies as one undo step. The save-time dry run
418
+ checks every handle record.
419
+
420
+ ### Writing a command
421
+
422
+ A command is a macro with a single `defineCommand` default export. Its
423
+ whole run is one undo step; Esc during a pick reverts cleanly. Its
424
+ `inputs` table is the same one blocks declare; `object` / `objects` inputs
425
+ take the current selection when it fits (types and count) and arrive as
426
+ ids. To ask more DURING the run (the next question depends on the last
427
+ answer), use `editor.pickPoint` / `pickObject` / `prompt`, or
428
+ `editor.ask(paramsTable, { title })` for several fields in one dialog —
429
+ and declare `requiresEditor: true`, so CLI / agent runs refuse the command
430
+ up front instead of failing halfway. A
431
+ `params` table (the same rows a block's `params` uses) is asked for in a
432
+ dialog before the run and arrives typed as `ctx.params`; headless callers
433
+ pass it by name.
434
+
435
+ ```ts
436
+ // commands/circle-here.cmd.ts
437
+ import { defineCommand } from "lineadraw";
438
+
439
+ export default defineCommand({
440
+ id: "@my-blocks/circle-here",
441
+ name: "Circle here",
442
+ inputs: [{ name: "center", type: "point", label: "Center point" }],
443
+ params: [{ name: "radius", type: "number", default: 10, min: 0.1 }],
444
+ // `inputs` are picked in the editor (or passed by name by a headless
445
+ // caller: { center: [0, 0] }), `params` asked for in a dialog; `editor`
446
+ // is absent when headless.
447
+ run: async ({ document, editor, inputs: { center }, params }) => {
448
+ document.add([{ type: "circle", center, radius: params.radius }]);
449
+ editor?.toast("Done", "success");
450
+ },
451
+ });
452
+ ```
453
+
454
+ ## 3. Marketplace management
455
+
456
+ A marketplace is a git repository of blocks and commands plus a generated
457
+ catalog (`.lineadraw/marketplace.json`) that the editor reads.
458
+
459
+ Scaffold a fresh one:
460
+
461
+ ```bash
462
+ npm create lineadraw my-blocks
463
+ ```
464
+
465
+ Or add the structure to an existing npm project:
466
+
467
+ ```bash
468
+ npm i -D lineadraw
469
+ npx lineadraw init
470
+ ```
471
+
472
+ Either way you get `blocks/` and `commands/` folders, a `tsconfig.json`
473
+ wired to the typings, a sample block, a CI workflow, the catalog, and
474
+ `npm run manifest` / `npm run check` scripts — the repo is addable in the
475
+ editor from the first push.
476
+
477
+ ```
478
+ lineadraw manifest [--dir <path>] Validate all modules and write .lineadraw/marketplace.json
479
+ lineadraw check [--dir <path>] Validate without writing; fail if the committed manifest is stale
480
+ lineadraw init [--dir <path>] Add the marketplace structure to the current npm project
481
+ ```
482
+
483
+ `manifest` and `check` **dry-run every block** through `lineadraw/sdk` and
484
+ bake preview thumbnails into the catalog. The manifest is deterministic
485
+ (repo-relative paths, no git metadata), so `check` compares the parsed JSON
486
+ — formatting, a trailing newline or CRLF line endings never make it stale.
487
+ The included CI workflow runs it on every push and PR.
488
+
489
+ Collection name and owner come from your package.json:
490
+
491
+ ```json
492
+ { "lineadraw": { "marketplaceName": "My blocks", "marketplaceOwner": "Me" } }
493
+ ```
494
+
495
+ ### Using a marketplace in the editor
496
+
497
+ In [Linea](https://lineadraw.com): **App settings → Marketplaces → Add a
498
+ marketplace** (the gear in Library → Marketplace opens the same tab) and
499
+ enter `owner/repo` (or `owner/repo@branch`). For a private repository,
500
+ create a fine-grained personal access token with read-only Contents access
501
+ to just that repository and paste it under "Add an access token" — it is stored only
502
+ on that device. Installing embeds the module (and its declared dependency
503
+ closure) in the document, so drawings keep working offline and travel with
504
+ the file.
505
+
506
+ ## 4. CLI for the running desktop app
507
+
508
+ ```
509
+ lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
510
+ lineadraw open <file.linea> Open a file in a new window
511
+ lineadraw close [--editor <x>] [--discard]
512
+ lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* operation
513
+ lineadraw tools The operations the bridge offers
514
+ ```
515
+
516
+ These commands talk to the running Linea desktop app on `127.0.0.1:7431`
517
+ (the app's `bridge.json` supplies the port and the agent token; `--port`,
518
+ `--token` and `LINEA_BRIDGE_TOKEN` override). `--editor` names a drawing by
519
+ document id, file path or unique name, and may be left out while only one
520
+ is open. Each `call` is one undo step in that editor. From code, use the
521
+ SDK's [`connectEditors()`](#running-editors-the-desktop-bridge).
522
+
523
+ ```bash
524
+ npx lineadraw call linea_add_objects '{"objects":[{"type":"circle","center":[0,0],"radius":500}]}'
525
+ ```
526
+
355
527
  ## Ecosystem
356
528
 
357
- - [Linea editor](https://lineadraw.com) — the browser CAD app that runs
358
- these modules.
529
+ - [Linea](https://lineadraw.com) — the CAD editor, on the desktop and on
530
+ the web, that runs these modules.
531
+ - [`create-lineadraw`](https://www.npmjs.com/package/create-lineadraw) —
532
+ `npm create lineadraw`, the marketplace scaffolder.
359
533
  - [`lineadraw/lineadraw`](https://github.com/lineadraw/lineadraw) — the
360
534
  public collection: real blocks/commands to learn from, agent skill, and
361
535
  guides.