@itsthecsguy/linework 0.2.0 → 0.2.1
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 +17 -1
- package/dist/cli.js +0 -0
- package/dist/commands.js +1 -1
- package/dist/drawio/write.js +68 -4
- package/dist/layout/layout.js +4 -1
- package/dist/layout/polish.js +112 -22
- package/dist/mcp/server.js +2 -2
- package/package.json +2 -1
- package/skills/linework/references/spec.md +1 -1
- package/skills/linework/references/style-guide.md +1 -1
package/README.md
CHANGED
|
@@ -258,7 +258,7 @@ Docs can't hold an SVG, so the diagram lives in Google Drive and draw.io's Googl
|
|
|
258
258
|
|
|
259
259
|
## Status
|
|
260
260
|
|
|
261
|
-
All
|
|
261
|
+
All five phases of [PLAN.md](PLAN.md) are built. Two paths haven't been tried yet, because they need a real account or app: the Google Docs add-on round trip, and saving from the VS Code Draw.io Integration extension. Both use the same file format and protocol that the tests cover with draw.io's own editor.
|
|
262
262
|
|
|
263
263
|
## Development
|
|
264
264
|
|
|
@@ -269,4 +269,20 @@ npm run build # compile to dist/
|
|
|
269
269
|
npm run dev -- build examples/checkout.yaml -o out/checkout.drawio.svg --png out/checkout.png
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
+
Every push to `main` and every pull request runs the typecheck and the tests on Node 22 and 24 in GitHub Actions. To run the suite with a browser that isn't in one of the usual places, set `LINEWORK_CHROME=/path/to/chromium`.
|
|
273
|
+
|
|
274
|
+
After a change to layout or rendering, rebuild the README gallery and look at what moved:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
npm run build && node docs/gallery/build.mjs
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Releasing
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
npm run release # patch; or: npm run release -- minor | major | 1.2.3
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
That bumps the version, commits it as `Release x.y.z`, tags `vx.y.z` and pushes both. The tag starts the release workflow, which runs the tests again, publishes to npm and opens a GitHub release with generated notes. Nobody logs in to npm: the workflow publishes through npm's [trusted publishing](https://docs.npmjs.com/trusted-publishers), which is set up once in the package's settings on npmjs.com (GitHub Actions, repository `itsthecsguy/linework`, workflow `release.yml`).
|
|
287
|
+
|
|
272
288
|
`assets/drawio` holds the parts of the draw.io web app that linework runs: the renderer it uses headless, and the editor behind `linework edit`. draw.io is Apache-2.0; see [NOTICE](NOTICE). To update it, check out a jgraph/drawio tag in `vendor/drawio` and run `npm run assets`.
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/commands.js
CHANGED
|
@@ -32,7 +32,7 @@ Usage:
|
|
|
32
32
|
Open the diagram in draw.io (bundled, offline) in your browser. Saving in
|
|
33
33
|
the editor writes the file. Stops when you exit the editor or close the tab.
|
|
34
34
|
linework import <file.mmd|doc.md|-> [--block N] [-o out.drawio.svg] [--png p.png]
|
|
35
|
-
Convert a Mermaid diagram (flowchart, state, sequence, ER) to
|
|
35
|
+
Convert a Mermaid diagram (flowchart, state, sequence, ER) to a linework spec.
|
|
36
36
|
Prints the spec, or builds it straight away with -o.
|
|
37
37
|
linework icons <aws|gcp|k8s|azure> [filter]
|
|
38
38
|
List icon names, for "icon: aws.lambda" on architecture nodes.
|
package/dist/drawio/write.js
CHANGED
|
@@ -175,6 +175,23 @@ export function sceneFromLayout(d, l) {
|
|
|
175
175
|
vertices.push(...nodeChildren(d, n, box));
|
|
176
176
|
}
|
|
177
177
|
const isCyl = (id) => { const n = d.nodes.find((x) => x.id === id); return !!n && kindDef(d.type, n.kind).shape === 'cylinder'; };
|
|
178
|
+
const isDecision = (id) => { const n = d.nodes.find((x) => x.id === id); return !!n && kindDef(d.type, n.kind).shape === 'diamond'; };
|
|
179
|
+
// How far an edge shares its first run with a sibling leaving the same point.
|
|
180
|
+
const sharedStem = (id, from) => {
|
|
181
|
+
const mine = l.edges.get(id).points;
|
|
182
|
+
let shared = 0;
|
|
183
|
+
for (const o of d.edges) {
|
|
184
|
+
const pts = o.id !== id && o.from === from && o.to !== from ? l.edges.get(o.id)?.points : undefined;
|
|
185
|
+
if (!pts || pts.length < 2 || mine.length < 2 || Math.hypot(pts[0].x - mine[0].x, pts[0].y - mine[0].y) > 1)
|
|
186
|
+
continue;
|
|
187
|
+
const dir = (p) => [Math.sign(Math.round(p[1].x - p[0].x)), Math.sign(Math.round(p[1].y - p[0].y))].join();
|
|
188
|
+
if (dir(pts) !== dir(mine))
|
|
189
|
+
continue;
|
|
190
|
+
const len = (p) => Math.hypot(p[1].x - p[0].x, p[1].y - p[0].y);
|
|
191
|
+
shared = Math.max(shared, Math.min(len(pts), len(mine)));
|
|
192
|
+
}
|
|
193
|
+
return shared;
|
|
194
|
+
};
|
|
178
195
|
const edges = d.edges.map((e) => {
|
|
179
196
|
const le = l.edges.get(e.id);
|
|
180
197
|
const cyl = { from: isCyl(e.from), to: isCyl(e.to) };
|
|
@@ -197,7 +214,7 @@ export function sceneFromLayout(d, l) {
|
|
|
197
214
|
...base,
|
|
198
215
|
style: base.style + portStyle(le.points, abs(e.from), abs(e.to), cyl),
|
|
199
216
|
points: le.points.slice(1, -1),
|
|
200
|
-
labelX: le.label && e.label ? (labelSpot(le.points, le.label, l.direction) ?? labelFraction(le.points, { x: le.label.x + le.label.w / 2, y: le.label.y + le.label.h / 2 })) : undefined,
|
|
217
|
+
labelX: le.label && e.label ? ((isDecision(e.from) ? branchSpot(le.points, le.label, l.direction, sharedStem(e.id, e.from)) : undefined) ?? labelSpot(le.points, le.label, l.direction, [...l.nodes].filter(([id]) => id !== e.from && id !== e.to).map(([, b]) => b)) ?? labelFraction(le.points, { x: le.label.x + le.label.w / 2, y: le.label.y + le.label.h / 2 })) : undefined,
|
|
201
218
|
};
|
|
202
219
|
});
|
|
203
220
|
alignLabels(d, l, edges);
|
|
@@ -334,7 +351,7 @@ export function toMxfile(d, l) {
|
|
|
334
351
|
* source node, which reads as a caption of that node. Returns draw.io's -1..1
|
|
335
352
|
* fraction, or undefined when no run is long enough.
|
|
336
353
|
*/
|
|
337
|
-
function labelSpot(pts, label, dir) {
|
|
354
|
+
export function labelSpot(pts, label, dir, avoid = []) {
|
|
338
355
|
let total = 0;
|
|
339
356
|
let best;
|
|
340
357
|
// Runs along the flow belong to one edge; runs across it are often shared
|
|
@@ -348,10 +365,57 @@ function labelSpot(pts, label, dir) {
|
|
|
348
365
|
const along = dir ? vertical === flowVertical : true;
|
|
349
366
|
const better = !best || (along && !best.along) || (along === best.along && room > best.room);
|
|
350
367
|
if (room >= 0 && better)
|
|
351
|
-
best = { room, at: total + len / 2, along };
|
|
368
|
+
best = { room, at: total + len / 2, along, a, b, start: total, len };
|
|
352
369
|
total += len;
|
|
353
370
|
}
|
|
354
|
-
|
|
371
|
+
if (!best || !total)
|
|
372
|
+
return undefined;
|
|
373
|
+
// Halfway along a long run can be right beside a box the edge only passes, where
|
|
374
|
+
// the label reads as that box's caption. Slide along the run to the nearest
|
|
375
|
+
// spot past the box, with a little air; stay in the middle when there is none.
|
|
376
|
+
const { a, b, start, len } = best;
|
|
377
|
+
const vertical = Math.abs(b.x - a.x) < Math.abs(b.y - a.y);
|
|
378
|
+
const half = (vertical ? label.h : label.w) / 2 + 10;
|
|
379
|
+
// Beside: level with the box along the run, and within 12px of it across the run.
|
|
380
|
+
const beside = (at, air) => {
|
|
381
|
+
const t = (at - start) / len, cx = a.x + t * (b.x - a.x), cy = a.y + t * (b.y - a.y);
|
|
382
|
+
const px = vertical ? 12 : air, py = vertical ? air : 12;
|
|
383
|
+
const r = { x: cx - label.w / 2 - px, y: cy - label.h / 2 - py, w: label.w + 2 * px, h: label.h + 2 * py };
|
|
384
|
+
return avoid.some((o) => r.x < o.x + o.w && r.x + r.w > o.x && r.y < o.y + o.h && r.y + r.h > o.y);
|
|
385
|
+
};
|
|
386
|
+
// Level by a few px or more: a label that only grazes the box's end is fine where it is.
|
|
387
|
+
if (beside(best.at, -4))
|
|
388
|
+
for (let step = 4; step <= len / 2 - half; step += 4)
|
|
389
|
+
for (const at of [best.at - step, best.at + step])
|
|
390
|
+
if (!beside(at, 4))
|
|
391
|
+
return (at / total) * 2 - 1;
|
|
392
|
+
return (best.at / total) * 2 - 1;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* A branch label ("yes", "no") belongs to its decision, so it sits where the
|
|
396
|
+
* branch leaves the diamond, not halfway to the next step: on the run out of the
|
|
397
|
+
* diamond's point when that has room, otherwise at the start of the first run
|
|
398
|
+
* along the flow, or of a long arm that forks off a short stem. `stem` is how much of the first run a sibling branch shares;
|
|
399
|
+
* the label starts past it, so it doesn't cover the place where they part.
|
|
400
|
+
*/
|
|
401
|
+
export function branchSpot(pts, label, dir, stem = 0) {
|
|
402
|
+
const flowVertical = dir === 'TB' || dir === 'BT';
|
|
403
|
+
const lens = pts.slice(1).map((b, i) => Math.hypot(b.x - pts[i].x, b.y - pts[i].y));
|
|
404
|
+
const total = lens.reduce((x, y) => x + y, 0);
|
|
405
|
+
let before = 0;
|
|
406
|
+
for (let i = 0; i < lens.length; i++) {
|
|
407
|
+
const a = pts[i], b = pts[i + 1], len = lens[i];
|
|
408
|
+
const vertical = Math.abs(b.x - a.x) < Math.abs(b.y - a.y);
|
|
409
|
+
const size = vertical ? label.h : label.w;
|
|
410
|
+
const lead = i === 0 && stem > 0 ? stem + 6 : 10;
|
|
411
|
+
// A long arm across the flow, straight after the stem out of the point, is
|
|
412
|
+
// nearer the diamond than the drop at its far end.
|
|
413
|
+
const arm = i === 1 && len >= 96 && lens[0] <= 24;
|
|
414
|
+
if ((i === 0 || arm || vertical === flowVertical) && len >= lead + size + 10)
|
|
415
|
+
return ((before + lead + size / 2) / total) * 2 - 1;
|
|
416
|
+
before += len;
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
355
419
|
}
|
|
356
420
|
// draw.io positions edge labels by fraction along the path, -1..1.
|
|
357
421
|
// Project ELK's label centre onto the route and convert.
|
package/dist/layout/layout.js
CHANGED
|
@@ -162,7 +162,10 @@ function nodeSizes(d, tm, fonts) {
|
|
|
162
162
|
if (t.lines > 2)
|
|
163
163
|
t = textOf(n, fonts.maxText);
|
|
164
164
|
const w = snap(Math.max(128, t.width * 1.5 + 40));
|
|
165
|
-
|
|
165
|
+
// The text takes about 0.6 of the width, which leaves it 0.4 of the height.
|
|
166
|
+
// Only that tall, and no taller: a diamond at half its width for a two-line
|
|
167
|
+
// question is the biggest thing in the flow, bigger than any step.
|
|
168
|
+
sizes.set(n.id, { w, h: snap(Math.max(72, w * 0.36, t.lines * fonts.line * 2.5 + 24)) });
|
|
166
169
|
continue;
|
|
167
170
|
}
|
|
168
171
|
const extra = shapeExtra(shape, !!n.icon);
|
package/dist/layout/polish.js
CHANGED
|
@@ -137,47 +137,108 @@ export function polish(d, l, inp) {
|
|
|
137
137
|
const b = nodes.get(n.id);
|
|
138
138
|
const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
|
|
139
139
|
const outs = edges.filter((e) => e.from === n.id && e.to !== n.id);
|
|
140
|
-
// The run each edge settles into once it is clear of the diamond.
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
// The run each edge settles into once it is clear of the diamond. ELK usually
|
|
141
|
+
// leaves on a short stub and turns; when it leaves straight down instead, the
|
|
142
|
+
// first segment is the run.
|
|
143
|
+
const runAt = (p) => {
|
|
144
|
+
if (p.length >= 2 && vert(p[0], p[1]) && p[1].y > b.y + b.h + 24)
|
|
145
|
+
return 0;
|
|
143
146
|
for (let i = 1; i < p.length - 1; i++)
|
|
144
147
|
if (vert(p[i], p[i + 1]) && p[i + 1].y > b.y + b.h + 1)
|
|
145
|
-
return
|
|
146
|
-
return
|
|
148
|
+
return i;
|
|
149
|
+
return -1;
|
|
150
|
+
};
|
|
151
|
+
const runX = (e) => {
|
|
152
|
+
const i = runAt(e.pts);
|
|
153
|
+
return (i >= 0 ? e.pts[i] : e.pts[e.pts.length - 1]).x;
|
|
147
154
|
};
|
|
155
|
+
// Points from the start of that run on.
|
|
148
156
|
const tail = (e) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (let i = 1; i < p.length - 1; i++)
|
|
152
|
-
if (vert(p[i], p[i + 1]) && p[i + 1].y > b.y + b.h + 1)
|
|
153
|
-
return p.slice(i);
|
|
154
|
-
return p.slice(-1);
|
|
157
|
+
const i = runAt(e.pts);
|
|
158
|
+
return i >= 0 ? e.pts.slice(i) : e.pts.slice(-1);
|
|
155
159
|
};
|
|
156
160
|
const sorted = [...outs].sort((a, c) => runX(a) - runX(c));
|
|
157
161
|
let bottomTaken = false;
|
|
162
|
+
// One branch per side point: a second one there would hide the first.
|
|
163
|
+
const taken = { left: false, right: false };
|
|
164
|
+
// A branch whose run sits under the diamond would have to fork off the bottom
|
|
165
|
+
// point next to its sibling. Move the run out past the side point instead, so
|
|
166
|
+
// each branch leaves from its own corner: side point, a short stub, then down.
|
|
167
|
+
const sideStep = (e, rest, left) => {
|
|
168
|
+
if (rest.length < 2)
|
|
169
|
+
return undefined;
|
|
170
|
+
const lab = l.edges.get(e.id)?.label;
|
|
171
|
+
// Far enough out that the branch label, centred on the run, clears the diamond.
|
|
172
|
+
const out = Math.max(24, lab ? Math.ceil(f.boxTo(lab).w / 2) + 8 : 0);
|
|
173
|
+
const beside = left ? b.x - out : b.x + b.w + out;
|
|
174
|
+
const end = rest[1], t = nodes.get(e.to);
|
|
175
|
+
// A run that drops straight into the target has to stay over the target.
|
|
176
|
+
const drops = rest.length === 2;
|
|
177
|
+
if (drops ? ['diamond', 'table'].includes(shapeOf(e.to) ?? '') || !near(end.y, t.y, 1) : !horiz(end, rest[2]) || end.y < b.y + b.h + 12)
|
|
178
|
+
return undefined;
|
|
179
|
+
// Into the target's middle when that is already clear of the diamond, else just beside the diamond.
|
|
180
|
+
const mid = t.x + t.w / 2;
|
|
181
|
+
const tries = drops && (left ? mid <= beside : mid >= beside) ? [mid, beside] : [beside];
|
|
182
|
+
const sideP = { x: left ? b.x : b.x + b.w, y: cy };
|
|
183
|
+
for (const nx of tries) {
|
|
184
|
+
if (drops && (nx < t.x + R || nx > t.x + t.w - R || usedAt(e.to, nx, e, 'top')))
|
|
185
|
+
continue;
|
|
186
|
+
const a = { x: nx, y: cy }, c = { x: nx, y: end.y };
|
|
187
|
+
if (!clear(sideP, a, [n.id]) || !clear(a, c, [n.id, e.to]) || !free(a, c, e))
|
|
188
|
+
continue;
|
|
189
|
+
if (!drops && !clear(c, rest[2], [n.id, e.to]))
|
|
190
|
+
continue;
|
|
191
|
+
return simplify([sideP, a, c, ...rest.slice(2)]);
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
};
|
|
158
195
|
// The edge closest to straight down gets the bottom point.
|
|
159
|
-
const
|
|
160
|
-
|
|
196
|
+
const nearest = (list) => list.reduce((m, e) => (!m || Math.abs(runX(e) - cx) < Math.abs(runX(m) - cx) ? e : m), undefined);
|
|
197
|
+
// Or, failing that, the one whose target reaches under the bottom point, so it can drop straight.
|
|
198
|
+
const reaches = (e) => {
|
|
199
|
+
const t = nodes.get(e.to), rest = tail(e);
|
|
200
|
+
return rest.length === 2 && near(rest[1].y, t.y, 1) && !['diamond', 'table'].includes(shapeOf(e.to) ?? '') && cx >= t.x + 16 && cx <= t.x + t.w - 16;
|
|
201
|
+
};
|
|
202
|
+
const mid = nearest(sorted);
|
|
203
|
+
const straight = mid && Math.abs(runX(mid) - cx) < 12 ? mid : nearest(sorted.filter(reaches));
|
|
204
|
+
// The middle branch goes last: whether it can drop straight depends on where
|
|
205
|
+
// its siblings ended up, not on the stubs ELK gave them at the bottom point.
|
|
206
|
+
for (const e of [...sorted.filter((x) => x !== straight), ...(straight ? [straight] : [])]) {
|
|
161
207
|
const x = runX(e), rest = tail(e);
|
|
162
208
|
const bottom = { x: cx, y: b.y + b.h };
|
|
209
|
+
// A run that starts on the diamond itself: what follows is from its far end on.
|
|
210
|
+
const onEdge = rest[0].y <= b.y + b.h + 1 && rest.length > 1;
|
|
163
211
|
if (e === straight && Math.abs(x - cx) < 12) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
const run = onEdge ? rest.slice(1) : rest;
|
|
213
|
+
e.pts = simplify([bottom, { x: cx, y: run[0].y }, ...run.slice(1)]);
|
|
214
|
+
if (!near(run[0].x, cx))
|
|
215
|
+
e.pts = simplify([bottom, { x: cx, y: run[0].y }, ...run]);
|
|
216
|
+
bottomTaken = true;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const t = nodes.get(e.to), floor = { x: cx, y: t.y };
|
|
220
|
+
if (e === straight && reaches(e) && !usedAt(e.to, cx, e, 'top') && clear(bottom, floor, [n.id, e.to]) && free(bottom, floor, e)) {
|
|
221
|
+
e.pts = [bottom, floor];
|
|
167
222
|
bottomTaken = true;
|
|
168
223
|
continue;
|
|
169
224
|
}
|
|
170
225
|
const left = x < cx;
|
|
171
226
|
const sideP = { x: left ? b.x : b.x + b.w, y: cy };
|
|
172
227
|
const corner = { x, y: cy };
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
const key = left ? 'left' : 'right';
|
|
229
|
+
if (!taken[key]) {
|
|
230
|
+
const sideOk = (left ? x < b.x - 10 : x > b.x + b.w + 10) && clear(sideP, corner, [n.id]) && clear(corner, rest[0], [n.id, e.to]);
|
|
231
|
+
const pts = sideOk ? simplify([sideP, corner, ...rest]) : sideStep(e, rest, left);
|
|
232
|
+
if (pts) {
|
|
233
|
+
e.pts = pts;
|
|
234
|
+
taken[key] = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
177
237
|
}
|
|
178
238
|
// Branches that both head down fork from the bottom point on a short stem.
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
// Such a run turns onto the stem below the point.
|
|
240
|
+
const y = onEdge ? b.y + b.h + 16 : Math.min(rest[0].y, b.y + b.h + 16);
|
|
241
|
+
e.pts = simplify([bottom, { x: cx, y }, { x, y }, ...(onEdge ? rest.slice(1) : rest)]);
|
|
181
242
|
bottomTaken = true;
|
|
182
243
|
}
|
|
183
244
|
for (const e of edges.filter((x) => x.to === n.id && x.from !== n.id)) {
|
|
@@ -188,9 +249,38 @@ export function polish(d, l, inp) {
|
|
|
188
249
|
e.pts[m - 1] = { x: cx, y: b.y };
|
|
189
250
|
continue;
|
|
190
251
|
}
|
|
252
|
+
// Arriving sideways and then dropping to a port beside the top point: carry
|
|
253
|
+
// the sideways run on to the middle. Stepping down first leaves a small kink.
|
|
254
|
+
const before = p[m - 3];
|
|
255
|
+
const across = { x: cx, y: prev.y };
|
|
256
|
+
if (before && horiz(before, prev) && !near(before.x, cx) && clear(prev, across, [e.from, n.id]) && clear(across, { x: cx, y: b.y }, [e.from, n.id])) {
|
|
257
|
+
e.pts = simplify([...p.slice(0, m - 2), across, { x: cx, y: b.y }]);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
191
260
|
const y = Math.max(prev.y, b.y - 16);
|
|
192
261
|
e.pts = simplify([...p.slice(0, m - 1), { x: last.x, y }, { x: cx, y }, { x: cx, y: b.y }]);
|
|
193
262
|
}
|
|
263
|
+
// Branches that join above a decision meet at one height, not a few px apart.
|
|
264
|
+
const joins = edges.flatMap((e) => {
|
|
265
|
+
const p = e.pts, m = p.length;
|
|
266
|
+
if (e.to !== n.id || e.from === n.id || m < 4)
|
|
267
|
+
return [];
|
|
268
|
+
const [up, a, c, top] = [p[m - 4], p[m - 3], p[m - 2], p[m - 1]];
|
|
269
|
+
const ok = vert(up, a) && up.y < a.y && horiz(a, c) && near(c.x, cx) && vert(c, top) && near(top.y, b.y, 1);
|
|
270
|
+
return ok ? [{ e, a, c }] : [];
|
|
271
|
+
});
|
|
272
|
+
if (joins.length > 1) {
|
|
273
|
+
const y = Math.max(...joins.map((j) => j.a.y));
|
|
274
|
+
for (const j of joins) {
|
|
275
|
+
if (near(j.a.y, y) || y - j.a.y > 24)
|
|
276
|
+
continue;
|
|
277
|
+
const na = { x: j.a.x, y }, nc = { x: cx, y };
|
|
278
|
+
if (!clear(na, nc, [j.e.from, n.id]))
|
|
279
|
+
continue;
|
|
280
|
+
const m = j.e.pts.length;
|
|
281
|
+
j.e.pts = simplify([...j.e.pts.slice(0, m - 3), na, nc, j.e.pts[m - 1]]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
194
284
|
}
|
|
195
285
|
dejog(true);
|
|
196
286
|
// Self-loops: wide enough to read as a loop, label outside it.
|
package/dist/mcp/server.js
CHANGED
|
@@ -137,8 +137,8 @@ export function createServer(opts) {
|
|
|
137
137
|
return { content: [text([res.fixed ? `rerouted ${res.fixed} edges in ${file}` : 'nothing to fix', ...checkLines(res.check)].join('\n'))] };
|
|
138
138
|
})());
|
|
139
139
|
server.registerTool('import_mermaid', {
|
|
140
|
-
title: 'Convert Mermaid to
|
|
141
|
-
description: 'Converts a Mermaid diagram (flowchart, stateDiagram, sequenceDiagram, erDiagram) to
|
|
140
|
+
title: 'Convert Mermaid to a linework spec',
|
|
141
|
+
description: 'Converts a Mermaid diagram (flowchart, stateDiagram, sequenceDiagram, erDiagram) to a linework spec. Returns the spec; review the guessed node kinds, then build it. With path, builds it straight away.',
|
|
142
142
|
inputSchema: z.object({ source: z.string().describe('Mermaid source, without the ``` fence.'), path: pathArg.optional() }),
|
|
143
143
|
}, (args) => wrap(async () => {
|
|
144
144
|
const { importMermaid } = await import('../import/mermaid.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itsthecsguy/linework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Software diagrams an AI can write and a person can edit. YAML spec in, .drawio.svg out, editable in draw.io.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"diagram",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"assets": "node scripts/copy-drawio-assets.mjs",
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
43
|
"prepack": "npm run build",
|
|
44
|
+
"release": "node scripts/release.mjs",
|
|
44
45
|
"fonts": "node scripts/copy-fonts.mjs"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
@@ -307,6 +307,6 @@ The middle is `--` for an identifying relationship (solid line) or `..` for a no
|
|
|
307
307
|
|
|
308
308
|
## Importing Mermaid
|
|
309
309
|
|
|
310
|
-
`linework import file.mmd` prints
|
|
310
|
+
`linework import file.mmd` prints a linework spec. `-o out.drawio.svg` builds it straight away. A Markdown file works too; use `--block N` when it has several Mermaid blocks. Supported: `flowchart`/`graph`, `stateDiagram`, `sequenceDiagram`, `erDiagram`.
|
|
311
311
|
|
|
312
312
|
A flowchart with diamonds becomes a `flow`. One with subgraphs or cylinders becomes an `architecture` diagram. In that case a `<br>` second line becomes `tech`, and kinds are guessed from shapes and names: a cylinder becomes a database, "Kafka" becomes a queue, "Stripe" becomes external. Mermaid styling (`classDef`, `style`, `click`) is dropped. Review the printed spec before building, and fix any kinds it guessed wrong.
|
|
@@ -20,7 +20,7 @@ Linework handles the layout and the look. These rules are about content, which i
|
|
|
20
20
|
- **Label edges in architecture diagrams with the protocol or the verb**: `HTTPS`, `gRPC`, `SQL`, `publishes OrderPlaced`, `reads session`. An unlabeled arrow makes the reader guess.
|
|
21
21
|
- **Use `-.->` for async**: events, queues, webhooks, fire-and-forget. Readers rely on the solid/dashed distinction to see where latency and failure modes change.
|
|
22
22
|
- **Point the arrow the way the request goes**, not the way data comes back. Only show a return edge when the reply is interesting.
|
|
23
|
-
- **Label every edge out of a decision** in a flow (`yes`/`no`, or the condition).
|
|
23
|
+
- **Label every edge out of a decision** in a flow (`yes`/`no`, or the condition). Keep those labels to a word or two: each one sits right beside its diamond, where the branch leaves it.
|
|
24
24
|
- In state diagrams, label transitions with the event that triggers them (`payment_succeeded`), not the resulting state.
|
|
25
25
|
|
|
26
26
|
## Groups
|