lineadraw 0.8.0 → 0.9.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 +249 -198
- package/bin/lineadraw.mjs +15 -110
- package/dist/bridge.d.ts +102 -0
- package/dist/bridge.js +16 -0
- package/dist/{chunk-VWED6V6F.js → chunk-BQCSFVAF.js} +401 -81
- package/dist/chunk-BQCSFVAF.js.map +1 -0
- package/dist/chunk-L2CWZAKW.js +141 -0
- package/dist/chunk-L2CWZAKW.js.map +1 -0
- package/dist/exportDxf-T2RTSTH5.js +10 -0
- package/dist/exportDxf-T2RTSTH5.js.map +1 -0
- package/dist/sdk.d.ts +469 -234
- package/dist/sdk.js +164 -168
- package/dist/sdk.js.map +1 -1
- package/lineadraw.d.ts +304 -1617
- package/package.json +2 -2
- 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
|
-
});
|
|
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
|
|
61
16
|
```
|
|
62
17
|
|
|
63
|
-
|
|
64
|
-
|
|
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`):
|
|
18
|
+
Requires Node.js >= 20. Units everywhere are millimeters, Y-up, angles in
|
|
19
|
+
radians CCW.
|
|
71
20
|
|
|
72
|
-
|
|
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" } }
|
|
153
|
-
```
|
|
154
|
-
|
|
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.
|
|
165
|
-
|
|
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
|
|
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)`,
|
|
@@ -271,8 +121,10 @@ appended when missing):
|
|
|
271
121
|
- **Layers**: `addLayer`, `updateLayer(nameOrId, patch)`,
|
|
272
122
|
`removeLayer(nameOrId)` (must be empty and not current),
|
|
273
123
|
`reorderLayers(order)`, `setCurrentLayer(nameOrId)`
|
|
274
|
-
- **Assets**: `addAsset(dataUrl) → assetId` (
|
|
275
|
-
|
|
124
|
+
- **Assets**: `addAsset(dataUrl) → assetId` (content-keyed; an embedded
|
|
125
|
+
reference names it), `listAssets()`, `removeAsset(id)` (refused while
|
|
126
|
+
referenced)
|
|
127
|
+
- **Settings**: `updateSettings(patch)` (document settings, e.g. units)
|
|
276
128
|
- **Transform**: `move`, `rotate` (radians), `scale`, `mirror`, `copy`, `array`
|
|
277
129
|
- **Layouts**: `addLayout(name, {sheet, scale, viewport, index})`,
|
|
278
130
|
`updateLayout(nameOrId, {name, sheet, printPalette})`, `removeLayout`,
|
|
@@ -286,13 +138,13 @@ appended when missing):
|
|
|
286
138
|
`unitOverride`), `importDwg(path)`/`importDwgData(bytes)` (DWG R13–R2018),
|
|
287
139
|
`importPdfData(arrayBuffer)`
|
|
288
140
|
|
|
289
|
-
Full parameter docs are in the bundled `dist/
|
|
141
|
+
Full parameter docs are in the bundled `dist/sdk.d.ts` JSDoc.
|
|
290
142
|
|
|
291
143
|
### Object types
|
|
292
144
|
|
|
293
145
|
`line` (a, b) · `polyline` (points with optional bulge, closed) · `circle` ·
|
|
294
146
|
`arc` (CCW start→end) · `ellipse` (center, majorAxis vector, ratio) ·
|
|
295
|
-
`hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
|
|
147
|
+
`point` (p) · `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
|
|
296
148
|
`text` (position, content; **default alignment center/center**; optional
|
|
297
149
|
leaderLines with the arrowhead at index 0) · `dimension` (linear/angular/
|
|
298
150
|
radial/diameter; linear: `offset` is the signed distance of the dimension
|
|
@@ -301,46 +153,70 @@ line from points[0] (left of the measuring direction positive) and
|
|
|
301
153
|
signed arc radius; `labels[i]` place and word each segment's text —
|
|
302
154
|
`along`/`across` in mm, unset = automatic, `content` with `{value}` for the
|
|
303
155
|
measurement; linear with >2 points = chain) · `block` (instances of script definitions) ·
|
|
304
|
-
`
|
|
156
|
+
`table` (columns, rows, cells) · `comment` (a review bubble, never
|
|
157
|
+
printed) · `reference` (a linked or embedded file: `.linea`, DXF, DWG,
|
|
158
|
+
PDF, PNG/JPEG — a picture is a reference whose `source` is
|
|
159
|
+
`{ kind: "asset", assetId }`) · `viewport` (layouts only).
|
|
160
|
+
|
|
161
|
+
### Block definitions in a document
|
|
305
162
|
|
|
306
|
-
|
|
163
|
+
`document.blockDefinitions` is an array of ES-module **source strings** —
|
|
164
|
+
the same `defineBlock` modules described under
|
|
165
|
+
[Extension development](#2-extension-development). Load them with
|
|
166
|
+
`await doc.setBlockDefinitions([...sources])` or `addBlockDefinitions`
|
|
167
|
+
(async, so the script transpiler is loaded and block geometry is
|
|
168
|
+
definitive when it resolves), then place instances with
|
|
169
|
+
`add([{ type: "block", definitionId }])`. An instance's first point input
|
|
170
|
+
is its pivot: `draw` sees inputs localized to it, and the instance's
|
|
171
|
+
`rotation`/`scale` apply about it.
|
|
307
172
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
173
|
+
### Running editors (the desktop bridge)
|
|
174
|
+
|
|
175
|
+
`Drawing` works on a document in your own process. To work on a drawing
|
|
176
|
+
that is OPEN in the Linea desktop app (or a web tab paired with it), connect
|
|
177
|
+
to the app's bridge on `127.0.0.1:7431`:
|
|
312
178
|
|
|
313
179
|
```ts
|
|
314
|
-
import {
|
|
315
|
-
import { polar, add } from "lineadraw/helpers"; // vector-math helpers
|
|
180
|
+
import { connectEditors } from "lineadraw/sdk";
|
|
316
181
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
],
|
|
182
|
+
const linea = await connectEditors(); // port + agent token from the app's bridge.json
|
|
183
|
+
const [first] = await linea.editors(); // { id, name, path, kind, readOnly, dirty }
|
|
184
|
+
const plan = linea.editor(first.id); // by document id, file path or unique name
|
|
185
|
+
|
|
186
|
+
// One linea_* operation = one undo step in that editor.
|
|
187
|
+
await plan.call("linea_add_objects", {
|
|
188
|
+
objects: [{ type: "circle", center: [0, 0], radius: 500 }],
|
|
325
189
|
});
|
|
190
|
+
|
|
191
|
+
// A whole command module, run INSIDE the editor against the full Drawing
|
|
192
|
+
// API: one round trip, one undo step. `inputs` / `params` answer the
|
|
193
|
+
// module's declared ones.
|
|
194
|
+
const result = await plan.runScript(
|
|
195
|
+
`import { defineCommand } from "lineadraw";
|
|
196
|
+
export default defineCommand({
|
|
197
|
+
id: "count-lines",
|
|
198
|
+
name: "Count lines",
|
|
199
|
+
run: ({ document }) => document.listObjects({ type: "line" }).length,
|
|
200
|
+
});`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const other = await linea.open("details.linea"); // a window of its own
|
|
204
|
+
await linea.editor(other.id).close({ discard: true });
|
|
326
205
|
```
|
|
327
206
|
|
|
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.)
|
|
207
|
+
The operations are the same `linea_*` catalog the MCP server and the CLI
|
|
208
|
+
offer (`await linea.tools()` lists them). Failures throw a `BridgeError`
|
|
209
|
+
whose `code` says why: `unreachable` (the app isn't running),
|
|
210
|
+
`ambiguous` (several editors are open and none was named — `details`
|
|
211
|
+
lists them), `noSuchEditor`, `readOnly`, `validation`… The connection
|
|
212
|
+
options (`port`, `token`, `bridgeFile`) override what bridge.json says;
|
|
213
|
+
`LINEA_BRIDGE_TOKEN` supplies a token too.
|
|
338
214
|
|
|
339
215
|
### Interchange notes
|
|
340
216
|
|
|
341
217
|
- DXF/DWG export flattens semantics: block instances are exported as their
|
|
342
|
-
evaluated geometry (no `INSERT` reuse) and
|
|
343
|
-
(DXF
|
|
218
|
+
evaluated geometry (no `INSERT` reuse), and embedded references are
|
|
219
|
+
skipped (DXF can't embed a file). The export result reports both in
|
|
344
220
|
`skipped`.
|
|
345
221
|
- PDF import extracts vector strokes and text runs (dimensions arrive as
|
|
346
222
|
exploded geometry). pdf.js loads lazily on first use, as do the PNG
|
|
@@ -352,10 +228,185 @@ wire form.)
|
|
|
352
228
|
- Dimensions validate their point counts per kind (linear ≥ 2, angular ≥ 3,
|
|
353
229
|
radial/diameter ≥ 2).
|
|
354
230
|
|
|
231
|
+
## 2. Extension development
|
|
232
|
+
|
|
233
|
+
Blocks and commands are plain TypeScript (or JavaScript) modules. The
|
|
234
|
+
package's `lineadraw.d.ts` types the two module names a script may import —
|
|
235
|
+
`"lineadraw"` (`defineBlock`, `defineCommand`) and `"lineadraw/helpers"`
|
|
236
|
+
(geometry helpers) — so authoring type-checks in VS Code, any other
|
|
237
|
+
TypeScript-aware editor, and CI. The editor resolves both names when it
|
|
238
|
+
runs your module; nothing else can be imported. `lineadraw init` writes a
|
|
239
|
+
`tsconfig.json` wired to the typings.
|
|
240
|
+
|
|
241
|
+
A module works on its own too: drop a `.block.ts` / `.cmd.ts` file onto the
|
|
242
|
+
editor, or exercise it headlessly with the SDK
|
|
243
|
+
(`await doc.addBlockDefinitions([source])`, `await doc.runScript(source)`).
|
|
244
|
+
|
|
245
|
+
### Writing a block
|
|
246
|
+
|
|
247
|
+
A block is one module with a single `defineBlock` default export:
|
|
248
|
+
typed parameters in, geometry out.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
// blocks/surface.block.ts
|
|
252
|
+
import { defineBlock } from "lineadraw";
|
|
253
|
+
|
|
254
|
+
export default defineBlock({
|
|
255
|
+
// The id is the block's identity everywhere: @<collection>/<slug>.
|
|
256
|
+
// Re-publishing the same id updates installed copies in place.
|
|
257
|
+
id: "@my-blocks/surface",
|
|
258
|
+
name: "Surface",
|
|
259
|
+
version: "1.0.0",
|
|
260
|
+
params: [
|
|
261
|
+
{ name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
|
|
262
|
+
],
|
|
263
|
+
place: ["Start point", "End point"], // one point pick per label, in order
|
|
264
|
+
draw: ({ params, inputs: [start, end] }) => {
|
|
265
|
+
params.kind; // "rock" | "ground" — inferred from the params table
|
|
266
|
+
return [{ type: "line", a: start, b: end }];
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Notable optional members: `place` may instead be an interactive
|
|
272
|
+
`({ params, pickPoint, pickObject })` function run once at insertion;
|
|
273
|
+
`dependencies` lists ids of blocks your `draw` emits instances of (installs
|
|
274
|
+
pull them along); `paramVisibility({ params })` hides property rows;
|
|
275
|
+
`previewInputs` curates the inputs used for thumbnails. Units are
|
|
276
|
+
millimeters, Y-up, angles in radians CCW.
|
|
277
|
+
|
|
278
|
+
Parameter types (the same rows serve a command's `params`):
|
|
279
|
+
|
|
280
|
+
- `number` — `min`/`max`, `integer`, `step`, `prefix`/`suffix` ("1:",
|
|
281
|
+
"mm"), `presets` (standard values in the field's dropdown).
|
|
282
|
+
- `string` — `placeholder`, `multiline`, `presets`.
|
|
283
|
+
- `boolean` (`display: "toggle"` — a toggle button showing the label,
|
|
284
|
+
compact in a row), `enum` (`options`, each optionally with a `swatch:
|
|
285
|
+
"#rrggbb"` color square; `display: "buttons"` — a segmented toggle group
|
|
286
|
+
for short choices, falling back to a dropdown when it doesn't fit),
|
|
287
|
+
`color` (`"#rrggbb"`, `presets` = a palette).
|
|
288
|
+
- `numberList` (`number[]`, typed Tekla-style: `"0 2*7200"` →
|
|
289
|
+
`[0, 7200, 7200]`) and `stringList` (`string[]`: `'A "B 2"'` →
|
|
290
|
+
`["A", "B 2"]`), both with `presets` and `placeholder` (shown while
|
|
291
|
+
empty).
|
|
292
|
+
- Pickers over the document's resources, valued by id / name:
|
|
293
|
+
`textStyle`, `dimStyle`, `font`, `lineType`, `hatchPattern`.
|
|
294
|
+
- `label` — a section heading (no value); `row` — 2–3 compact params on one
|
|
295
|
+
panel row (`items`, each label a short prefix like "W"); its items are
|
|
296
|
+
ordinary flat params (`params.w`).
|
|
297
|
+
|
|
298
|
+
Every param may carry a `hint` (the label's tooltip). An instance that
|
|
299
|
+
overrides a param shows "Reset" beside it in Properties, back to your
|
|
300
|
+
default. `draw` also receives
|
|
301
|
+
`styles`: `styles.text(id)` / `styles.dim(id)` give a document style (an
|
|
302
|
+
empty or unknown id → the first), for blocks that lay out with real text
|
|
303
|
+
sizes.
|
|
304
|
+
|
|
305
|
+
### Writing a command
|
|
306
|
+
|
|
307
|
+
A command is a macro with a single `defineCommand` default export. Its
|
|
308
|
+
whole run is one undo step; Esc during a pick reverts cleanly. A
|
|
309
|
+
`params` table (the same rows a block's `params` uses) is asked for in a
|
|
310
|
+
dialog before the run and arrives typed as `ctx.params`; headless callers
|
|
311
|
+
pass it by name.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
// commands/circle-here.cmd.ts
|
|
315
|
+
import { defineCommand } from "lineadraw";
|
|
316
|
+
|
|
317
|
+
export default defineCommand({
|
|
318
|
+
id: "@my-blocks/circle-here",
|
|
319
|
+
name: "Circle here",
|
|
320
|
+
inputs: ["Center point"],
|
|
321
|
+
params: [{ name: "radius", type: "number", default: 10, min: 0.1 }],
|
|
322
|
+
// `inputs` are picked in the editor (or passed by a headless caller),
|
|
323
|
+
// `params` asked for in a dialog; `editor` is absent when headless.
|
|
324
|
+
run: async ({ document, editor, inputs: [center], params }) => {
|
|
325
|
+
document.add([{ type: "circle", center, radius: params.radius }]);
|
|
326
|
+
editor?.toast("Done", "success");
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## 3. Marketplace management
|
|
332
|
+
|
|
333
|
+
A marketplace is a git repository of blocks and commands plus a generated
|
|
334
|
+
catalog (`.lineadraw/marketplace.json`) that the editor reads.
|
|
335
|
+
|
|
336
|
+
Scaffold a fresh one:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
npm create lineadraw my-blocks
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Or add the structure to an existing npm project:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm i -D lineadraw
|
|
346
|
+
npx lineadraw init
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Either way you get `blocks/` and `commands/` folders, a `tsconfig.json`
|
|
350
|
+
wired to the typings, a sample block, a CI workflow, the catalog, and
|
|
351
|
+
`npm run manifest` / `npm run check` scripts — the repo is addable in the
|
|
352
|
+
editor from the first push.
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
lineadraw manifest [--dir <path>] Validate all modules and write .lineadraw/marketplace.json
|
|
356
|
+
lineadraw check [--dir <path>] Validate without writing; fail if the committed manifest is stale
|
|
357
|
+
lineadraw init [--dir <path>] Add the marketplace structure to the current npm project
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`manifest` and `check` **dry-run every block** through `lineadraw/sdk` and
|
|
361
|
+
bake preview thumbnails into the catalog. The manifest is deterministic
|
|
362
|
+
(repo-relative paths, no git metadata), so `check` compares the parsed JSON
|
|
363
|
+
— formatting, a trailing newline or CRLF line endings never make it stale.
|
|
364
|
+
The included CI workflow runs it on every push and PR.
|
|
365
|
+
|
|
366
|
+
Collection name and owner come from your package.json:
|
|
367
|
+
|
|
368
|
+
```json
|
|
369
|
+
{ "lineadraw": { "marketplaceName": "My blocks", "marketplaceOwner": "Me" } }
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Using a marketplace in the editor
|
|
373
|
+
|
|
374
|
+
In [the editor](https://lineadraw.com): **Library → Marketplace tab → the
|
|
375
|
+
gear button (Manage marketplaces…) → Add a marketplace** and enter
|
|
376
|
+
`owner/repo` (or `owner/repo@branch`). For a private repository,
|
|
377
|
+
create a fine-grained personal access token with read-only Contents access
|
|
378
|
+
to just that repository and paste it in the add dialog — it is stored only
|
|
379
|
+
on that device. Installing embeds the module (and its declared dependency
|
|
380
|
+
closure) in the document, so drawings keep working offline and travel with
|
|
381
|
+
the file.
|
|
382
|
+
|
|
383
|
+
## 4. CLI for the running desktop app
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
|
|
387
|
+
lineadraw open <file.linea> Open a file in a new window
|
|
388
|
+
lineadraw close [--editor <x>] [--discard]
|
|
389
|
+
lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* operation
|
|
390
|
+
lineadraw tools The operations the bridge offers
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
These commands talk to the running Linea desktop app on `127.0.0.1:7431`
|
|
394
|
+
(the app's `bridge.json` supplies the port and the agent token; `--port`,
|
|
395
|
+
`--token` and `LINEA_BRIDGE_TOKEN` override). `--editor` names a drawing by
|
|
396
|
+
document id, file path or unique name, and may be left out while only one
|
|
397
|
+
is open. Each `call` is one undo step in that editor. From code, use the
|
|
398
|
+
SDK's [`connectEditors()`](#running-editors-the-desktop-bridge).
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
npx lineadraw call linea_add_objects '{"objects":[{"type":"circle","center":[0,0],"radius":500}]}'
|
|
402
|
+
```
|
|
403
|
+
|
|
355
404
|
## Ecosystem
|
|
356
405
|
|
|
357
|
-
- [Linea
|
|
358
|
-
these modules.
|
|
406
|
+
- [Linea](https://lineadraw.com) — the CAD editor, on the desktop and on
|
|
407
|
+
the web, that runs these modules.
|
|
408
|
+
- [`create-lineadraw`](https://www.npmjs.com/package/create-lineadraw) —
|
|
409
|
+
`npm create lineadraw`, the marketplace scaffolder.
|
|
359
410
|
- [`lineadraw/lineadraw`](https://github.com/lineadraw/lineadraw) — the
|
|
360
411
|
public collection: real blocks/commands to learn from, agent skill, and
|
|
361
412
|
guides.
|
package/bin/lineadraw.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// `call`, `tools`. Reads the desktop's bridge.json for the port
|
|
11
11
|
// and the agent token; `--port` / `--token` / LINEA_BRIDGE_TOKEN override.
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
13
|
import { isDeepStrictEqual } from "node:util";
|
|
15
14
|
import { dirname, join, resolve } from "node:path";
|
|
16
15
|
import {
|
|
@@ -67,130 +66,36 @@ const parseArgs = (argv) => {
|
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
// --- bridge --------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const DEFAULT_PORT = 7431;
|
|
73
|
-
|
|
74
|
-
const bridgeFilePath = () => {
|
|
75
|
-
const home = homedir();
|
|
76
|
-
if (process.platform === "win32")
|
|
77
|
-
return join(
|
|
78
|
-
process.env.APPDATA ?? join(home, "AppData", "Roaming"),
|
|
79
|
-
APP_ID,
|
|
80
|
-
"bridge.json",
|
|
81
|
-
);
|
|
82
|
-
if (process.platform === "darwin")
|
|
83
|
-
return join(home, "Library", "Application Support", APP_ID, "bridge.json");
|
|
84
|
-
return join(
|
|
85
|
-
process.env.XDG_DATA_HOME ?? join(home, ".local", "share"),
|
|
86
|
-
APP_ID,
|
|
87
|
-
"bridge.json",
|
|
88
|
-
);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const readBridgeFile = () => {
|
|
92
|
-
const path = bridgeFilePath();
|
|
93
|
-
if (!existsSync(path)) return {};
|
|
94
|
-
try {
|
|
95
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
96
|
-
} catch {
|
|
97
|
-
return {};
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const bridge = (flags) => {
|
|
102
|
-
const file = readBridgeFile();
|
|
103
|
-
const port =
|
|
104
|
-
typeof flags.port === "string"
|
|
105
|
-
? Number(flags.port)
|
|
106
|
-
: file.port || DEFAULT_PORT;
|
|
107
|
-
const token =
|
|
108
|
-
(typeof flags.token === "string" ? flags.token : undefined) ??
|
|
109
|
-
process.env.LINEA_BRIDGE_TOKEN ??
|
|
110
|
-
file.token;
|
|
111
|
-
const headers = { "content-type": "application/json" };
|
|
112
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
113
|
-
return { base: `http://127.0.0.1:${port}`, headers };
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const request = async (b, method, path, body) => {
|
|
117
|
-
let response;
|
|
118
|
-
try {
|
|
119
|
-
response = await fetch(`${b.base}${path}`, {
|
|
120
|
-
method,
|
|
121
|
-
headers: b.headers,
|
|
122
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
123
|
-
});
|
|
124
|
-
} catch (e) {
|
|
125
|
-
const cause = e?.cause?.code ?? e?.cause?.message ?? e?.message ?? e;
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Nothing answers on ${b.base} (${cause}). Is the Linea desktop app running?`,
|
|
128
|
-
{ cause: e },
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
const text = await response.text();
|
|
132
|
-
let parsed = text;
|
|
133
|
-
try {
|
|
134
|
-
parsed = JSON.parse(text);
|
|
135
|
-
} catch {
|
|
136
|
-
// not JSON: keep the text
|
|
137
|
-
}
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
const error = parsed?.error;
|
|
140
|
-
throw new Error(
|
|
141
|
-
error?.message ?? `HTTP ${response.status}: ${text.slice(0, 200)}`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return parsed;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const editorQuery = (flags) =>
|
|
148
|
-
typeof flags.editor === "string"
|
|
149
|
-
? `?editor=${encodeURIComponent(flags.editor)}`
|
|
150
|
-
: "";
|
|
69
|
+
// The client is lineadraw/sdk's (src/bridge.ts), built on its own as
|
|
70
|
+
// dist/bridge.js so these commands never load the kernel.
|
|
151
71
|
|
|
152
72
|
const bridgeCommand = async (cmd, positional, flags) => {
|
|
153
|
-
const
|
|
73
|
+
const { createBridge } = await import("../dist/bridge.js");
|
|
74
|
+
const b = createBridge({
|
|
75
|
+
port: typeof flags.port === "string" ? Number(flags.port) : undefined,
|
|
76
|
+
token: typeof flags.token === "string" ? flags.token : undefined,
|
|
77
|
+
});
|
|
78
|
+
const editor = typeof flags.editor === "string" ? flags.editor : undefined;
|
|
154
79
|
switch (cmd) {
|
|
155
80
|
case "status":
|
|
156
|
-
return
|
|
81
|
+
return b.status();
|
|
157
82
|
case "open": {
|
|
158
83
|
const file = positional[0];
|
|
159
84
|
if (!file) throw new Error("open needs a .linea file path");
|
|
160
|
-
return
|
|
161
|
-
path: resolve(file),
|
|
162
|
-
});
|
|
85
|
+
return b.open(file);
|
|
163
86
|
}
|
|
164
87
|
case "close":
|
|
165
|
-
return
|
|
166
|
-
b,
|
|
167
|
-
"POST",
|
|
168
|
-
`/api/linea_close_document${editorQuery(flags)}`,
|
|
169
|
-
{ discard: flags.discard === true },
|
|
170
|
-
);
|
|
88
|
+
return b.editor(editor).close({ discard: flags.discard === true });
|
|
171
89
|
case "call": {
|
|
172
90
|
const [operation, json] = positional;
|
|
173
91
|
if (!operation)
|
|
174
92
|
throw new Error("call needs an operation name (see `lineadraw tools`)");
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
b,
|
|
178
|
-
"POST",
|
|
179
|
-
`/api/${encodeURIComponent(operation)}${editorQuery(flags)}`,
|
|
180
|
-
payload,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
case "tools": {
|
|
184
|
-
const reply = await request(b, "POST", "/mcp", {
|
|
185
|
-
jsonrpc: "2.0",
|
|
186
|
-
id: 1,
|
|
187
|
-
method: "tools/list",
|
|
93
|
+
return b.call(operation, json === undefined ? {} : JSON.parse(json), {
|
|
94
|
+
editor,
|
|
188
95
|
});
|
|
189
|
-
return (reply.result?.tools ?? []).map((t) => ({
|
|
190
|
-
name: t.name,
|
|
191
|
-
description: t.description,
|
|
192
|
-
}));
|
|
193
96
|
}
|
|
97
|
+
case "tools":
|
|
98
|
+
return b.tools();
|
|
194
99
|
default:
|
|
195
100
|
throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
|
|
196
101
|
}
|