reladraw 0.5.0 → 0.7.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 +3 -3
- package/SYNTAX.md +140 -4
- package/dist/ast.d.ts +25 -0
- package/dist/ast.js +18 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +15 -0
- package/dist/model.d.ts +28 -1
- package/dist/parser.js +50 -6
- package/dist/render.js +1292 -17
- package/dist/resolve.js +110 -8
- package/package.json +1 -1
package/dist/resolve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ALL_ATTR_KEYS, ATTR_KEYS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, describePlacement, nameTarget, } from './ast.js';
|
|
2
|
-
import {
|
|
1
|
+
import { ALL_ATTR_KEYS, ATTR_KEYS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, LINE_VALUES, describePlacement, nameTarget, } from './ast.js';
|
|
2
|
+
import { CHILD_GAP, DECK_STEP, DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, HEADER_GAP, ICON_GAP, ICON_LINES, TEXT_CLEARANCE, PAD, SEPARATION_GAP, THICKNESS, arrowLength, fontSizeFor, leafTop, textExtent, textStyleFor, widestLine, } from './constants.js';
|
|
3
3
|
import { fix, reachability, tightest } from './constrain.js';
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
5
5
|
import { bodyFor } from './icons.js';
|
|
@@ -457,6 +457,25 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
457
457
|
}),
|
|
458
458
|
...(stmt.between.axis !== undefined ? { axis: stmt.between.axis } : {}),
|
|
459
459
|
};
|
|
460
|
+
const passes = stmt.passes?.map((placement) => {
|
|
461
|
+
const written = describePlacement(placement);
|
|
462
|
+
return {
|
|
463
|
+
direction: placement.direction,
|
|
464
|
+
written,
|
|
465
|
+
nodes: placement.targets.map(({ name, part }) => {
|
|
466
|
+
if (part !== undefined) {
|
|
467
|
+
// The line passes a box, and a side or a point has no side of its
|
|
468
|
+
// own to be passed on. Refused by name rather than dropped.
|
|
469
|
+
throw new SourceError(`edge passes "${written}", and a line passes a whole box — drop "${part}"`, stmt.line);
|
|
470
|
+
}
|
|
471
|
+
const node = byName.get(name);
|
|
472
|
+
if (!node) {
|
|
473
|
+
throw new SourceError(`edge passes "${written}", and "${name}" does not exist`, stmt.line);
|
|
474
|
+
}
|
|
475
|
+
return node;
|
|
476
|
+
}),
|
|
477
|
+
};
|
|
478
|
+
});
|
|
460
479
|
const appearance = { ...defaults.get('edge'), ...appearanceOf(stmt.attrs, styles, stmt.line) };
|
|
461
480
|
const what = `${stmt.from} -> ${stmt.to}`;
|
|
462
481
|
checkAttrs('edge', what, stmt.attrs, stmt.line);
|
|
@@ -479,6 +498,8 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
479
498
|
? { text: stmt.text, lines: linesFor(stmt.text, textAttrs, `edge ${what}`, stmt.line) }
|
|
480
499
|
: {}),
|
|
481
500
|
...(between ? { between } : {}),
|
|
501
|
+
...(passes ? { passes } : {}),
|
|
502
|
+
look: lineLook(stmt.attrs, appearance, what, stmt.line),
|
|
482
503
|
attrs: stmt.attrs,
|
|
483
504
|
appearance,
|
|
484
505
|
line: stmt.line,
|
|
@@ -486,6 +507,49 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
486
507
|
}
|
|
487
508
|
return edges;
|
|
488
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The line's bracket, checked, with a default for everything unsaid. A value is
|
|
512
|
+
* checked wherever it came from — a style's `path: squar` is as much a typo as
|
|
513
|
+
* an edge's — but `corners: rounded` is refused only when the edge itself says
|
|
514
|
+
* it on a curved line, which has no corners. From a style or a default it
|
|
515
|
+
* applies to the edges that have corners, the way a style's `fill:` lands on
|
|
516
|
+
* the nodes that have an inside.
|
|
517
|
+
*/
|
|
518
|
+
function lineLook(written, appearance, what, line) {
|
|
519
|
+
const word = (property) => {
|
|
520
|
+
const value = appearance[`line.${property}`];
|
|
521
|
+
if (value === undefined)
|
|
522
|
+
return LINE_VALUES[property][0];
|
|
523
|
+
if (!LINE_VALUES[property].includes(value)) {
|
|
524
|
+
throw new SourceError(`edge ${what}: "${property}: ${value}" is not a ${property} — use ${(property === 'thickness' ? ['thin', 'normal', 'thick'] : LINE_VALUES[property]).join(', ')}` +
|
|
525
|
+
(property === 'thickness' ? ', or a number of pixels' : ''), line);
|
|
526
|
+
}
|
|
527
|
+
return value;
|
|
528
|
+
};
|
|
529
|
+
const thickness = (() => {
|
|
530
|
+
const value = appearance['line.thickness'];
|
|
531
|
+
if (value !== undefined && /^[0-9.]/.test(value)) {
|
|
532
|
+
if (!/^\d+(\.\d+)?$/.test(value) || Number(value) <= 0) {
|
|
533
|
+
throw new SourceError(`edge ${what}: "thickness: ${value}" — a thickness is thin, normal, thick, ` +
|
|
534
|
+
'or a plain number of pixels greater than zero, such as `thickness: 3`', line);
|
|
535
|
+
}
|
|
536
|
+
return Number(value);
|
|
537
|
+
}
|
|
538
|
+
return THICKNESS[word('thickness')];
|
|
539
|
+
})();
|
|
540
|
+
const look = {
|
|
541
|
+
path: word('path'),
|
|
542
|
+
corners: word('corners'),
|
|
543
|
+
crossing: word('crossing'),
|
|
544
|
+
pattern: word('pattern'),
|
|
545
|
+
thickness,
|
|
546
|
+
};
|
|
547
|
+
if (look.path === 'curved' && written['line.corners'] === 'rounded') {
|
|
548
|
+
throw new SourceError(`edge ${what}: a curved line has no corners to round — \`corners: rounded\` takes a ` +
|
|
549
|
+
'`path: square` or `path: straight` line', line);
|
|
550
|
+
}
|
|
551
|
+
return look;
|
|
552
|
+
}
|
|
489
553
|
/**
|
|
490
554
|
* Give a node a width and height, sizing its children first. Also records each
|
|
491
555
|
* child's offset within this node, which pass three turns into absolute
|
|
@@ -1268,6 +1332,30 @@ const AXIS_WORD = { x: 'horizontally', y: 'vertically' };
|
|
|
1268
1332
|
function memberOn(target, axis) {
|
|
1269
1333
|
return target.byAxis ? target.byAxis[axis] : target.index;
|
|
1270
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* The ways named sides can face away from the other end. Opposite sides face
|
|
1337
|
+
* away together or not at all (`left right` with `from` first); sides at right
|
|
1338
|
+
* angles each may, and either is enough (`left top` with `from` first, where
|
|
1339
|
+
* the edge turns back over the top, or with `to` below, where it goes round).
|
|
1340
|
+
*/
|
|
1341
|
+
function facingAway(fromSide, toSide) {
|
|
1342
|
+
// The axis a side is on, and whether it faces away when its own end is first.
|
|
1343
|
+
const SIDES = {
|
|
1344
|
+
left: { axis: 'x', first: true },
|
|
1345
|
+
right: { axis: 'x', first: false },
|
|
1346
|
+
top: { axis: 'y', first: true },
|
|
1347
|
+
bottom: { axis: 'y', first: false },
|
|
1348
|
+
};
|
|
1349
|
+
const f = SIDES[String(fromSide)];
|
|
1350
|
+
const t = SIDES[String(toSide)];
|
|
1351
|
+
if (!f || !t)
|
|
1352
|
+
return [];
|
|
1353
|
+
const byFrom = { axis: f.axis, fromFirst: f.first };
|
|
1354
|
+
const byTo = { axis: t.axis, fromFirst: !t.first };
|
|
1355
|
+
if (f.axis !== t.axis)
|
|
1356
|
+
return [byFrom, byTo];
|
|
1357
|
+
return byFrom.fromFirst === byTo.fromFirst ? [byFrom] : [];
|
|
1358
|
+
}
|
|
1271
1359
|
/**
|
|
1272
1360
|
* Where a node sits within the group being solved: which member holds it, and
|
|
1273
1361
|
* where inside that member. An edge may name anything at any depth, so its ends
|
|
@@ -1303,7 +1391,9 @@ function corridorsIn(edges, locate, measurer, fontSize) {
|
|
|
1303
1391
|
// edge told to pass between two named things carries its text in *that*
|
|
1304
1392
|
// corridor rather than in the gap between its own ends, so widening this one
|
|
1305
1393
|
// would make room where the text never goes.
|
|
1306
|
-
|
|
1394
|
+
// An edge that says which side of something it passes is routed round it,
|
|
1395
|
+
// and its text rides on that route for the same reason.
|
|
1396
|
+
if (edge.text === undefined || edge.between || edge.passes)
|
|
1307
1397
|
continue;
|
|
1308
1398
|
const from = locate(edge.from);
|
|
1309
1399
|
const to = locate(edge.to);
|
|
@@ -1312,11 +1402,18 @@ function corridorsIn(edges, locate, measurer, fontSize) {
|
|
|
1312
1402
|
// The clearance is doubled because the text is drawn at the *midpoint* of
|
|
1313
1403
|
// the line, so the room it needs is symmetric about that point whatever sits
|
|
1314
1404
|
// at either end. The arrowhead is charged on both sides for the same reason:
|
|
1315
|
-
// it covers
|
|
1316
|
-
// one end only would move the midpoint rather than lengthen the run.
|
|
1405
|
+
// it covers the head's length of the line it arrives on, and reserving that
|
|
1406
|
+
// at one end only would move the midpoint rather than lengthen the run. A
|
|
1407
|
+
// thick line's head is longer, and takes the room it needs.
|
|
1317
1408
|
const extent = (axis) => textExtent(edge.lines, edge.textAttrs, axis, measurer, fontSize, edge.line) +
|
|
1318
|
-
(TEXT_CLEARANCE +
|
|
1319
|
-
corridors.push({
|
|
1409
|
+
(TEXT_CLEARANCE + arrowLength(edge.look.thickness)) * 2;
|
|
1410
|
+
corridors.push({
|
|
1411
|
+
edge,
|
|
1412
|
+
from,
|
|
1413
|
+
to,
|
|
1414
|
+
need: { x: extent('x'), y: extent('y') },
|
|
1415
|
+
away: facingAway(edge.attrs['from'], edge.attrs['to']),
|
|
1416
|
+
});
|
|
1320
1417
|
}
|
|
1321
1418
|
return corridors;
|
|
1322
1419
|
}
|
|
@@ -1658,7 +1755,7 @@ function alignedAt(side, span, own) {
|
|
|
1658
1755
|
*/
|
|
1659
1756
|
function room(corridors, constraints, solved, solveAll, made) {
|
|
1660
1757
|
let added = false;
|
|
1661
|
-
for (const [index, { from, to, need }] of corridors.entries()) {
|
|
1758
|
+
for (const [index, { from, to, need, away }] of corridors.entries()) {
|
|
1662
1759
|
const clear = (axis) => {
|
|
1663
1760
|
const at = (end) => solved[axis][end.index] + end.offset[axis];
|
|
1664
1761
|
const size = (end) => (axis === 'x' ? end.width : end.height);
|
|
@@ -1672,6 +1769,11 @@ function room(corridors, constraints, solved, solveAll, made) {
|
|
|
1672
1769
|
if (open.length !== 1)
|
|
1673
1770
|
continue;
|
|
1674
1771
|
const axis = open[0];
|
|
1772
|
+
// Ends facing away along the only open axis mean the boxes share a row, and
|
|
1773
|
+
// the renderer takes the line over the top with its text — see `planLoops`.
|
|
1774
|
+
// Widening the gap would make room where the text never goes.
|
|
1775
|
+
if (away.some((way) => way.axis === axis && (clear(axis).before === from) === way.fromFirst))
|
|
1776
|
+
continue;
|
|
1675
1777
|
// Given already, and a minimum stays met: asking again would widen
|
|
1676
1778
|
// nothing and only keep the caller looking.
|
|
1677
1779
|
if (made.has(`${index}:${axis}`))
|