lineadraw 0.9.0 → 0.11.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.
Files changed (36) hide show
  1. package/README.md +154 -20
  2. package/bin/lineadraw.mjs +60 -10
  3. package/dist/bridge.d.ts +1 -0
  4. package/dist/bridge.js +1 -1
  5. package/dist/{chunk-BQCSFVAF.js → chunk-7UFXC7OI.js} +5570 -1174
  6. package/dist/chunk-7UFXC7OI.js.map +1 -0
  7. package/dist/chunk-FDLANLIC.js +1161 -0
  8. package/dist/chunk-FDLANLIC.js.map +1 -0
  9. package/dist/chunk-M3EDRSHW.js +81 -0
  10. package/dist/chunk-M3EDRSHW.js.map +1 -0
  11. package/dist/chunk-MYP7RUYK.js +9 -0
  12. package/dist/chunk-MYP7RUYK.js.map +1 -0
  13. package/dist/{chunk-L2CWZAKW.js → chunk-RNA2RDGL.js} +13 -6
  14. package/dist/chunk-RNA2RDGL.js.map +1 -0
  15. package/dist/dist-56K6CNML.js +188 -0
  16. package/dist/dist-56K6CNML.js.map +1 -0
  17. package/dist/dist-IEN64QSV.js +10 -0
  18. package/dist/dist-IEN64QSV.js.map +1 -0
  19. package/dist/emscripten-module-Q67P5WYC-ITCU5LXL.js +2424 -0
  20. package/dist/emscripten-module-Q67P5WYC-ITCU5LXL.js.map +1 -0
  21. package/dist/{exportDxf-T2RTSTH5.js → exportDxf-6XTXQ4XN.js} +2 -2
  22. package/dist/ffi-SHMEF5YV.js +82 -0
  23. package/dist/ffi-SHMEF5YV.js.map +1 -0
  24. package/dist/module-ES6BEMUI-BJFWC656.js +14 -0
  25. package/dist/module-ES6BEMUI-BJFWC656.js.map +1 -0
  26. package/dist/module-asyncify-2EFITU5U-7F5WZXRA.js +9 -0
  27. package/dist/module-asyncify-2EFITU5U-7F5WZXRA.js.map +1 -0
  28. package/dist/sdk.d.ts +1118 -128
  29. package/dist/sdk.js +742 -620
  30. package/dist/sdk.js.map +1 -1
  31. package/lineadraw.d.ts +666 -44
  32. package/package.json +1 -1
  33. package/templates/README.md +6 -4
  34. package/dist/chunk-BQCSFVAF.js.map +0 -1
  35. package/dist/chunk-L2CWZAKW.js.map +0 -1
  36. /package/dist/{exportDxf-T2RTSTH5.js.map → exportDxf-6XTXQ4XN.js.map} +0 -0
package/README.md CHANGED
@@ -59,9 +59,13 @@ await doc.exportPdf("plan.pdf");
59
59
  its objects; an object's own `opacity` overrides it. Paints across canvas,
60
60
  SVG, and PDF; DXF/DWG export drops it (no direct mapping).
61
61
  - **Annotation scale is per-object**: for a 1:N drawing set the top-level
62
- `scale: N` on text, dimensions, and hatches, keeping `textHeight` at
63
- paper size (2.5 body / 3.5 / 5 titles). `scale` is NOT a `styleOverride`
64
- key; `add`/`update` reject it there.
62
+ `scale: N` on text, dimensions, and hatches, keeping the text height
63
+ at paper size (2.5 body / 3.5 / 5 titles). `scale` is NOT a
64
+ `styleOverride` key; `add`/`update` reject it there.
65
+ - **One shape per field.** Style keys of a text, table or dimension
66
+ (`textHeight`, `font`, `bold`, `arrowSize`, …) live in `styleOverride`
67
+ (or a named style via `styleId`), never at the top level; a hidden layer
68
+ is `visible: false`. Unknown fields throw, naming where they belong.
65
69
  - **Reads are detached snapshots**: `doc.document`, `doc.objects`,
66
70
  `getObject`, `listObjects()` return deep clones — mutating them does nothing.
67
71
  - **New documents are layout-less**; `exportPdf` of a layout-less document
@@ -113,7 +117,9 @@ appended when missing):
113
117
  configuration, never written to the file.
114
118
  - **Scripts**: `await runScript(source, inputs?, params?)` runs a
115
119
  `defineCommand` module (the same one the editor palette runs) headlessly
