reladraw 0.17.0 → 0.18.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 +1 -1
- package/SYNTAX.md +58 -11
- package/dist/checks.js +30 -9
- package/dist/cli.js +8 -0
- package/dist/icons.d.ts +13 -13
- package/dist/icons.js +61 -15
- package/dist/lines.d.ts +8 -0
- package/dist/lines.js +50 -4
- package/dist/outline.d.ts +123 -0
- package/dist/outline.js +564 -0
- package/dist/parser.js +51 -1
- package/dist/place.d.ts +5 -0
- package/dist/place.js +44 -14
- package/dist/render.js +43 -18
- package/dist/resolve.js +43 -3
- package/package.json +1 -1
package/dist/parser.js
CHANGED
|
@@ -3,7 +3,10 @@ import { CHECK_DEFAULTS, CHECK_KINDS, setCheck } from './checks.js';
|
|
|
3
3
|
import { SourceError } from './errors.js';
|
|
4
4
|
import { STATEMENT_KEYWORDS } from './grammar.js';
|
|
5
5
|
import { isAttrKey, nameMark, OpenString, tokenizeLine } from './lexer.js';
|
|
6
|
+
import { SHAPE_SETTINGS } from './outline.js';
|
|
6
7
|
import { checkOpacity } from './place.js';
|
|
8
|
+
/** The settings a shape's bracket takes as a word on its own: `cylinder (lying)`. */
|
|
9
|
+
const SHAPE_FLAGS = [...new Set(Object.values(SHAPE_SETTINGS).flatMap((one) => Object.entries(one).filter(([, kind]) => kind === 'flag').map(([name]) => name)))];
|
|
7
10
|
import { readSize, SIZE_KEYS, writeSize } from './size.js';
|
|
8
11
|
import { THEME_COLORS, THEME_NAMES, THEMES } from './themes.js';
|
|
9
12
|
/**
|
|
@@ -601,6 +604,53 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
601
604
|
}
|
|
602
605
|
return next;
|
|
603
606
|
}
|
|
607
|
+
if (key === 'shape' && follows(tokens, at + 2, '(')) {
|
|
608
|
+
// `shape: cylinder (cap: 1.5, lying)` — a value, then its settings. Which
|
|
609
|
+
// settings the shape takes is checked once its shape is known, since a
|
|
610
|
+
// style may carry the shape and a node the bracket's other half.
|
|
611
|
+
const value = tokens[at + 1];
|
|
612
|
+
if (value.quoted || isAttrKey(value)) {
|
|
613
|
+
throw new SourceError(`${subject}: shape: names an outline, such as diamond, before its settings`, line);
|
|
614
|
+
}
|
|
615
|
+
setOnce(attrs, 'shape', value.text, subject, line);
|
|
616
|
+
// A flag stands alone, `lying`; read it as `lying: true`.
|
|
617
|
+
const inner = [];
|
|
618
|
+
let depth = 0;
|
|
619
|
+
let end = at + 2;
|
|
620
|
+
for (; end < tokens.length; end++) {
|
|
621
|
+
const token = tokens[end];
|
|
622
|
+
if (!token.quoted && token.text === '(')
|
|
623
|
+
depth += 1;
|
|
624
|
+
if (!token.quoted && token.text === ')')
|
|
625
|
+
depth -= 1;
|
|
626
|
+
const bare = token.text.replace(/,$/, '');
|
|
627
|
+
if (depth === 1 && !token.quoted && SHAPE_FLAGS.includes(bare) && !isAttrKey(tokens[end - 1])) {
|
|
628
|
+
inner.push({ ...token, text: `${bare}:` }, { ...token, text: token.text.endsWith(',') ? 'true,' : 'true' });
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
inner.push(token);
|
|
632
|
+
}
|
|
633
|
+
if (depth === 0)
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
const takes = Object.keys(SHAPE_SETTINGS[value.text] ?? {});
|
|
637
|
+
if (takes.length === 0) {
|
|
638
|
+
throw new SourceError(`${subject}: \`shape: ${value.text}\` takes no settings, so it has no bracket`, line);
|
|
639
|
+
}
|
|
640
|
+
const read = readBracket(inner, 0, takes, {
|
|
641
|
+
subject,
|
|
642
|
+
what: `\`shape: ${value.text}\``,
|
|
643
|
+
kind: `a ${value.text}`,
|
|
644
|
+
example: `${takes[0]}: …`,
|
|
645
|
+
line,
|
|
646
|
+
});
|
|
647
|
+
if (Object.keys(read.values).length === 0) {
|
|
648
|
+
throw new SourceError(`${subject}: \`shape: ${value.text}\` opens empty brackets`, line);
|
|
649
|
+
}
|
|
650
|
+
for (const [inside, set] of Object.entries(read.values))
|
|
651
|
+
setOnce(attrs, `shape.${inside}`, set, subject, line);
|
|
652
|
+
return end + 1;
|
|
653
|
+
}
|
|
604
654
|
const bracketKeys = BRACKET_KEYS[key];
|
|
605
655
|
if (bracketKeys !== undefined && follows(tokens, at + 1, '(')) {
|
|
606
656
|
// `text: (color: theme-muted)` — the whole bracket belongs to one part, and it is
|
|
@@ -1177,7 +1227,7 @@ function parseNode(head, line) {
|
|
|
1177
1227
|
* A node's text takes the look keys and `reads:` (design.org 7.2). `at` is
|
|
1178
1228
|
* read only to be refused with the side form, so it isn't listed as one it takes.
|
|
1179
1229
|
*/
|
|
1180
|
-
const NODE_TEXT_KEYS = [...TEXT_KEYS, 'reads'];
|
|
1230
|
+
const NODE_TEXT_KEYS = [...TEXT_KEYS, 'reads', 'fill'];
|
|
1181
1231
|
const NODE_TEXT_LISTED = NODE_TEXT_KEYS.filter((key) => key !== 'at');
|
|
1182
1232
|
/** A node's text turns a quarter turn; `along` follows a line, and a box has none. */
|
|
1183
1233
|
function refuseNodeReads(reads, subject, line, whose = "a node's text") {
|
package/dist/place.d.ts
CHANGED
|
@@ -253,6 +253,11 @@ export declare class Placer {
|
|
|
253
253
|
* the part it names, or the whole box.
|
|
254
254
|
*/
|
|
255
255
|
private ownPart;
|
|
256
|
+
/**
|
|
257
|
+
* For a node that is not a rectangle, where the line from its middle toward
|
|
258
|
+
* a point of its box meets its outline, from the box's top-left.
|
|
259
|
+
*/
|
|
260
|
+
private outlineOf;
|
|
256
261
|
private overStated;
|
|
257
262
|
/** Every statement a node's placements make about one axis. */
|
|
258
263
|
private statements;
|
package/dist/place.js
CHANGED
|
@@ -2,6 +2,7 @@ import { describePlacement, describePoint, fractionAxis, nameTarget, PART_SIDES,
|
|
|
2
2
|
import { DECK_STEP, GAPS, HEADER_GAP, UNIT } from './constants.js';
|
|
3
3
|
import { SourceError } from './errors.js';
|
|
4
4
|
import { isWithin, joinedToText } from './model.js';
|
|
5
|
+
import { isBoxy, middleOf, shapeOf, towardOutline } from './outline.js';
|
|
5
6
|
import { SIZE_KEYS, storedSize } from './size.js';
|
|
6
7
|
/**
|
|
7
8
|
* How a region grows to hold its text, on one axis (design.org 7.4). Inside,
|
|
@@ -546,7 +547,29 @@ export class Placer {
|
|
|
546
547
|
}
|
|
547
548
|
return this.textOf(node, axis, 0);
|
|
548
549
|
}
|
|
549
|
-
return narrow(whole, placement.own, axis, this.
|
|
550
|
+
return narrow(whole, placement.own, axis, this.outlineOf(node));
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* For a node that is not a rectangle, where the line from its middle toward
|
|
554
|
+
* a point of its box meets its outline, from the box's top-left.
|
|
555
|
+
*/
|
|
556
|
+
outlineOf(node) {
|
|
557
|
+
const body = node.body;
|
|
558
|
+
if (body.kind !== 'shape' || isBoxy(body.outline))
|
|
559
|
+
return undefined;
|
|
560
|
+
return (across, down) => {
|
|
561
|
+
const width = this.size(node, 'x');
|
|
562
|
+
const height = this.size(node, 'y');
|
|
563
|
+
if (body.outline === 'circle') {
|
|
564
|
+
// An ellipse, round when the two are equal; exact rather than sampled.
|
|
565
|
+
const dx = (across - 0.5) * width;
|
|
566
|
+
const dy = (down - 0.5) * height;
|
|
567
|
+
const scale = dx === 0 && dy === 0 ? 0 : 1 / Math.hypot((dx * 2) / width, (dy * 2) / height);
|
|
568
|
+
return { x: width / 2 + dx * scale, y: height / 2 + dy * scale };
|
|
569
|
+
}
|
|
570
|
+
const middle = middleOf(body.outline, body.look, width, height);
|
|
571
|
+
return towardOutline(shapeOf(body.outline, body.look, width, height).polygon, middle, { x: across * width, y: down * height });
|
|
572
|
+
};
|
|
550
573
|
}
|
|
551
574
|
overStated(node, axis, statements) {
|
|
552
575
|
const said = [...new Set(statements.map((statement) => statement.placement))]
|
|
@@ -730,7 +753,7 @@ export class Placer {
|
|
|
730
753
|
if (target.part === 'text') {
|
|
731
754
|
throw new SourceError(`"${node.name}" is placed against "${nameTarget(target)}", and a ${defined.kind} has no text`, placement.line);
|
|
732
755
|
}
|
|
733
|
-
return this.alongFraction(whole, target, axis, () => narrow(whole, target.part, axis
|
|
756
|
+
return this.alongFraction(whole, target, axis, () => narrow(whole, target.part, axis));
|
|
734
757
|
}
|
|
735
758
|
if (target.fraction) {
|
|
736
759
|
const whole = this.extentOf(node, { name: target.name }, axis, placement);
|
|
@@ -1004,7 +1027,7 @@ export class Placer {
|
|
|
1004
1027
|
}
|
|
1005
1028
|
return text;
|
|
1006
1029
|
}
|
|
1007
|
-
return narrow(whole, target.part, axis, this.
|
|
1030
|
+
return narrow(whole, target.part, axis, this.outlineOf(named));
|
|
1008
1031
|
}
|
|
1009
1032
|
/** A node's text on one axis, given where the node starts. */
|
|
1010
1033
|
textOf(node, axis, start) {
|
|
@@ -1546,7 +1569,7 @@ export class Placer {
|
|
|
1546
1569
|
const whole = { start: this.start(child, axis), size: this.size(child, axis) };
|
|
1547
1570
|
const at = target.part === 'text'
|
|
1548
1571
|
? this.textOf(child, axis, whole.start)
|
|
1549
|
-
: narrow(whole, target.part, axis, this.
|
|
1572
|
+
: narrow(whole, target.part, axis, this.outlineOf(child));
|
|
1550
1573
|
start = Math.min(start, at.start);
|
|
1551
1574
|
end = Math.max(end, at.start + at.size);
|
|
1552
1575
|
}
|
|
@@ -2089,6 +2112,11 @@ export class Placer {
|
|
|
2089
2112
|
width: box.width,
|
|
2090
2113
|
height: box.height,
|
|
2091
2114
|
};
|
|
2115
|
+
if (node.body.kind === 'shape' && !isBoxy(node.body.outline) && node.body.outline !== 'circle' && (node.textAttrs['place'] === undefined || title.how === 'center')) {
|
|
2116
|
+
// At the shape's own middle, which moves with its size: a triangle's stays a third of the way up.
|
|
2117
|
+
const middle = middleOf(node.body.outline, node.body.look, width, height);
|
|
2118
|
+
return { x: middle.x - box.width / 2, y: middle.y - box.height / 2, width: box.width, height: box.height };
|
|
2119
|
+
}
|
|
2092
2120
|
if (node.textAttrs['place'] === undefined || title.how === 'center' || facts.circle || node.body.kind === 'icon') {
|
|
2093
2121
|
return centered;
|
|
2094
2122
|
}
|
|
@@ -2204,9 +2232,11 @@ function commonFrame(a, b) {
|
|
|
2204
2232
|
/**
|
|
2205
2233
|
* Narrow an extent to the part a placement named. A side leaves the axis it
|
|
2206
2234
|
* doesn't name alone (`right` is the whole right edge); a point is a point on
|
|
2207
|
-
* both. On a
|
|
2235
|
+
* both. On a shape other than a rectangle, a point is where the line from the
|
|
2236
|
+
* shape's middle toward that point of its box meets the outline: on a circle
|
|
2237
|
+
* a corner is halfway between its sides, on a diamond the middle of an edge.
|
|
2208
2238
|
*/
|
|
2209
|
-
function narrow(whole, part, axis,
|
|
2239
|
+
function narrow(whole, part, axis, onOutline) {
|
|
2210
2240
|
if (part === undefined || part === 'text')
|
|
2211
2241
|
return whole;
|
|
2212
2242
|
const words = part.split('-');
|
|
@@ -2215,12 +2245,12 @@ function narrow(whole, part, axis, circle, diameter) {
|
|
|
2215
2245
|
const named = words.includes(near) ? 0 : words.includes(far) ? whole.size : undefined;
|
|
2216
2246
|
if (named === undefined && isSide)
|
|
2217
2247
|
return whole;
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2224
|
-
}
|
|
2225
|
-
return { start:
|
|
2248
|
+
if (onOutline) {
|
|
2249
|
+
// Where on the box the point is, as fractions across and down.
|
|
2250
|
+
const across = words.includes('left') ? 0 : words.includes('right') ? 1 : 0.5;
|
|
2251
|
+
const down = words.includes('top') ? 0 : words.includes('bottom') ? 1 : 0.5;
|
|
2252
|
+
const point = onOutline(across, down);
|
|
2253
|
+
return { start: whole.start + (axis === 'x' ? point.x : point.y), size: 0 };
|
|
2254
|
+
}
|
|
2255
|
+
return { start: whole.start + (named ?? whole.size / 2), size: 0 };
|
|
2226
2256
|
}
|
package/dist/render.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ARROW_LENGTH, ARROW_MARKER_WIDTH, DEFAULT_FONT_SIZE, ICON_LINES, LINE_WIDTH, PAD, arrowLength, fontSizeFor, textStyleFor, widestLine, } from './constants.js';
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
3
|
import { ICON_STROKE } from './icons.js';
|
|
4
|
+
import { insideOutline, isBoxy, isRound, placePath, shapeOf, SUBPROCESS_INSET } from './outline.js';
|
|
4
5
|
import { monospaceMeasurer } from './measure.js';
|
|
5
6
|
import { joinedToText } from './model.js';
|
|
6
7
|
import { DARK_THEME, THEMES, overlapFill, regionTint, textOnFill, themeColor } from './themes.js';
|
|
@@ -246,6 +247,8 @@ function patchSvg(patch, beneath, theme) {
|
|
|
246
247
|
return ` <rect ${rect} fill="${themeColor(patch.fill, theme)}"/>`;
|
|
247
248
|
const { x, y } = patch.center;
|
|
248
249
|
const under = beneath.flatMap((drawn) => drawn.fills ?? []).filter((fill) => {
|
|
250
|
+
if (fill.polygon)
|
|
251
|
+
return insideOutline(fill.polygon, { x, y });
|
|
249
252
|
if (!fill.round)
|
|
250
253
|
return x >= fill.x && x <= fill.x + fill.width && y >= fill.y && y <= fill.y + fill.height;
|
|
251
254
|
const r = Math.min(fill.width, fill.height) / 2;
|
|
@@ -362,7 +365,7 @@ function borderSvg(node, theme) {
|
|
|
362
365
|
return '';
|
|
363
366
|
const face = faceOf(node);
|
|
364
367
|
const border = borderOf(node.appearance, theme, theme.containerStroke);
|
|
365
|
-
return ` <path d="${outlinePath(node.body.outline, face.x, face.y, face.width, face.height, node.corners)}" fill="none" stroke="${border}"/>`;
|
|
368
|
+
return ` <path d="${outlinePath(node.body.outline, face.x, face.y, face.width, face.height, node.corners, node.body.look)}" fill="none" stroke="${border}"/>`;
|
|
366
369
|
}
|
|
367
370
|
/** What a node draws of its own, without its children, which are drawn as items of their own. */
|
|
368
371
|
function nodeSvg(node, theme, measurer, fontSize, markup) {
|
|
@@ -409,6 +412,7 @@ function nodeSvg(node, theme, measurer, fontSize, markup) {
|
|
|
409
412
|
return drawn;
|
|
410
413
|
}
|
|
411
414
|
const outline = node.body.outline;
|
|
415
|
+
const look = node.body.look;
|
|
412
416
|
const parts = [];
|
|
413
417
|
const face = faceOf(node);
|
|
414
418
|
// A node with children is colored as a backdrop however they are placed,
|
|
@@ -432,12 +436,12 @@ function nodeSvg(node, theme, measurer, fontSize, markup) {
|
|
|
432
436
|
const room = PAD / 2;
|
|
433
437
|
const hole = node.textBox;
|
|
434
438
|
const grown = { x: face.x - PAD, y: face.y - PAD, w: face.width + PAD * 2, h: face.height + PAD * 2 };
|
|
435
|
-
parts.push(` <mask id="${id}" maskUnits="userSpaceOnUse" x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}">`, ` <rect x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}" fill="white"/>`, ` <rect x="${round(node.x + hole.x - room)}" y="${round(node.y + hole.y)}" width="${round(hole.width + room * 2)}" height="${round(hole.height)}" fill="black"/>`, ' </mask>', ` <path d="${outlinePath(outline, face.x, face.y, face.width, face.height, node.corners)}" fill="${fill}" stroke="none"/>`, ` <path d="${outlinePath(outline, face.x, face.y, face.width, face.height, node.corners)}" fill="none" stroke="${border}" mask="url(#${id})"/>`);
|
|
439
|
+
parts.push(` <mask id="${id}" maskUnits="userSpaceOnUse" x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}">`, ` <rect x="${round(grown.x)}" y="${round(grown.y)}" width="${round(grown.w)}" height="${round(grown.h)}" fill="white"/>`, ` <rect x="${round(node.x + hole.x - room)}" y="${round(node.y + hole.y)}" width="${round(hole.width + room * 2)}" height="${round(hole.height)}" fill="black"/>`, ' </mask>', ` <path d="${outlinePath(outline, face.x, face.y, face.width, face.height, node.corners, look)}" fill="${fill}" stroke="none"/>`, ` <path d="${outlinePath(outline, face.x, face.y, face.width, face.height, node.corners, look)}" fill="none" stroke="${border}" mask="url(#${id})"/>`);
|
|
436
440
|
}
|
|
437
441
|
else {
|
|
438
|
-
parts.push(` <path d="${outlinePath(outline, face.x, face.y, face.width, face.height, node.corners)}" fill="${fill}" stroke="${border}"/>`);
|
|
442
|
+
parts.push(` <path d="${outlinePath(outline, face.x, face.y, face.width, face.height, node.corners, look)}" fill="${fill}" stroke="${border}"/>`);
|
|
439
443
|
}
|
|
440
|
-
for (const extra of outlineDetail(outline, face.x, face.y, face.width, face.height)) {
|
|
444
|
+
for (const extra of outlineDetail(outline, face.x, face.y, face.width, face.height, look)) {
|
|
441
445
|
parts.push(` <path d="${extra}" fill="none" stroke="${border}"/>`);
|
|
442
446
|
}
|
|
443
447
|
// A leaf's text defaults to the middle of its box, a container's to the top
|
|
@@ -447,6 +451,15 @@ function nodeSvg(node, theme, measurer, fontSize, markup) {
|
|
|
447
451
|
// colors: a badged leaf is a backdrop but its text still centers. A box
|
|
448
452
|
// whose only children are joined to its text is a leaf, and its lines center
|
|
449
453
|
// against each other as a leaf's do.
|
|
454
|
+
// Collate's text sits on the point where its two triangles meet, so the
|
|
455
|
+
// lines break behind it on a patch of its own fill, as a line's text does.
|
|
456
|
+
const patch = node.textAttrs['fill'];
|
|
457
|
+
if (outline === 'flow-collate' && patch !== 'none' && node.lines.length > 0) {
|
|
458
|
+
const box = node.textBox;
|
|
459
|
+
const reach = 4;
|
|
460
|
+
const color = patch !== undefined ? themeColor(patch, theme) : fill === 'none' ? theme.background : fill;
|
|
461
|
+
parts.push(` <rect x="${round(node.x + box.x - reach)}" y="${round(node.y + box.y)}" width="${round(box.width + reach * 2)}" height="${round(box.height)}" fill="${color}"/>`);
|
|
462
|
+
}
|
|
450
463
|
const titled = node.banded && container;
|
|
451
464
|
const textStyle = textStyleFor(node.textAttrs, node.line, titled ? 'start' : 'middle', titled ? 'top-left' : 'center');
|
|
452
465
|
parts.push(sized(textBlock(node.lines, node.x, node.y, textHeight, size, node.textBox, {
|
|
@@ -630,12 +643,15 @@ const FOLD = CORNER * 2;
|
|
|
630
643
|
* corners and a document's three round ones; the fold and the circle are as
|
|
631
644
|
* they are either way.
|
|
632
645
|
*/
|
|
633
|
-
function outlinePath(shape, x, y, w, h, corners) {
|
|
646
|
+
function outlinePath(shape, x, y, w, h, corners, look = {}) {
|
|
634
647
|
const r = CORNER;
|
|
648
|
+
const fold = shape === 'document' ? (look.fold ?? FOLD) : FOLD;
|
|
649
|
+
if (!isBoxy(shape) && !isRound(shape, w, h))
|
|
650
|
+
return placePath(shapeOf(shape, look, w, h).path, x, y);
|
|
635
651
|
if (corners === 'sharp' && shape !== 'circle') {
|
|
636
652
|
return [
|
|
637
653
|
`M${round(x)} ${round(y)}`,
|
|
638
|
-
...(shape === 'document' ? [`H${round(x + w -
|
|
654
|
+
...(shape === 'document' ? [`H${round(x + w - fold)}`, `L${round(x + w)} ${round(y + fold)}`] : [`H${round(x + w)}`]),
|
|
639
655
|
`V${round(y + h)}`,
|
|
640
656
|
`H${round(x)}`,
|
|
641
657
|
'Z',
|
|
@@ -655,8 +671,8 @@ function outlinePath(shape, x, y, w, h, corners) {
|
|
|
655
671
|
// Every corner rounded but the top-right one, which is cut away and folded.
|
|
656
672
|
return [
|
|
657
673
|
`M${round(x + r)} ${round(y)}`,
|
|
658
|
-
`H${round(x + w -
|
|
659
|
-
`L${round(x + w)} ${round(y +
|
|
674
|
+
`H${round(x + w - fold)}`,
|
|
675
|
+
`L${round(x + w)} ${round(y + fold)}`,
|
|
660
676
|
`V${round(y + h - r)}`,
|
|
661
677
|
`a${r} ${r} 0 0 1 ${-r} ${r}`,
|
|
662
678
|
`H${round(x + r)}`,
|
|
@@ -679,14 +695,19 @@ function outlinePath(shape, x, y, w, h, corners) {
|
|
|
679
695
|
'Z',
|
|
680
696
|
].join(' ');
|
|
681
697
|
}
|
|
682
|
-
/** Lines drawn inside the outline: the flap of a fold,
|
|
683
|
-
function outlineDetail(shape, x, y, w, h) {
|
|
684
|
-
|
|
685
|
-
|
|
698
|
+
/** Lines drawn inside the outline: the flap of a fold, a subprocess's bars, a cylinder's near rim. */
|
|
699
|
+
function outlineDetail(shape, x, y, w, h, look = {}) {
|
|
700
|
+
if (shape === 'document') {
|
|
701
|
+
const fold = look.fold ?? FOLD;
|
|
702
|
+
return [`M${round(x + w - fold)} ${round(y)} V${round(y + fold)} H${round(x + w)}`];
|
|
703
|
+
}
|
|
704
|
+
if (shape === 'flow-subprocess') {
|
|
705
|
+
const inset = SUBPROCESS_INSET;
|
|
706
|
+
return [`M${round(x + inset)} ${round(y)} V${round(y + h)} M${round(x + w - inset)} ${round(y)} V${round(y + h)}`];
|
|
707
|
+
}
|
|
708
|
+
if (isBoxy(shape) || isRound(shape, w, h))
|
|
686
709
|
return [];
|
|
687
|
-
return
|
|
688
|
-
`M${round(x + w - FOLD)} ${round(y)} V${round(y + FOLD)} H${round(x + w)}`,
|
|
689
|
-
];
|
|
710
|
+
return shapeOf(shape, look, w, h).detail.map((d) => placePath(d, x, y));
|
|
690
711
|
}
|
|
691
712
|
/** One icon, scaled from its own grid onto a square of `side` at `x, y`. */
|
|
692
713
|
function drawIcon(icon, x, y, side, theme) {
|
|
@@ -1093,15 +1114,19 @@ function markMarker(mark, color, background, free) {
|
|
|
1093
1114
|
function markerId(mark, color, free) {
|
|
1094
1115
|
return `${mark}-${color.replace(/[^a-zA-Z0-9]/g, '')}${free && (mark === 'dot' || mark === 'odot') ? '-free' : ''}`;
|
|
1095
1116
|
}
|
|
1096
|
-
/** The rectangle drawn, and whether it is round. */
|
|
1117
|
+
/** The rectangle drawn, and whether it is round or another shape. */
|
|
1097
1118
|
function faceOf(node) {
|
|
1098
|
-
|
|
1119
|
+
const box = {
|
|
1099
1120
|
x: node.x,
|
|
1100
1121
|
y: node.y,
|
|
1101
1122
|
width: node.width,
|
|
1102
1123
|
height: node.height,
|
|
1103
|
-
round: node.body.kind === 'shape' && node.body.outline
|
|
1124
|
+
round: node.body.kind === 'shape' && isRound(node.body.outline, node.width, node.height),
|
|
1104
1125
|
};
|
|
1126
|
+
if (node.body.kind === 'shape' && !isBoxy(node.body.outline) && !box.round) {
|
|
1127
|
+
box.polygon = shapeOf(node.body.outline, node.body.look, node.width, node.height).polygon.map((p) => ({ x: node.x + p.x, y: node.y + p.y }));
|
|
1128
|
+
}
|
|
1129
|
+
return box;
|
|
1105
1130
|
}
|
|
1106
1131
|
/**
|
|
1107
1132
|
* Wrap a block of text in its own size, but only when that differs from the
|
package/dist/resolve.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, ICON_GAP, ICON_LINES, LINE_WID
|
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
5
5
|
import { bodyFor, pastedIcon } from './icons.js';
|
|
6
6
|
import { monospaceMeasurer } from './measure.js';
|
|
7
|
+
import { isBoxy, middleOf, sizeFor, SUBPROCESS_INSET } from './outline.js';
|
|
7
8
|
import { markupStyles, parseMarkup, plain, splitRuns, wrapLine } from './text.js';
|
|
8
9
|
import { joinedToText } from './model.js';
|
|
9
10
|
import { checkOpacity, gapPixels, Placer, regionTextAt } from './place.js';
|
|
@@ -47,6 +48,11 @@ function factsFor(node, measurer, fontSize) {
|
|
|
47
48
|
}
|
|
48
49
|
pad[side] = gapPixels(written, `"${node.name}" padding-${side}:`, node.line);
|
|
49
50
|
}
|
|
51
|
+
// A subprocess's bars stand inside its sides, so its text keeps its padding clear of them.
|
|
52
|
+
if (node.body.kind === 'shape' && node.body.outline === 'flow-subprocess') {
|
|
53
|
+
pad.left += SUBPROCESS_INSET;
|
|
54
|
+
pad.right += SUBPROCESS_INSET;
|
|
55
|
+
}
|
|
50
56
|
node.textSide = 'center';
|
|
51
57
|
const measures = {
|
|
52
58
|
lineHeight,
|
|
@@ -83,6 +89,26 @@ function factsFor(node, measurer, fontSize) {
|
|
|
83
89
|
circle,
|
|
84
90
|
};
|
|
85
91
|
}
|
|
92
|
+
if (node.body.kind === 'shape' && !circle && !isBoxy(node.body.outline)) {
|
|
93
|
+
if (node.children.length > 0) {
|
|
94
|
+
throw new SourceError(`"${node.name}" is a ${node.body.outline} and has children. Only a rectangle, a document or a circle holds other nodes so far`, node.line);
|
|
95
|
+
}
|
|
96
|
+
// Grown until the padded text fits at the shape's middle: a diamond twice
|
|
97
|
+
// the block each way, a triangle's text a third of the way from its base.
|
|
98
|
+
const block = { width: textWidth + pad.left + pad.right, height: textHeight + pad.top + pad.bottom };
|
|
99
|
+
const natural = sizeFor(node.body.outline, node.body.look, block.width, block.height);
|
|
100
|
+
const middle = middleOf(node.body.outline, node.body.look, natural.width, natural.height);
|
|
101
|
+
return {
|
|
102
|
+
natural,
|
|
103
|
+
textBox: { x: middle.x - textWidth / 2, y: middle.y - textHeight / 2, width: textWidth, height: textHeight },
|
|
104
|
+
hasText,
|
|
105
|
+
padding,
|
|
106
|
+
pad,
|
|
107
|
+
...measures,
|
|
108
|
+
title,
|
|
109
|
+
circle,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
86
112
|
if (layout && node.children.length === 0) {
|
|
87
113
|
throw new SourceError(`"${node.name}" has layout: but no children — a node with no children is not a container`, node.line);
|
|
88
114
|
}
|
|
@@ -611,6 +637,10 @@ function over(base, top) {
|
|
|
611
637
|
if (top['shape'] === undefined && top['icon'] === undefined)
|
|
612
638
|
return { ...base, ...top };
|
|
613
639
|
const { shape: _shape, icon: _icon, ...rest } = base;
|
|
640
|
+
// A shape's settings belong to the shape they were written with.
|
|
641
|
+
for (const key of Object.keys(rest))
|
|
642
|
+
if (key.startsWith('shape.'))
|
|
643
|
+
delete rest[key];
|
|
614
644
|
return { ...rest, ...top };
|
|
615
645
|
}
|
|
616
646
|
/** A file holds one diagram, so a second `diagram` statement is a mistake. */
|
|
@@ -702,6 +732,12 @@ function buildTree(statements, styles, defaults, icons) {
|
|
|
702
732
|
...(stmt.attrs['heading.align'] !== undefined ? { align: stmt.attrs['heading.align'] } : {}),
|
|
703
733
|
}
|
|
704
734
|
: { ...bracketOf('text', appearance), ...stmt.textAttrs };
|
|
735
|
+
// The patch behind a text is a line's text's, and flow-collate's, whose
|
|
736
|
+
// text sits on the point where its two halves meet.
|
|
737
|
+
if (stmt.textAttrs['fill'] !== undefined && !(body.kind === 'shape' && body.outline === 'flow-collate')) {
|
|
738
|
+
throw new SourceError(`"${stmt.name}" has fill: in its text's brackets, which is the patch behind a text on a line or on a flow-collate — ` +
|
|
739
|
+
'a box\'s own color is fill: outside the brackets', stmt.line);
|
|
740
|
+
}
|
|
705
741
|
// A card's text sits at its top left, where it shows above the card in
|
|
706
742
|
// front of it, unless it says where.
|
|
707
743
|
const deck = byName.get(stmt.name.slice(0, Math.max(stmt.name.lastIndexOf('.'), 0)));
|
|
@@ -1113,9 +1149,13 @@ function cornersOf(node, value, written) {
|
|
|
1113
1149
|
if (!NODE_CORNERS.includes(value)) {
|
|
1114
1150
|
throw new SourceError(`"${node.name}" has corners: ${value}, which is not a way to draw corners — use ${NODE_CORNERS.join(' or ')}`, node.line);
|
|
1115
1151
|
}
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1152
|
+
if (!isBoxy(node.body.outline)) {
|
|
1153
|
+
// A circle has no corners, and a diamond's or a hexagon's are drawn as
|
|
1154
|
+
// points; only a box's are rounded. From a style it lands where it can.
|
|
1155
|
+
if (!written)
|
|
1156
|
+
return NODE_CORNERS[0];
|
|
1157
|
+
throw new SourceError(`"${node.name}" is a ${node.body.outline} and has corners: ${value}. Only a box's corners are rounded or sharp — ` +
|
|
1158
|
+
'`corners:` takes a `rectangle`, a `document` or a `flow-subprocess`', node.line);
|
|
1119
1159
|
}
|
|
1120
1160
|
return value;
|
|
1121
1161
|
}
|