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 +373 -199
- package/bin/lineadraw.mjs +15 -110
- package/dist/bridge.d.ts +102 -0
- package/dist/bridge.js +16 -0
- package/dist/chunk-L2CWZAKW.js +141 -0
- package/dist/chunk-L2CWZAKW.js.map +1 -0
- package/dist/{chunk-VWED6V6F.js → chunk-YYJAXJOY.js} +4291 -1114
- package/dist/chunk-YYJAXJOY.js.map +1 -0
- package/dist/exportDxf-C4CLQCC3.js +10 -0
- package/dist/exportDxf-C4CLQCC3.js.map +1 -0
- package/dist/sdk.d.ts +705 -253
- package/dist/sdk.js +288 -231
- package/dist/sdk.js.map +1 -1
- package/lineadraw.d.ts +804 -1649
- package/package.json +3 -3
- package/templates/README.md +6 -4
- package/templates/sample.block.ts +2 -2
- package/dist/chunk-VWED6V6F.js.map +0 -1
- package/dist/exportDxf-2VU5475O.js +0 -8
- /package/dist/{exportDxf-2VU5475O.js.map → bridge.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,178 +1,29 @@
|
|
|
1
1
|
# lineadraw
|
|
2
2
|
|
|
3
|
-
[Linea](https://lineadraw.com) for Node
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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`
|
|
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` (
|
|
275
|
-
|
|
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/
|
|
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
|
-
`
|
|
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
|
-
###
|
|
175
|
+
### Running editors (the desktop bridge)
|
|
307
176
|
|
|
308
|
-
`
|
|
309
|
-
|
|
310
|
-
|
|
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 {
|
|
315
|
-
import { polar, add } from "lineadraw/helpers"; // vector-math helpers
|
|
182
|
+
import { connectEditors } from "lineadraw/sdk";
|
|
316
183
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
|
|
333
|
-
`
|
|
334
|
-
|
|
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
|
|
343
|
-
(DXF
|
|
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
|
|
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.
|