116
- as ONE undo step — its declared `inputs` (points) are supplied up front,
120
+ as ONE undo step — its declared `inputs` are supplied up front (by name
121
+ for an inputs table: `{ origin: [0, 0], doors: [id] }`; points in order
122
+ for labels),
117
123
  its declared `params` by name (`{ count: 3 }`; omitted ones take their
118
124
  defaults, unknown names and wrong types throw); `runUserCommand(id, inputs?,
119
125
  params?)` runs one of the document's own commands. Returns the script's
@@ -144,7 +150,10 @@ Full parameter docs are in the bundled `dist/sdk.d.ts` JSDoc.
144
150
 
145
151
  `line` (a, b) · `polyline` (points with optional bulge, closed) · `circle` ·
146
152
  `arc` (CCW start→end) · `ellipse` (center, majorAxis vector, ratio) ·
147
- `point` (p) · `hatch` (loops — outer + holes, even-odd; fill solid/pattern/lines) ·
153
+ `point` (p) · `hatch` (loops — outer + holes, even-odd; each loop a closed
154
+ chain of `edges`: `line`, `arc`, `ellipse` or `spline`, with the fields of that
155
+ object type — a circle, ellipse or closed spline is one edge; `hatchLoop(points)`
156
+ builds a polygon / bulged loop; fill solid/pattern/lines) ·
148
157
  `text` (position, content; **default alignment center/center**; optional
149
158
  leaderLines with the arrowhead at index 0) · `dimension` (linear/angular/
150
159
  radial/diameter; linear: `offset` is the signed distance of the dimension
@@ -260,16 +269,32 @@ export default defineBlock({
260
269
  params: [
261
270
  { name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
262
271
  ],
263
- place: ["Start point", "End point"], // one point pick per label, in order
264
- draw: ({ params, inputs: [start, end] }) => {
272
+ // What the user picks at insertion, read by name in `draw`.
273
+ inputs: [
274
+ { name: "start", type: "point", label: "Start point" },
275
+ { name: "end", type: "point", label: "End point" },
276
+ ],
277
+ draw: ({ params, inputs: { start, end } }) => {
265
278
  params.kind; // "rock" | "ground" — inferred from the params table
266
279
  return [{ type: "line", a: start, b: end }];
267
280
  },
268
281
  });
269
282
  ```
270
283
 
271
- Notable optional members: `place` may instead be an interactive
272
- `({ params, pickPoint, pickObject })` function run once at insertion;
284
+ Input kinds: `point`; `points` — a chain picked until Enter (`min`,
285
+ `max`, `closed` for a polygon) arriving as `Vec2[]`; `object` / `objects`
286
+ — picked objects filtered by `types` (`["line", "polyline"]`), arriving in
287
+ `draw` as the objects themselves (re-evaluated when they change; a
288
+ deleted one is `null` / dropped). `point` and `object` may be `optional`
289
+ (Enter skips). The first point is the pivot. Instances store their inputs
290
+ by name, so reordering or adding inputs never breaks placed instances —
291
+ removing or renaming one does. (The older `place: ["Start", "End"]` labels
292
+ form, with `draw` reading `inputs` as an array, still works.)
293
+
294
+ Notable optional members: `place` — an interactive
295
+ `({ params, pickPoint, pickPoints, pickObject, pickObjects })` function run
296
+ once at insertion, for conditional or chained picks; with an `inputs`
297
+ table it returns the inputs by name;
273
298
  `dependencies` lists ids of blocks your `draw` emits instances of (installs
274
299
  pull them along); `paramVisibility({ params })` hides property rows;
275
300
  `previewInputs` curates the inputs used for thumbnails. Units are
@@ -294,6 +319,26 @@ Parameter types (the same rows serve a command's `params`):
294
319
  - `label` — a section heading (no value); `row` — 2–3 compact params on one
295
320
  panel row (`items`, each label a short prefix like "W"); its items are
296
321
  ordinary flat params (`params.w`).
322
+ - `data` — structured data the block owns: any JSON value, typed by its
323
+ default (`default: {} as Record<string, Wall>`). It has no Properties row;
324
+ handles, handle menus and actions edit it (agents set it like any param).
325
+
326
+ Geometry helpers come from `"lineadraw/helpers"` — vector math
327
+ (`add`, `sub`, `scale`, `rotate`, `polar`, …) and polygon booleans:
328
+ `union(...polygons)`, `difference(subject, ...clips)`,
329
+ `intersection(...polygons)`. A polygon is a ring of points, or rings with
330
+ holes (first the outer boundary). The result is a flat list of rings,
331
+ ready for closed polylines or ONE hatch, which fills even-odd so holes
332
+ stay holes:
333
+
334
+ ```ts
335
+ const solid = union(...walls.map(wallRect)); // overlapping walls → clean outlines
336
+ return [
337
+ { type: "hatch", loops: solid.map(hatchLoop), // a ring → line edges
338
+ fill: { kind: "pattern", name: "ANSI31", angle: 0, scale: 20 } },
339
+ ...solid.map((points) => ({ type: "polyline", points, closed: true })),
340
+ ];
341
+ ```
297
342
 
298
343
  Every param may carry a `hint` (the label's tooltip). An instance that
299
344
  overrides a param shows "Reset" beside it in Properties, back to your
@@ -302,10 +347,94 @@ default. `draw` also receives
302
347
  empty or unknown id → the first), for blocks that lay out with real text
303
348
  sizes.
304
349
 
350
+ #### Hosted blocks
351
+
352
+ A block can LIVE ON another instance: a door on a floor plan. Give it a
353
+ `host` input; it then draws in the host's frame (moving, rotating and
354
+ copying with it, and deleted with it), and the two talk through
355
+ `expose`, JSON each publishes from its OWN params:
356
+
357
+ ```ts
358
+ // The plan publishes its walls and cuts them where its openings say.
359
+ expose: ({ params }) => ({ walls: wallTable(params) }),
360
+ draw: ({ params, hosted }) => {
361
+ const cuts = hosted.map((h) => h.exposed?.cut).filter(Boolean);
362
+ return wallSolids(params, cuts);
363
+ },
364
+
365
+ // A door: picked by clicking a plan's wall; `attach` turns the click
366
+ // (plan-local) into its params.
367
+ inputs: [{ name: "plan", type: "host", types: ["@my-blocks/floor-plan"] }],
368
+ expose: ({ params }) => ({ cut: { wall: params.wall, at: params.at, width: params.width } }),
369
+ attach: ({ host, point }) => nearestWall(host.walls, point), // → { wall, at }
370
+ draw: ({ params, host }) => doorLeaf(host.walls[params.wall], params),
371
+ ```
372
+
373
+ `expose` never sees the host or what's on it, so the two can't loop
374
+ (ADR 0008). What a host publishes and what its openings read is a
375
+ contract between the modules: document it.
376
+
377
+ A hosted block has no host in Library / marketplace previews. Give it a
378
+ stand-in with `previewHost`: the JSON a host's `expose` would return.
379
+ The preview runs `attach` as if the host were clicked at its origin, and
380
+ `draw` gets the stand-in as `host` (the save-time dry run uses it too):
381
+
382
+ ```ts
383
+ previewHost: { preview: true, walls: { A: { a: [-1500, 0], b: [1500, 0], t: 200 } } },
384
+ // …and in draw, `host.preview` can add context: a stretch of wall.
385
+ ```
386
+
387
+ #### Handles and actions
388
+
389
+ `handles({ params, inputs })` returns the handles a selected instance
390
+ shows, in block-local coordinates. It re-runs as values change, so a
391
+ handle can sit at the current width:
392
+
393
+ ```ts
394
+ handles: ({ params }) => [
395
+ // Drag along local X: the distance becomes `width` (clamped to its min/max).
396
+ { id: "width", at: [params.width, 0], shape: "triangle",
397
+ along: { origin: [0, 0], direction: [1, 0] }, param: "width" },
398
+ // A button: one click, one undo step.
399
+ { id: "flip", at: [params.width / 2, 150], fixed: true, icon: "flip-horizontal",
400
+ click: ({ params }) => ({ params: { swing: params.swing === "left" ? "right" : "left" } }) },
401
+ // A line handle (shown while hovered) with a hover menu.
402
+ { id: "wall", kind: "line", from: [0, 0], to: [params.width, 0], fixed: true,
403
+ menu: ["thickness",
404
+ { field: { type: "boolean", label: "Enabled" }, value: !params.off,
405
+ set: (on) => ({ params: { off: !on } }) }],
406
+ actions: [{ label: "Reset", icon: "x", run: () => ({ params: { off: false } }) }] },
407
+ ],
408
+ // The instance's context menu (right-click): pick a point, then run.
409
+ actions: [
410
+ { label: "Add mark", pick: "point",
411
+ run: ({ params, point }) => ({ params: { marks: [...params.marks, [point.x, point.y]] } }) },
412
+ ],
413
+ ```
414
+
415
+ A handle has ONE drag behaviour: `param` (the constrained value → a number
416
+ param), `input` (moves a point input, replacing its default handle), or
417
+ `drag` (a function returning a patch). Otherwise it is `fixed`. The
418
+ constraint is free, `along` an axis (value = distance) or `around` a
419
+ centre (value = angle in radians). A line handle moves along its normal by
420
+ default. `click` turns it into a button. Shapes are `square`, `circle`,
421
+ `diamond`, `triangle`, `arrow`, `double-arrow`, `plus` and `minus`, and `icon` names a
422
+ lucide glyph from a small set. Handle functions and actions return a PATCH
423
+ — `{ params?, inputs? }`, with inputs by name and block-local — and never
424
+ mutate anything. Each applies as one undo step. The save-time dry run
425
+ checks every handle record.
426
+
305
427
  ### Writing a command
306
428
 
307
429
  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
430
+ whole run is one undo step; Esc during a pick reverts cleanly. Its
431
+ `inputs` table is the same one blocks declare; `object` / `objects` inputs
432
+ take the current selection when it fits (types and count) and arrive as
433
+ ids. To ask more DURING the run (the next question depends on the last
434
+ answer), use `editor.pickPoint` / `pickObject` / `prompt`, or
435
+ `editor.ask(paramsTable, { title })` for several fields in one dialog —
436
+ and declare `requiresEditor: true`, so CLI / agent runs refuse the command
437
+ up front instead of failing halfway. A
309
438
  `params` table (the same rows a block's `params` uses) is asked for in a
310
439
  dialog before the run and arrives typed as `ctx.params`; headless callers
311
440
  pass it by name.
@@ -317,11 +446,12 @@ import { defineCommand } from "lineadraw";
317
446
  export default defineCommand({
318
447
  id: "@my-blocks/circle-here",
319
448
  name: "Circle here",
320
- inputs: ["Center point"],
449
+ inputs: [{ name: "center", type: "point", label: "Center point" }],
321
450
  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 }) => {
451
+ // `inputs` are picked in the editor (or passed by name by a headless
452
+ // caller: { center: [0, 0] }), `params` asked for in a dialog; `editor`
453
+ // is absent when headless.
454
+ run: async ({ document, editor, inputs: { center }, params }) => {
325
455
  document.add([{ type: "circle", center, radius: params.radius }]);
326
456
  editor?.toast("Done", "success");
327
457
  },
@@ -371,11 +501,11 @@ Collection name and owner come from your package.json:
371
501
 
372
502
  ### Using a marketplace in the editor
373
503
 
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,
504
+ In [Linea](https://lineadraw.com): **App settings → Marketplaces → Add a
505
+ marketplace** (the gear in Library → Marketplace opens the same tab) and
506
+ enter `owner/repo` (or `owner/repo@branch`). For a private repository,
377
507
  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
508
+ to just that repository and paste it under "Add an access token" — it is stored only
379
509
  on that device. Installing embeds the module (and its declared dependency
380
510
  closure) in the document, so drawings keep working offline and travel with
381
511
  the file.
@@ -386,8 +516,9 @@ the file.
386
516
  lineadraw status App version, port, the open drawings: id, name, path, kind, dirty
387
517
  lineadraw open <file.linea> Open a file in a new window
388
518
  lineadraw close [--editor <x>] [--discard]
389
- lineadraw call <operation> ['<json>'] [--editor <x>] Run one linea_* operation
390
- lineadraw tools The operations the bridge offers
519
+ lineadraw call <operation> ['<json>' | @file.json | -] [--editor <x>] Run one linea_* operation
520
+ lineadraw tools [<operation>] The operations the bridge offers, or one's argument schema
521
+ lineadraw --version
391
522
  ```
392
523
 
393
524
  These commands talk to the running Linea desktop app on `127.0.0.1:7431`
@@ -401,6 +532,9 @@ SDK's [`connectEditors()`](#running-editors-the-desktop-bridge).
401
532
  npx lineadraw call linea_add_objects '{"objects":[{"type":"circle","center":[0,0],"radius":500}]}'
402
533
  ```
403
534
 
535
+ The arguments can also come from a file (`@args.json`) or stdin (`-`),
536
+ which spares quoting JSON in PowerShell or cmd.
537
+
404
538
  ## Ecosystem
405
539
 
406
540
  - [Linea](https://lineadraw.com) — the CAD editor, on the desktop and on
package/bin/lineadraw.mjs CHANGED
@@ -12,6 +12,7 @@
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { isDeepStrictEqual } from "node:util";
14
14
  import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
15
16
  import {
16
17
  buildManifest,
17
18
  manifestJson,
@@ -35,12 +36,42 @@ Running editors (the Linea desktop app on 127.0.0.1:7431):
35
36
  status app version, port, the open drawings (id, name, path, kind, dirty)
36
37
  open <file.linea> open a file in a window of its own (returns its editor)
37
38
  close [--editor <x>] [--discard] close an editor like its window's x
38
- call <operation> ['<json>'] [--editor <x>]
39
- run one linea_* operation (see tools)
40
- tools the operations the bridge offers
39
+ call <operation> ['<json>' | @file.json | -] [--editor <x>]
40
+ run one linea_* operation (see tools);
41
+ its arguments as JSON text, a file, or stdin
42
+ tools [<operation>] the operations the bridge offers, or
43
+ one operation's argument schema
41
44
 
42
45
  The desktop's bridge.json (port, agent token) is read automatically;
43
- LINEA_BRIDGE_TOKEN or --token override the token, --port the port.`;
46
+ LINEA_BRIDGE_TOKEN or --token override the token, --port the port.
47
+
48
+ --version the lineadraw version --help this text`;
49
+
50
+ const VERSION = JSON.parse(
51
+ readFileSync(
52
+ join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
53
+ "utf8",
54
+ ),
55
+ ).version;
56
+
57
+ /** `call`'s arguments: JSON text, `@file.json`, or `-` for stdin (quoting
58
+ * JSON on a Windows command line is a fight best not picked). */
59
+ const readCallArgs = (spec) => {
60
+ if (spec === undefined) return {};
61
+ const text =
62
+ spec === "-"
63
+ ? readFileSync(0, "utf8")
64
+ : spec.startsWith("@")
65
+ ? readFileSync(resolve(spec.slice(1)), "utf8")
66
+ : spec;
67
+ try {
68
+ return JSON.parse(text);
69
+ } catch (e) {
70
+ throw new Error(
71
+ `call: the arguments are not valid JSON (${e instanceof Error ? e.message : e})`,
72
+ );
73
+ }
74
+ };
44
75
 
45
76
  const args = process.argv.slice(2);
46
77
  const command = args[0];
@@ -90,12 +121,20 @@ const bridgeCommand = async (cmd, positional, flags) => {
90
121
  const [operation, json] = positional;
91
122
  if (!operation)
92
123
  throw new Error("call needs an operation name (see `lineadraw tools`)");
93
- return b.call(operation, json === undefined ? {} : JSON.parse(json), {
94
- editor,
95
- });
124
+ return b.call(operation, readCallArgs(json), { editor });
125
+ }
126
+ case "tools": {
127
+ const tools = await b.tools();
128
+ const wanted = positional[0];
129
+ if (wanted === undefined)
130
+ return tools.map(({ name, description }) => ({ name, description }));
131
+ const tool = tools.find((t) => t.name === wanted);
132
+ if (!tool)
133
+ throw new Error(
134
+ `Unknown operation "${wanted}". \`lineadraw tools\` lists them.`,
135
+ );
136
+ return tool;
96
137
  }
97
- case "tools":
98
- return b.tools();
99
138
  default:
100
139
  throw new Error(`Unknown command: ${cmd}\n\n${USAGE}`);
101
140
  }
@@ -184,8 +223,19 @@ switch (command) {
184
223
  }
185
224
  break;
186
225
  }
226
+ case "--version":
227
+ case "-v":
228
+ console.log(VERSION);
229
+ break;
230
+ case "--help":
231
+ case "-h":
232
+ case "help":
233
+ console.log(USAGE);
234
+ break;
187
235
  default: {
188
- if (!BRIDGE_COMMANDS.includes(command)) fail(USAGE);
236
+ if (command === undefined) fail(USAGE);
237
+ if (!BRIDGE_COMMANDS.includes(command))
238
+ fail(`Unknown command "${command}".\n\n${USAGE}`);
189
239
  const { positional, flags } = parseArgs(args.slice(1));
190
240
  try {
191
241
  const result = await bridgeCommand(command, positional, flags);
package/dist/bridge.d.ts CHANGED
@@ -78,6 +78,7 @@ interface LineaBridge {
78
78
  tools(): Promise<{
79
79
  name: string;
80
80
  description?: string;
81
+ inputSchema?: unknown;
81
82
  }[]>;
82
83
  /** Open a `.linea` file in a window of its own (an absolute path, or one
83
84
  * relative to the current directory); resolves once it has loaded. */
package/dist/bridge.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  bridgeFilePath,
5
5
  connectEditors,
6
6
  createBridge
7
- } from "./chunk-L2CWZAKW.js";
7
+ } from "./chunk-RNA2RDGL.js";
8
8
  import "./chunk-PZ5AY32C.js";
9
9
  export {
10
10
  BridgeError,